Learning Platform
Глоссарий
Troubleshooting

Решение проблем — Python для Data Engineer

Частые ошибки Python от синтаксиса до production-tooling — симптомы, причины и пошаговые решения для Data Engineer.

Область

Категория

Показано 25 из 25 ошибок

Симптомы

  • Python падает на первой строке вложенного блока с `IndentationError: unexpected indent`
  • Код выглядит корректно visually, но интерпретатор отказывается компилировать модуль
  • Ошибка появляется после copy-paste из браузера, документации или другого редактора
  • Editor показывает 4 пробела, но Python tokenizer видит смесь tab/space или non-breaking space (U+00A0)

Причина

Python 3 запрещает смешивать tab и space внутри одного блока (PEP 8 + tokenizer error TabError). При copy-paste из веб-страниц часто заносится non-breaking space (U+00A0) вместо ASCII space (0x20), либо локальные настройки editor'а отличаются от tabstop проекта.

Решение

  1. Включите визуализацию whitespace в editor: VS Code "editor.renderWhitespace": "all", PyCharm — View → Active Editor → Show Whitespaces
  2. Запустите python -X dev script.py — расширенные диагностики покажут точный токен и позицию
  3. Перенаберите проблемный блок вручную, без copy-paste
  4. Создайте .editorconfig в корне проекта: indent_style = space + indent_size = 4 (PEP 8 standard)
  5. Запустите linter: ruff check --select E1,E2,W1 — раскроет mixed indent + non-ASCII whitespace

Связанные уроки:

Симптомы

  • Python 3.x падает с `TabError: inconsistent use of tabs and spaces in indentation` на загрузке модуля
  • Ранее код работал на Python 2 (где смесь была разрешена в нестрогом режиме)
  • Visual diff показывает идентичный отступ, но `cat -A file.py` раскрывает `^I` (tab) вперемешку с пробелами
  • Один и тот же файл фейлится у одного разработчика и работает у другого с разной конфигурацией editor

Причина

PEP 8 рекомендует исключительно пробелы (4 на уровень). Python 3 строже Python 2 и эмитит TabError при любой смеси tab+space внутри одного логического блока. Часто причина — IDE с автоматическим преобразованием (VS Code `editor.detectIndentation: true`) или унаследованный код, частично сконвертированный.

Решение

  1. Запустите python -tt script.py (Python 3 включает строгий режим по умолчанию; флаг сохранён для совместимости)
  2. Конвертируйте все tabs в spaces: expand -t 4 file.py > file.py.tmp && mv file.py.tmp file.py (UNIX)
  3. В VS Code: Ctrl+Shift+P → Convert Indentation to Spaces; в PyCharm: Edit → Convert Indents → To Spaces
  4. Добавьте pre-commit hook с ruff format или black — оба нормализуют отступы автоматически
  5. Проверьте .editorconfig: indent_style = space, indent_size = 4, end_of_line = lf

Связанные уроки:

Симптомы

  • Python падает с `NameError: name 'X' is not defined` при первом обращении к переменной
  • В traceback видно строку с использованием X, но не место определения
  • После `from module import *` имя всё равно не находится — модуль не экспортирует X через `__all__`
  • Ошибка возникает только в части веток if/else или внутри функции (LEGB scope mismatch)

Причина

Имя X не существует в текущей области видимости. Python разрешает имена по правилу LEGB (Local → Enclosing → Global → Built-in) и кидает NameError, если X не найден ни на одном уровне. Частые причины: typo, забытый import, переменная определена только в одной ветке if, использование переменной до её присваивания внутри функции (UnboundLocalError — подкласс NameError в Python 3.11+).

Решение

  1. Проверьте typo: запустите ruff check --select F821 (undefined name) или pyflakes file.py
  2. Убедитесь, что переменная определена ДО первого использования (включая все ветки if/else)
  3. Проверьте импорт: python -c "from module import X; print(X)" — либо имя не существует, либо отсутствует в __all__
  4. Если переменная глобальная, но используется внутри функции — добавьте global X или nonlocal X (для замыканий)
  5. Используйте mypy с --strict или ruff F rules — они ловят undefined-name на этапе static-analysis

Связанные уроки:

