Решение проблем — Python для Data Engineer
Частые ошибки Python от синтаксиса до production-tooling — симптомы, причины и пошаговые решения для Data Engineer.
Область
Категория
Симптомы
- 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 проекта.
Решение
- Включите визуализацию whitespace в editor: VS Code
"editor.renderWhitespace": "all", PyCharm —View → Active Editor → Show Whitespaces - Запустите
python -X dev script.py— расширенные диагностики покажут точный токен и позицию - Перенаберите проблемный блок вручную, без copy-paste
- Создайте
.editorconfigв корне проекта:indent_style = space+indent_size = 4(PEP 8 standard) - Запустите 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`) или унаследованный код, частично сконвертированный.
Решение
- Запустите
python -tt script.py(Python 3 включает строгий режим по умолчанию; флаг сохранён для совместимости) - Конвертируйте все tabs в spaces:
expand -t 4 file.py > file.py.tmp && mv file.py.tmp file.py(UNIX) - В VS Code:
Ctrl+Shift+P → Convert Indentation to Spaces; в PyCharm:Edit → Convert Indents → To Spaces - Добавьте pre-commit hook с
ruff formatилиblack— оба нормализуют отступы автоматически - Проверьте
.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+).
Решение
- Проверьте typo: запустите
ruff check --select F821(undefined name) илиpyflakes file.py - Убедитесь, что переменная определена ДО первого использования (включая все ветки if/else)
- Проверьте импорт:
python -c "from module import X; print(X)"— либо имя не существует, либо отсутствует в__all__ - Если переменная глобальная, но используется внутри функции — добавьте
global Xилиnonlocal X(для замыканий) - Используйте mypy с
--strictили ruffFrules — они ловят 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.
Решение
- Проверьте функцию-источник: каждая ветка должна возвращать значение явно —
return result(не падать в неявныйreturn None) - Используйте
dict.get(key, default)вместоdict[key]для безопасного доступа с fallback - Добавьте guard:
if result is None: raise ValueError('expected non-None'); return result[...] - Включите mypy
--strict+Optional[Dict]annotations — type checker поймает None-flow на этапе анализа - Используйте 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 первым).
Решение
- Активируйте правильный venv:
source .venv/bin/activate(Linux/macOS) или.venv\Scripts\activate(Windows) - Установите модуль:
pip install Xили быстрее черезuv pip install X - Проверьте sys.path:
python -c "import sys; print('\n'.join(sys.path))"— модуль должен быть в одной из директорий - Проверьте конфликт имён: убедитесь, что в cwd нет файла
X.pyили директорииX/, перекрывающих установленный пакет - Если устанавливаете локальный пакет:
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 определён, но модуль ещё не дошёл до его создания.
Решение
- Проверьте версию пакета:
pip show Xи сравните с docs.python.org или changelog проекта - Закрепите версию:
pip install 'X>=1.2,<2.0'в requirements.txt или pyproject.tomldependencies - Найдите символ:
python -c "import X; print([n for n in dir(X) if 'Y' in n])"— может быть переименован - Используйте miграционный alias:
try: from X import Y\nexcept ImportError: from X.submodule import Y - Прочитайте 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 или забытое выделение общего слоя.
Решение
- Переместите проблемный import внутрь функции/метода (lazy import) — модуль импортируется при первом вызове, когда оба готовы
- Используйте
if TYPE_CHECKING:блок (PEP 484) для type-only импортов — не выполняется в runtime - Выделите общий код в третий модуль C, который импортируют и A и B — устранит cycle архитектурно
- Применяйте dependency injection: вместо импорта класса передавайте его как параметр
- Проверьте граф зависимостей:
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 рекурсия достигает лимита.
Решение
- Перепишите алгоритм итеративно: используйте explicit stack/queue (
collections.deque) вместо вызова себя - Увеличьте лимит осторожно:
sys.setrecursionlimit(10000)— но риск real stack overflow процесса (segfault) - Запустите в потоке с увеличенным stack size:
threading.Thread(target=fn).start()послеthreading.stack_size(64*1024*1024) - Используйте генераторы вместо рекурсии для lazy iteration:
yield fromделегирует без углубления стека - Для динамики используйте 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.
Решение
- Профилируйте:
python -m tracemalloc script.pyилиpip install memory-profiler && mprof run script.py - Замените list comprehension на generator:
(x*2 for x in data)вместо[x*2 for x in data]для streaming - Читайте файлы потоково:
for line in open(path): ...не загружает весь файл в RAM - Очищайте long-lived caches явно:
cache.clear()или используйтеweakref.WeakValueDictionary - Запустите 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).
Решение
- Используйте
dict.get(key, default)для безопасного доступа:d.get('user_id', 0)возвращает 0 если ключа нет - Проверьте наличие явно:
if key in d: process(d[key])— Pythonic check, O(1) lookup - Используйте
collections.defaultdict(list)для аккумуляции: создаёт default при первом обращении - Применяйте
dataclassesилиpydantic.BaseModelдля structured data — валидация schema на этапе создания - Проверяйте 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, пустой список, обращение к индексу после удаления элементов в цикле.
Решение
- Проверяйте границы явно:
if i < len(lst): process(lst[i])— defensive programming - Используйте
try/except IndexErrorдля graceful fallback на default-значение - Применяйте slicing — он не кидает IndexError:
lst[i:i+1]возвращает [] если i вне границ - Для итерации используйте
for item in lstилиenumerate(lst)— Python сам управляет индексом - Не модифицируйте список во время итерации: создайте новый или используйте
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 видна всем последующим вызовам.
Решение
- Используйте sentinel
Noneи создавайте mutable внутри:def f(x=None): x = [] if x is None else x - Для dataclasses используйте
field(default_factory=list)вместоfield(default=[]) - Включите ruff rule
B006(mutable-argument-default) или pylintW0102— поймают на этапе lint - Используйте type-immutable структуры:
tupleвместо list,frozensetвместо set когда возможно - Проверяйте
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 возникает на первом не-валидном байте.
Решение
- Всегда указывайте encoding явно:
open(path, encoding='utf-8')(илиcp1251,latin1) - Определите кодировку:
pip install chardet && python -c "import chardet; print(chardet.detect(open(path,'rb').read(10000)))" - Используйте
errors='replace'илиerrors='ignore'для tolerant чтения:open(path, encoding='utf-8', errors='replace')— заменит invalid bytes на U+FFFD - Для смешанных данных читайте в bytes, декодируйте по chunks:
f.read().decode('utf-8', errors='strict') - Установите
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.
Решение
- Проверьте
Class.__mro__каждого базового класса — найдите conflict: где A и B в разных порядках - Перестройте иерархию: измените порядок базовых классов в одном из определений, чтобы согласовать с другим
- Используйте composition вместо multiple inheritance: agg.process() через
self.helper = HelperClass() - Применяйте mixins только для horizontal-функциональности, не для domain-классов
- Документируйте
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 не завершена.
Решение
- Добавьте секцию [build-system] в pyproject.toml:
requires = ["setuptools>=61", "wheel"]+build-backend = "setuptools.build_meta" - Для hatchling (рекомендуется):
requires = ["hatchling"]+build-backend = "hatchling.build" - Для poetry:
requires = ["poetry-core>=1.0.0"]+build-backend = "poetry.core.masonry.api" - Установите backend в build-окружение:
pip install build && python -m buildсоздаст sdist+wheel в dist/ - Проверьте:
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'ами на зависимости.
Решение
- Используйте uv (рекомендуется):
pip install uv && uv pip install pkg— Rust-based proper resolver, в 10-100× быстрее + полный backtracking - Создайте чистое venv:
python -m venv .venv-clean && source .venv-clean/bin/activate && pip install pkg - Закрепите версии в lockfile:
pip install pip-tools && pip-compile requirements.in -o requirements.txt - Для production используйте
pyproject.toml+uv lock— создаёт reproducibleuv.lockсо всеми transitive версиями - В крайнем случае:
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`.
Решение
- Активируйте корректно для shell: bash/zsh —
source .venv/bin/activate, fish —source .venv/bin/activate.fish, Windows PowerShell —.venv\Scripts\Activate.ps1 - После активации:
hash -r(bash/zsh) — сбросит cache путей;which pythonдолжен показать.venv/bin/python - Проверьте
$VIRTUAL_ENV:echo $VIRTUAL_ENVдолжен совпадать с путём к venv - Удалите конфликтующие aliases из shell rc:
unalias python pipили закомментируйте в .bashrc/.zshrc - Используйте 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` пакете).
Решение
- Установите C build toolchain: Linux —
apt install build-essential python3-dev; macOS —xcode-select --install; Windows —pip install --upgrade pip(обычно включает MSVC build tools) - Используйте binary-only wheel:
pip install --only-binary :all: pkg— pip не будет собирать из sdist - Для psycopg2 используйте
psycopg2-binary(precompiled wheel) вместоpsycopg2 - На macos с Apple Silicon:
arch -arm64 pip install pkg(для нативных wheels) илиarch -x86_64через Rosetta - Альтернатива — 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.
Решение
- Переименуйте файлы:
my_tests.py→test_my.py; функцииcheck_xxx()→test_xxx() - Запустите с verbose collection:
pytest --collect-only -q— покажет, какие файлы найдены - Проверьте rootdir:
pytest --rootdir=. -v— pytest должен находить ваш pyproject.toml/setup.cfg как rootdir - Настройте discovery через pyproject.toml:
[tool.pytest.ini_options]+testpaths = ["tests"]+python_files = ["test_*.py"] - Убедитесь, что 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, который пересоздаётся каждый тест.
Решение
- Перенесите общие fixtures в
tests/conftest.py— будут доступны во всех тестах автоматически - Не импортируйте fixtures явно — pytest сам их инжектит по имени параметра
- Согласуйте scopes: session > module > class > function. Fixture может зависеть только от равного или более широкого scope
- Проверьте структуру:
pytest --fixtures tests/— покажет все доступные fixtures для конкретной директории - Если нужна изоляция между тестами с 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+ соответственно.
Решение
- Сузьте Optional:
if x is not None: f(x)илиassert x is not None— mypy понимает narrowing - Используйте
cast(int, value)или# type: ignore[arg-type]для целевых suppressions с обоснованием - Установите stubs для third-party:
pip install types-requests types-pyyamlили используйтеmypy --install-types - Включите
--strict-optionalи--warn-return-anyпостепенно — поэтапная миграция через per-file[mypy-module.*] - Проверяйте локально перед 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.
Решение
- Установите уровни корректно:
logging.getLogger().setLevel(logging.DEBUG)(root) + handler сsetLevel(DEBUG) - Используйте
logging.basicConfig(level=DEBUG, force=True)(Python 3.8+) —force=Trueпересоздаёт handlers - Проверьте propagate:
logging.getLogger('app.module').propagate— должно быть True (default) - Дебаг иерархии:
logging.getLogger('app').handlersи.levelдля каждого уровня цепочки - Используйте
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 — антипаттерн.
Решение
- Замените list на set для membership:
s = set(lst); for x in items: if x in s: ...— O(1) per check - Для key→value lookup используйте dict:
d = {item.id: item for item in lst}— O(1) get - Профилируйте:
python -m cProfile -s cumulative script.py | head -30— раскроет O(n²) циклы - Используйте
collections.Counterдля подсчёта частот:Counter(items).most_common(10)— O(n) total - Микробенчмаркинг:
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+ — ошибка).
Решение
- В Jupyter:
await coro()напрямую (cell поддерживает top-level await) — не оборачивайтеasyncio.run() - Установите
nest-asyncio:pip install nest_asyncio && nest_asyncio.apply()— патчит asyncio для re-entrancy - Внутри запущенного loop используйте
asyncio.create_task(coro())— добавит task в текущий loop - В FastAPI / aiohttp: НЕ вызывайте
asyncio.run()— фреймворк сам управляет loop - Для верификации запущенности:
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).
Решение
- Для CPU-bound: используйте
multiprocessing.Poolилиconcurrent.futures.ProcessPoolExecutor— каждый процесс имеет свой GIL - Для I/O-bound: threading или asyncio — оба выпускают GIL во время I/O wait (asyncio предпочтительнее для high concurrency)
- Для numerical CPU-bound: NumPy / Polars / numba — операции выполняются в C без GIL (vectorized)
- Python 3.13t (free-threaded build, PEP 703) — экспериментальный no-GIL режим; production-ready ожидается 3.14+
- Профилируйте GIL contention:
pip install py-spy && py-spy record --gil --rate 100 -- python script.py— раскроет % времени GIL заблокирован