Симптомы

  • Python падает с `TypeError: 'NoneType' object is not subscriptable` на выражении вида `result['key']` или `arr[0]`
  • Ранее в trace видна функция, которая должна была вернуть dict/list, но фактически вернула None
  • Ошибка plавающая — иногда происходит, иногда нет (зависит от входных данных)
  • Часто следствие неявного fallthrough в функции: ветка без return, либо return после исключения

Причина

Объект, к которому применяется `[...]`, равен None. Python функция без явного `return` возвращает None — типичная причина: пропущенный return в одной из веток, обработка KeyError/IndexError с возвратом None, либо вызов метода dict.get() без default. Также возникает при цепочках `obj.method().another()` где промежуточный метод вернул None.

Решение

  1. Проверьте функцию-источник: каждая ветка должна возвращать значение явно — return result (не падать в неявный return None)
  2. Используйте dict.get(key, default) вместо dict[key] для безопасного доступа с fallback
  3. Добавьте guard: if result is None: raise ValueError('expected non-None'); return result[...]
  4. Включите mypy --strict + Optional[Dict] annotations — type checker поймает None-flow на этапе анализа
  5. Используйте walrus operator для inline-проверки: if (val := func()) is not None: process(val[0])

Связанные уроки:

Симптомы

  • Python падает с `ModuleNotFoundError: No module named 'X'` при `import X`
  • `pip list | grep X` ничего не возвращает в активном venv
  • Ошибка возникает после переключения venv, Python версии или CI runner
  • Импорт работает в одной директории, но фейлится из другой (PYTHONPATH разный)

Причина

Модуль X не установлен в текущем virtual environment, либо установлен в другую Python-версию (например, system python3 vs venv python3.12). PYTHONPATH не включает директорию с модулем. Возможен конфликт с одноимённым local file (X.py в cwd shadowing installed package — Python ищет cwd первым).

Решение

  1. Активируйте правильный venv: source .venv/bin/activate (Linux/macOS) или .venv\Scripts\activate (Windows)
  2. Установите модуль: pip install X или быстрее через uv pip install X
  3. Проверьте sys.path: python -c "import sys; print('\n'.join(sys.path))" — модуль должен быть в одной из директорий
  4. Проверьте конфликт имён: убедитесь, что в cwd нет файла X.py или директории X/, перекрывающих установленный пакет
  5. Если устанавливаете локальный пакет: pip install -e . из корня проекта с pyproject.toml

Связанные уроки:

Симптомы

  • Python падает с `ImportError: cannot import name 'Y' from 'X' (/path/to/X/__init__.py)`
  • Сам модуль X импортируется успешно, но конкретное имя Y отсутствует
  • Версия X установлена, но Y был добавлен/удалён в другой версии (API drift)
  • После `pip install --upgrade X` старый код перестал работать — breaking change в minor/patch версии

Причина

Имя Y не экспортируется из модуля X в установленной версии. Возможные причины: (1) Y был перенесён/переименован между версиями (например, `from collections import Mapping` → `from collections.abc import Mapping` в 3.10+); (2) старая версия X не имела Y; (3) ошибка в `__init__.py` (Y не реэкспортирован); (4) circular import — Y определён, но модуль ещё не дошёл до его создания.

Решение

  1. Проверьте версию пакета: pip show X и сравните с docs.python.org или changelog проекта
  2. Закрепите версию: pip install 'X>=1.2,<2.0' в requirements.txt или pyproject.toml dependencies
  3. Найдите символ: python -c "import X; print([n for n in dir(X) if 'Y' in n])" — может быть переименован
  4. Используйте miграционный alias: try: from X import Y\nexcept ImportError: from X.submodule import Y
  5. Прочитайте changelog X между установленной и ожидаемой версией — breaking changes часто документированы

Связанные уроки:

Симптомы

  • Python падает с `ImportError: cannot import name 'X' from partially initialized module 'M'`
  • Сообщение явно упоминает `(most likely due to a circular import)` — Python 3.5+ raycing detection
  • Ошибка возникает только при определённом порядке импорта (запуск `python -m M` vs `python script.py`)
  • Удаление одного из импортов или перемещение его внутрь функции устраняет проблему

Причина

Модули A и B импортируют друг друга на top-level. Когда A импортирует B, B пытается импортировать A — но A ещё не закончил инициализацию (его module dict частично пуст). Python видит `partially initialized module` и кидает ImportError. Архитектурный smell — обычно нарушение Single Responsibility или забытое выделение общего слоя.

Решение

  1. Переместите проблемный import внутрь функции/метода (lazy import) — модуль импортируется при первом вызове, когда оба готовы
  2. Используйте if TYPE_CHECKING: блок (PEP 484) для type-only импортов — не выполняется в runtime
  3. Выделите общий код в третий модуль C, который импортируют и A и B — устранит cycle архитектурно
  4. Применяйте dependency injection: вместо импорта класса передавайте его как параметр
  5. Проверьте граф зависимостей: pydeps src/ --max-bacon 2 --pylib-all визуализирует cycles в проекте

Связанные уроки:

Симптомы

  • Python падает с `RecursionError: maximum recursion depth exceeded in comparison` (или другой context)
  • Stack trace содержит сотни одинаковых фреймов одной функции
  • Лимит достигается при определённом размере входных данных (рекурсивный обход дерева/графа)
  • Замена на iteratiьный код устраняет ошибку, но переписать сложно

Причина

Python имеет жёсткий лимит глубины стека вызовов (`sys.getrecursionlimit()` — обычно 1000). Лимит защищает от stack overflow процесса. Python (CPython) НЕ оптимизирует tail calls, поэтому глубокая рекурсия не превращается в цикл автоматически. При обходе deep tree / linked list / parsing рекурсия достигает лимита.

Решение

  1. Перепишите алгоритм итеративно: используйте explicit stack/queue (collections.deque) вместо вызова себя
  2. Увеличьте лимит осторожно: sys.setrecursionlimit(10000) — но риск real stack overflow процесса (segfault)
  3. Запустите в потоке с увеличенным stack size: threading.Thread(target=fn).start() после threading.stack_size(64*1024*1024)
  4. Используйте генераторы вместо рекурсии для lazy iteration: yield from делегирует без углубления стека
  5. Для динамики используйте memoization (@functools.lru_cache) — уменьшает число рекурсивных вызовов экспоненциально

Симптомы

  • Python падает с `MemoryError` или процесс убивается OOM killer'ом (`Killed` без traceback)
  • RSS процесса монотонно растёт со временем, не освобождается после окончания обработки batch'а
  • `tracemalloc.take_snapshot().statistics('lineno')` показывает аккумулирующиеся allocation на одной строке
  • Memory profile (`mprof run script.py && mprof plot`) показывает sawtooth pattern или линейный рост

Причина

Утечка памяти — объекты не освобождаются GC из-за reference cycle или удерживания в long-lived collection (cache, list, dict). Также частая причина: чтение большого файла целиком (`f.read()`) вместо потокового; использование list comprehension там, где нужен generator. Reference cycle с `__del__` методом блокирует cyclic GC до Python 3.4.

Решение

  1. Профилируйте: python -m tracemalloc script.py или pip install memory-profiler && mprof run script.py
  2. Замените list comprehension на generator: (x*2 for x in data) вместо [x*2 for x in data] для streaming
  3. Читайте файлы потоково: for line in open(path): ... не загружает весь файл в RAM
  4. Очищайте long-lived caches явно: cache.clear() или используйте weakref.WeakValueDictionary
  5. Запустите cyclic GC принудительно: import gc; gc.collect() — освобождает reference cycles

Связанные уроки:

Симптомы

  • Python падает с `KeyError: 'user_id'` или `KeyError: 0` при обращении `d[key]`
  • Trace показывает строку с `[...]`, но не место создания dict
  • Ошибка plавающая — зависит от входных данных (некоторые записи не содержат поле)
  • После `json.loads(response)` ключ ожидается, но API вернул другой schema (drift)

Причина

Ключ отсутствует в dict. CPython реализует dict через open-addressing hash table (PEP 468 compact dict с Python 3.6+). При лишнем `[]` без проверки наличия Python кидает KeyError. Частые причины: typo в имени ключа, несоответствие schema (API вернул другую структуру), case mismatch (`'User_ID'` vs `'user_id'`), JSON deserializeрует `null` в `None` (ключ есть, но значение None).

Решение

  1. Используйте dict.get(key, default) для безопасного доступа: d.get('user_id', 0) возвращает 0 если ключа нет
  2. Проверьте наличие явно: if key in d: process(d[key]) — Pythonic check, O(1) lookup
  3. Используйте collections.defaultdict(list) для аккумуляции: создаёт default при первом обращении
  4. Применяйте dataclasses или pydantic.BaseModel для structured data — валидация schema на этапе создания
  5. Проверяйте API response: assert 'user_id' in response, f'Unexpected schema: {list(response.keys())}'

Связанные уроки:

Симптомы

  • Python падает с `IndexError: list index out of range` на выражении `lst[i]`
  • Ошибка возникает на границе: i == len(lst) (off-by-one) или на пустом списке (i == 0)
  • Trace показывает индекс, но не содержимое списка — не видно, какой длины он был
  • Иногда возникает после `lst.pop()` или фильтрации, уменьшившей размер

Причина

Индекс i выходит за границы списка [0, len(lst)). CPython list реализован как dynamic array (PyListObject с over-allocation): хранит ob_item (массив указателей) и Py_SIZE (текущая длина). При `lst[i]` Python проверяет `0 <= i < Py_SIZE` (с поддержкой negative indexing) и кидает IndexError если нарушено. Частые причины: off-by-one, пустой список, обращение к индексу после удаления элементов в цикле.

Решение

  1. Проверяйте границы явно: if i < len(lst): process(lst[i]) — defensive programming
  2. Используйте try/except IndexError для graceful fallback на default-значение
  3. Применяйте slicing — он не кидает IndexError: lst[i:i+1] возвращает [] если i вне границ
  4. Для итерации используйте for item in lst или enumerate(lst) — Python сам управляет индексом
  5. Не модифицируйте список во время итерации: создайте новый или используйте lst[:] = [x for x in lst if cond]

Связанные уроки:

Симптомы

  • Функция со списочным/словарным default аргументом аккумулирует данные между вызовами
  • `def append_to(item, lst=[]): lst.append(item); return lst` — второй вызов возвращает не пустой список
  • Тесты pytest проходят изолированно, но фейлятся в suite (порядок зависимостей)
  • После рефакторинга на `def f(x=None): if x is None: x = []` баг исчезает

Причина

Default arguments вычисляются ОДИН раз на этапе определения функции (когда `def` выполняется), а не при каждом вызове. Для immutable types (int, str, tuple) это безопасно. Для mutable (list, dict, set) — один и тот же объект разделяется между всеми вызовами. CPython хранит defaults в `func.__defaults__` tuple — модификация мутабельного default видна всем последующим вызовам.

Решение

  1. Используйте sentinel None и создавайте mutable внутри: def f(x=None): x = [] if x is None else x
  2. Для dataclasses используйте field(default_factory=list) вместо field(default=[])
  3. Включите ruff rule B006 (mutable-argument-default) или pylint W0102 — поймают на этапе lint
  4. Используйте type-immutable структуры: tuple вместо list, frozenset вместо set когда возможно
  5. Проверяйте f.__defaults__ при отладке: содержит фактические default-объекты функции

Связанные уроки:

Симптомы

  • Python падает с `UnicodeDecodeError: 'utf-8' codec can't decode byte 0xXX in position N`
  • Файл открывается без явной encoding и Python использует locale-зависимый default (cp1251 на Windows-RU, utf-8 на Linux)
  • Ошибка plавает между OS: работает локально, фейлится в Docker / CI
  • После `open(path, 'rb')` (binary mode) чтение работает, но потом не получается декодировать в str

Причина

Байты в файле не валидны как UTF-8 (либо файл в другой кодировке — cp1251/latin1/koi8-r, либо файл бинарный). Python 3 разделяет str (Unicode codepoints, PEP 393 flexible representation) и bytes. При `open(path)` без encoding Python использует `locale.getpreferredencoding()` — может отличаться от фактической кодировки файла. UnicodeDecodeError возникает на первом не-валидном байте.

Решение

  1. Всегда указывайте encoding явно: open(path, encoding='utf-8') (или cp1251, latin1)
  2. Определите кодировку: pip install chardet && python -c "import chardet; print(chardet.detect(open(path,'rb').read(10000)))"
  3. Используйте errors='replace' или errors='ignore' для tolerant чтения: open(path, encoding='utf-8', errors='replace') — заменит invalid bytes на U+FFFD
  4. Для смешанных данных читайте в bytes, декодируйте по chunks: f.read().decode('utf-8', errors='strict')
  5. Установите PYTHONUTF8=1 для consistent UTF-8 mode (PEP 540) на всех OS

Симптомы

  • Python падает с `TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B`
  • Ошибка возникает на этапе определения класса (выполнения `class` statement), не при инстанциации
  • Diamond-inheritance: класс наследует от двух базовых, которые сами имеют общего предка в несовместимом порядке
  • После `cls.__mro__` или `cls.mro()` видно цепочку, но C3 linearization не может её упорядочить

Причина

Python 3 использует C3 linearization алгоритм (PEP 3119) для построения MRO. C3 требует, чтобы порядок базовых классов сохранялся монотонно во всей иерархии — `linearize(C(A, B)) = C + merge(linearize(A), linearize(B), [A, B])`. Если A и B в разных частях иерархии указаны в противоречивом порядке (A до B и B до A одновременно), merge не сходится — Python кидает TypeError. Типичный smell — diamond inheritance с противоречивыми порядками baz.

Решение

  1. Проверьте Class.__mro__ каждого базового класса — найдите conflict: где A и B в разных порядках
  2. Перестройте иерархию: измените порядок базовых классов в одном из определений, чтобы согласовать с другим
  3. Используйте composition вместо multiple inheritance: agg.process() через self.helper = HelperClass()
  4. Применяйте mixins только для horizontal-функциональности, не для domain-классов
  5. Документируйте super().__init__() chain: каждый класс должен вызывать super, даже Mixins

Связанные уроки:

Симптомы

  • `pip install .` или `python -m build` падает с `ModuleNotFoundError: No module named 'setuptools.build_meta'` или подобным
  • pyproject.toml содержит `[project]`, но не `[build-system]`
  • Старый `setup.py` удалён, новый pyproject.toml неполный — переходный артефакт
  • Установка работает на dev-машине, но фейлится в CI с чистым venv

Причина

PEP 517 требует, чтобы pyproject.toml содержал секцию `[build-system]` с `requires` и `build-backend`. Без этой секции pip не знает, какой backend использовать для сборки sdist/wheel. Раньше setuptools был implicit-default, но PEP 517/518/621 сделали backend explicit. Частые причины: copy-paste pyproject.toml без [build-system], миграция со setup.py не завершена.

Решение

  1. Добавьте секцию [build-system] в pyproject.toml: requires = ["setuptools>=61", "wheel"] + build-backend = "setuptools.build_meta"
  2. Для hatchling (рекомендуется): requires = ["hatchling"] + build-backend = "hatchling.build"
  3. Для poetry: requires = ["poetry-core>=1.0.0"] + build-backend = "poetry.core.masonry.api"
  4. Установите backend в build-окружение: pip install build && python -m build создаст sdist+wheel в dist/
  5. Проверьте: pip install -e . работает локально, потом — CI с pip install build && python -m build && pip install dist/*.whl

Связанные уроки:

Симптомы

  • `pip install pkg` падает с длинным backtrace и финальным `ResolutionImpossible`
  • Сообщение: `package A requires X>=1.0, but you have X==0.9` или `X<2.0` vs `Y requires X>=2.0`
  • После установки `pip check` показывает несовместимости пакетов
  • Иногда установка завершается, но импорт фейлится: `ImportError: cannot import name 'Y' from 'X' (you may need to install ...)`

Причина

Pip resolver не выполняет полный backtracking SAT-solving (в отличие от uv / poetry). При несовместимых constraint'ах между transitive dependencies pip выбирает версии greedy и оставляет конфликт. PEP 440 version specifiers могут быть неоднозначны в edge-cases (например, `>=1.0` + `<2.0` + `!=1.5` через 3 пакета). Также проблема — заброшенные пакеты с устаревшими constraint'ами на зависимости.

Решение

  1. Используйте uv (рекомендуется): pip install uv && uv pip install pkg — Rust-based proper resolver, в 10-100× быстрее + полный backtracking
  2. Создайте чистое venv: python -m venv .venv-clean && source .venv-clean/bin/activate && pip install pkg
  3. Закрепите версии в lockfile: pip install pip-tools && pip-compile requirements.in -o requirements.txt
  4. Для production используйте pyproject.toml + uv lock — создаёт reproducible uv.lock со всеми transitive версиями
  5. В крайнем случае: pip install --use-deprecated=legacy-resolver pkg (быстрый, но небезопасный — оставит broken deps)

Связанные уроки:

Симптомы

  • После `source .venv/bin/activate` команда `which python` возвращает `/usr/bin/python3` вместо `.venv/bin/python`
  • `pip install pkg` устанавливает в global site-packages — нужны sudo или фейлится с PermissionError
  • `echo $VIRTUAL_ENV` пустой или указывает на другой venv
  • VS Code / PyCharm показывает interpreter path не из активного venv

Причина

Активация venv не выполнилась корректно. Возможные причины: (1) запущен `bash` поверх `zsh` после activate (parent shell сохраняет PATH); (2) использован Windows-стиль на Linux (`activate.bat` вместо `activate`); (3) прописан `alias python=...` в .bashrc, перекрывающий venv; (4) shell hash cache держит старый путь — нужен `hash -r`. activate работает через модификацию `$PATH` (добавляет .venv/bin в начало) и установку `$VIRTUAL_ENV`.

Решение

  1. Активируйте корректно для shell: bash/zsh — source .venv/bin/activate, fish — source .venv/bin/activate.fish, Windows PowerShell — .venv\Scripts\Activate.ps1
  2. После активации: hash -r (bash/zsh) — сбросит cache путей; which python должен показать .venv/bin/python
  3. Проверьте $VIRTUAL_ENV: echo $VIRTUAL_ENV должен совпадать с путём к venv
  4. Удалите конфликтующие aliases из shell rc: unalias python pip или закомментируйте в .bashrc/.zshrc
  5. Используйте direnv (brew install direnv) — автоматически активирует venv при входе в директорию через .envrc

Связанные уроки:

Симптомы

  • `pip install pkg` падает с `error: command 'gcc' failed` или `Python.h: No such file or directory`
  • Падение происходит на этапе сборки wheel из sdist (когда binary wheel недоступен для платформы)
  • На macOS: `xcrun: error: invalid active developer path` — Xcode CLI tools не установлены
  • На Linux: `gcc: command not found` — build-essential / gcc package отсутствует

Причина

Пакет содержит C extension (например, numpy, lxml, psycopg2-binary). PyPI обычно имеет prebuilt wheels для популярных платформ (manylinux, macos, win), но для редких комбинаций (musl, ARM, новый Python) wheel недоступен — pip собирает из sdist, требуя C compiler + Python development headers (Python.h в `python3-dev` пакете).

Решение

  1. Установите C build toolchain: Linux — apt install build-essential python3-dev; macOS — xcode-select --install; Windows — pip install --upgrade pip (обычно включает MSVC build tools)
  2. Используйте binary-only wheel: pip install --only-binary :all: pkg — pip не будет собирать из sdist
  3. Для psycopg2 используйте psycopg2-binary (precompiled wheel) вместо psycopg2
  4. На macos с Apple Silicon: arch -arm64 pip install pkg (для нативных wheels) или arch -x86_64 через Rosetta
  5. Альтернатива — Docker с official python image: docker run python:3.12-slim pip install pkg (Linux build environment гарантирован)

Связанные уроки:

Симптомы

  • `pytest` завершается с `collected 0 items` и exit code 5 (no tests collected)
  • Тестовые файлы существуют, но pytest их не находит
  • Файлы названы не по конвенции (`my_test.py` вместо `test_my.py`) или функции без префикса `test_`
  • После добавления `[tool.pytest.ini_options]` в pyproject.toml ничего не изменилось

Причина

Pytest discovery работает по строгим конвенциям (можно переопределить через config): test files должны называться `test_*.py` или `*_test.py`; функции — `test_*`; классы — `Test*` (без `__init__`). Иначе pytest их игнорирует. Также: тесты в директории без `__init__.py` могут конфликтовать с rootdir; conftest.py не подхватывается, если он выше cwd.

Решение

  1. Переименуйте файлы: my_tests.pytest_my.py; функции check_xxx()test_xxx()
  2. Запустите с verbose collection: pytest --collect-only -q — покажет, какие файлы найдены
  3. Проверьте rootdir: pytest --rootdir=. -v — pytest должен находить ваш pyproject.toml/setup.cfg как rootdir
  4. Настройте discovery через pyproject.toml: [tool.pytest.ini_options] + testpaths = ["tests"] + python_files = ["test_*.py"]
  5. Убедитесь, что conftest.py находится в rootdir или выше тестов; наличие __init__.py в test-директориях контролирует import mode

Связанные уроки:

Симптомы

  • `pytest` падает с `fixture 'db' not found` при запуске теста
  • В trace: `available fixtures: tmp_path, capsys, monkeypatch, ...` — но кастомного `db` нет
  • Или: `ScopeMismatch: You tried to access the function scoped fixture X with a session scoped request object`
  • Fixture определён в одном тесте, но не найден в другом (conftest.py отсутствует или в неправильной директории)

Причина

Pytest fixtures видны только в области их определения: fixture в `test_a.py` НЕ доступен в `test_b.py`. Для шаринга используется `conftest.py` — он автоматически подхватывается во всех тестах в этой и подпапках. ScopeMismatch — fixture с более широким scope (session) пытается использовать fixture с более узким (function): session-fixture создаётся раз на весь run, но запрашивает function-fixture, который пересоздаётся каждый тест.

Решение

  1. Перенесите общие fixtures в tests/conftest.py — будут доступны во всех тестах автоматически
  2. Не импортируйте fixtures явно — pytest сам их инжектит по имени параметра
  3. Согласуйте scopes: session > module > class > function. Fixture может зависеть только от равного или более широкого scope
  4. Проверьте структуру: pytest --fixtures tests/ — покажет все доступные fixtures для конкретной директории
  5. Если нужна изоляция между тестами с session-scoped ресурсом — используйте truncate/reset внутри function-scoped wrapper

Связанные уроки:

Симптомы

  • `mypy src/` сообщает `error: Argument 1 to "f" has incompatible type "int"; expected "str" [arg-type]`
  • Код выполняется без ошибок в runtime (annotations не проверяются Python'ом), но mypy фейлит CI
  • После `mypy --strict` появляются десятки новых ошибок (Optional, Any, missing return types)
  • Третья сторона (typeshed stub) сообщает о несоответствии, но реальный API работает

Причина

Mypy выполняет static type checking на основе annotations PEP 484/585/604. Python НЕ проверяет типы в runtime — это задача mypy/pyright. Несовместимость может означать реальный баг (передаём int вместо str) или ложное срабатывание (Optional не сужен через `is not None`, протокол не указан, generic не специализирован). Modern syntax (PEP 585 `list[int]`, PEP 604 `int | str`) требует Python 3.9+/3.10+ соответственно.

Решение

  1. Сузьте Optional: if x is not None: f(x) или assert x is not None — mypy понимает narrowing
  2. Используйте cast(int, value) или # type: ignore[arg-type] для целевых suppressions с обоснованием
  3. Установите stubs для third-party: pip install types-requests types-pyyaml или используйте mypy --install-types
  4. Включите --strict-optional и --warn-return-any постепенно — поэтапная миграция через per-file [mypy-module.*]
  5. Проверяйте локально перед commit: mypy --strict src/ или интегрируйте в pre-commit hook

Связанные уроки:

Симптомы

  • `logger.info('msg')` ничего не выводит в stdout/файл — `print()` работает, logger молчит
  • Логи появляются в одной библиотеке, но не в другой (разный logger config)
  • `logging.basicConfig()` вызван, но всё равно тишина — кто-то уже инициализировал root logger ранее
  • После `logger.setLevel(DEBUG)` ничего не изменилось — handler ниже фильтрует уровень

Причина

Python logging построен на иерархии loggers (root → app → app.module). Каждый logger имеет уровень + handlers. Сообщение пропускается если: (1) уровень logger выше severity (`logger.warning('x')` при level=ERROR пропадёт); (2) handler уровень фильтрует ниже logger; (3) `logger.propagate = False` отключает bubbling к root; (4) handlers не привязаны (особенно к non-root logger). `logging.basicConfig()` работает только если root logger ещё без handlers — повторный вызов NoOp.

Решение

  1. Установите уровни корректно: logging.getLogger().setLevel(logging.DEBUG) (root) + handler с setLevel(DEBUG)
  2. Используйте logging.basicConfig(level=DEBUG, force=True) (Python 3.8+) — force=True пересоздаёт handlers
  3. Проверьте propagate: logging.getLogger('app.module').propagate — должно быть True (default)
  4. Дебаг иерархии: logging.getLogger('app').handlers и .level для каждого уровня цепочки
  5. Используйте dictConfig для централизации: logging.config.dictConfig(config_dict) гарантирует replication

Связанные уроки:

Симптомы

  • Цикл с `lst.index(item)` или `item in lst` для больших lst занимает минуты вместо секунд
  • `cProfile` показывает `list.index` как доминирующий cumulative time
  • Время выполнения растёт квадратично с размером данных (10× данных = 100× времени)
  • Замена list на set/dict даёт 100-1000× ускорение на тех же входных данных

Причина

list реализован как dynamic array (PyListObject — contiguous PyObject* pointers). `lst.index(x)` и `x in lst` сканируют linear — O(n). Внутри цикла O(n) это превращается в O(n²) total. set/dict реализованы через hash table (open addressing) — `x in s` это O(1) amortized. Для повторяющихся membership-tests / lookups list — антипаттерн.

Решение

  1. Замените list на set для membership: s = set(lst); for x in items: if x in s: ... — O(1) per check
  2. Для key→value lookup используйте dict: d = {item.id: item for item in lst} — O(1) get
  3. Профилируйте: python -m cProfile -s cumulative script.py | head -30 — раскроет O(n²) циклы
  4. Используйте collections.Counter для подсчёта частот: Counter(items).most_common(10) — O(n) total
  5. Микробенчмаркинг: python -m timeit -s 'lst=list(range(10000))' '5000 in lst' vs то же для set

Симптомы

  • Python падает с `RuntimeError: This event loop is already running` при `asyncio.run(coro())`
  • Ошибка возникает в Jupyter notebook, IPython или nested context (FastAPI startup, существующий loop)
  • В синхронном коде вызывается `loop.run_until_complete()`, хотя loop уже запущен parent'ом
  • `asyncio.get_event_loop()` deprecation warning + behavioural change между Python 3.10 и 3.12

Причина

asyncio модель: только один event loop может быть active per thread в каждый момент. `asyncio.run()` создаёт НОВЫЙ loop, выполняет coroutine и закрывает — нельзя вложить в уже запущенный loop. Jupyter использует tornado/asyncio loop под капотом, поэтому `asyncio.run()` в cell конфликтует. Python 3.10+ deprecation: `get_event_loop()` без running loop кидает DeprecationWarning (3.12+ — ошибка).

Решение

  1. В Jupyter: await coro() напрямую (cell поддерживает top-level await) — не оборачивайте asyncio.run()
  2. Установите nest-asyncio: pip install nest_asyncio && nest_asyncio.apply() — патчит asyncio для re-entrancy
  3. Внутри запущенного loop используйте asyncio.create_task(coro()) — добавит task в текущий loop
  4. В FastAPI / aiohttp: НЕ вызывайте asyncio.run() — фреймворк сам управляет loop
  5. Для верификации запущенности: asyncio.get_running_loop() (3.7+) кидает RuntimeError если loop неактивен — отличает от get_event_loop()

Связанные уроки:

Симптомы

  • `concurrent.futures.ThreadPoolExecutor` для CPU-bound кода — производительность хуже single-thread
  • `htop` показывает 100% CPU на одном ядре, остальные ядра простаивают
  • `cProfile` показывает основное время в Python bytecode (а не в C extension)
  • Замена threading на multiprocessing даёт линейное ускорение, но threading не ускоряет

Причина

GIL (Global Interpreter Lock) в CPython гарантирует, что только один поток выполняет Python bytecode в каждый момент. Для CPU-bound (чистый Python) threading НЕ даёт parallelism — потоки переключаются (каждые 100 bytecode инструкций или 5ms по умолчанию), но не работают параллельно. Threading помогает только для I/O-bound (network, disk) — пока один поток ждёт I/O, GIL отпускается для другого. C extensions могут отпускать GIL вручную (numpy operations).

Решение

  1. Для CPU-bound: используйте multiprocessing.Pool или concurrent.futures.ProcessPoolExecutor — каждый процесс имеет свой GIL
  2. Для I/O-bound: threading или asyncio — оба выпускают GIL во время I/O wait (asyncio предпочтительнее для high concurrency)
  3. Для numerical CPU-bound: NumPy / Polars / numba — операции выполняются в C без GIL (vectorized)
  4. Python 3.13t (free-threaded build, PEP 703) — экспериментальный no-GIL режим; production-ready ожидается 3.14+
  5. Профилируйте GIL contention: pip install py-spy && py-spy record --gil --rate 100 -- python script.py — раскроет % времени GIL заблокирован

Связанные уроки: