Частые ошибки Python от синтаксиса до production-tooling — симптомы, причины и пошаговые решения для Data Engineer.
Python 3 запрещает смешивать tab и space внутри одного блока (PEP 8 + tokenizer error TabError). При copy-paste из веб-страниц часто заносится non-breaking space (U+00A0) вместо ASCII space (0x20), либо локальные настройки editor'а отличаются от tabstop проекта.
"editor.renderWhitespace": "all", PyCharm — View → Active Editor → Show Whitespacespython -X dev script.py — расширенные диагностики покажут точный токен и позицию.editorconfig в корне проекта: indent_style = space + indent_size = 4 (PEP 8 standard)ruff check --select E1,E2,W1 — раскроет mixed indent + non-ASCII whitespacePEP 8 рекомендует исключительно пробелы (4 на уровень). Python 3 строже Python 2 и эмитит TabError при любой смеси tab+space внутри одного логического блока. Часто причина — IDE с автоматическим преобразованием (VS Code `editor.detectIndentation: true`) или унаследованный код, частично сконвертированный.
python -tt script.py (Python 3 включает строгий режим по умолчанию; флаг сохранён для совместимости)expand -t 4 file.py > file.py.tmp && mv file.py.tmp file.py (UNIX)Ctrl+Shift+P → Convert Indentation to Spaces; в PyCharm: Edit → Convert Indents → To Spacesruff format или black — оба нормализуют отступы автоматически.editorconfig: indent_style = space, indent_size = 4, end_of_line = lfИмя X не существует в текущей области видимости. Python разрешает имена по правилу LEGB (Local → Enclosing → Global → Built-in) и кидает NameError, если X не найден ни на одном уровне. Частые причины: typo, забытый import, переменная определена только в одной ветке if, использование переменной до её присваивания внутри функции (UnboundLocalError — подкласс NameError в Python 3.11+).
ruff check --select F821 (undefined name) или pyflakes file.pypython -c "from module import X; print(X)" — либо имя не существует, либо отсутствует в __all__global X или nonlocal X (для замыканий)--strict или ruff F rules — они ловят undefined-name на этапе static-analysisОбъект, к которому применяется `[...]`, равен 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] для безопасного доступа с fallbackif result is None: raise ValueError('expected non-None'); return result[...]--strict + Optional[Dict] annotations — type checker поймает None-flow на этапе анализаif (val := func()) is not None: process(val[0])Модуль X не установлен в текущем virtual environment, либо установлен в другую Python-версию (например, system python3 vs venv python3.12). PYTHONPATH не включает директорию с модулем. Возможен конфликт с одноимённым local file (X.py в cwd shadowing installed package — Python ищет cwd первым).
source .venv/bin/activate (Linux/macOS) или .venv\Scripts\activate (Windows)pip install X или быстрее через uv pip install Xpython -c "import sys; print('\n'.join(sys.path))" — модуль должен быть в одной из директорийX.py или директории X/, перекрывающих установленный пакетpip install -e . из корня проекта с pyproject.tomlИмя 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.toml dependenciespython -c "import X; print([n for n in dir(X) if 'Y' in n])" — может быть переименованtry: from X import Y\nexcept ImportError: from X.submodule import YМодули A и B импортируют друг друга на top-level. Когда A импортирует B, B пытается импортировать A — но A ещё не закончил инициализацию (его module dict частично пуст). Python видит `partially initialized module` и кидает ImportError. Архитектурный smell — обычно нарушение Single Responsibility или забытое выделение общего слоя.
if TYPE_CHECKING: блок (PEP 484) для type-only импортов — не выполняется в runtimepydeps src/ --max-bacon 2 --pylib-all визуализирует cycles в проектеPython имеет жёсткий лимит глубины стека вызовов (`sys.getrecursionlimit()` — обычно 1000). Лимит защищает от stack overflow процесса. Python (CPython) НЕ оптимизирует tail calls, поэтому глубокая рекурсия не превращается в цикл автоматически. При обходе deep tree / linked list / parsing рекурсия достигает лимита.
collections.deque) вместо вызова себяsys.setrecursionlimit(10000) — но риск real stack overflow процесса (segfault)threading.Thread(target=fn).start() после threading.stack_size(64*1024*1024)yield from делегирует без углубления стека@functools.lru_cache) — уменьшает число рекурсивных вызовов экспоненциальноУтечка памяти — объекты не освобождаются 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(x*2 for x in data) вместо [x*2 for x in data] для streamingfor line in open(path): ... не загружает весь файл в RAMcache.clear() или используйте weakref.WeakValueDictionaryimport gc; gc.collect() — освобождает reference cyclesКлюч отсутствует в 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) lookupcollections.defaultdict(list) для аккумуляции: создаёт default при первом обращенииdataclasses или pydantic.BaseModel для structured data — валидация schema на этапе созданияassert 'user_id' in response, f'Unexpected schema: {list(response.keys())}'Индекс 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 programmingtry/except IndexError для graceful fallback на default-значениеlst[i:i+1] возвращает [] если i вне границfor item in lst или enumerate(lst) — Python сам управляет индексомlst[:] = [x for x in lst if cond]Default arguments вычисляются ОДИН раз на этапе определения функции (когда `def` выполняется), а не при каждом вызове. Для immutable types (int, str, tuple) это безопасно. Для mutable (list, dict, set) — один и тот же объект разделяется между всеми вызовами. CPython хранит defaults в `func.__defaults__` tuple — модификация мутабельного default видна всем последующим вызовам.
None и создавайте mutable внутри: def f(x=None): x = [] if x is None else xfield(default_factory=list) вместо field(default=[])B006 (mutable-argument-default) или pylint W0102 — поймают на этапе linttuple вместо list, frozenset вместо set когда возможноf.__defaults__ при отладке: содержит фактические default-объекты функцииБайты в файле не валидны как UTF-8 (либо файл в другой кодировке — cp1251/latin1/koi8-r, либо файл бинарный). Python 3 разделяет str (Unicode codepoints, PEP 393 flexible representation) и bytes. При `open(path)` без encoding Python использует `locale.getpreferredencoding()` — может отличаться от фактической кодировки файла. UnicodeDecodeError возникает на первом не-валидном байте.
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+FFFDf.read().decode('utf-8', errors='strict')PYTHONUTF8=1 для consistent UTF-8 mode (PEP 540) на всех OSPython 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 в разных порядкахself.helper = HelperClass()super().__init__() chain: каждый класс должен вызывать super, даже MixinsPEP 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 не завершена.
requires = ["setuptools>=61", "wheel"] + build-backend = "setuptools.build_meta"requires = ["hatchling"] + build-backend = "hatchling.build"requires = ["poetry-core>=1.0.0"] + build-backend = "poetry.core.masonry.api"pip install build && python -m build создаст sdist+wheel в dist/pip install -e . работает локально, потом — CI с pip install build && python -m build && pip install dist/*.whlPip 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'ами на зависимости.
pip install uv && uv pip install pkg — Rust-based proper resolver, в 10-100× быстрее + полный backtrackingpython -m venv .venv-clean && source .venv-clean/bin/activate && pip install pkgpip install pip-tools && pip-compile requirements.in -o requirements.txtpyproject.toml + uv lock — создаёт reproducible uv.lock со всеми transitive версиямиpip install --use-deprecated=legacy-resolver pkg (быстрый, но небезопасный — оставит broken deps)Активация 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`.
source .venv/bin/activate, fish — source .venv/bin/activate.fish, Windows PowerShell — .venv\Scripts\Activate.ps1hash -r (bash/zsh) — сбросит cache путей; which python должен показать .venv/bin/python$VIRTUAL_ENV: echo $VIRTUAL_ENV должен совпадать с путём к venvunalias python pip или закомментируйте в .bashrc/.zshrcbrew install direnv) — автоматически активирует venv при входе в директорию через .envrcПакет содержит 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` пакете).
apt install build-essential python3-dev; macOS — xcode-select --install; Windows — pip install --upgrade pip (обычно включает MSVC build tools)pip install --only-binary :all: pkg — pip не будет собирать из sdistpsycopg2-binary (precompiled wheel) вместо psycopg2arch -arm64 pip install pkg (для нативных wheels) или arch -x86_64 через Rosettadocker run python:3.12-slim pip install pkg (Linux build environment гарантирован)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()pytest --collect-only -q — покажет, какие файлы найденыpytest --rootdir=. -v — pytest должен находить ваш pyproject.toml/setup.cfg как rootdir[tool.pytest.ini_options] + testpaths = ["tests"] + python_files = ["test_*.py"]__init__.py в test-директориях контролирует import modePytest fixtures видны только в области их определения: fixture в `test_a.py` НЕ доступен в `test_b.py`. Для шаринга используется `conftest.py` — он автоматически подхватывается во всех тестах в этой и подпапках. ScopeMismatch — fixture с более широким scope (session) пытается использовать fixture с более узким (function): session-fixture создаётся раз на весь run, но запрашивает function-fixture, который пересоздаётся каждый тест.
tests/conftest.py — будут доступны во всех тестах автоматическиpytest --fixtures tests/ — покажет все доступные fixtures для конкретной директории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+ соответственно.
if x is not None: f(x) или assert x is not None — mypy понимает narrowingcast(int, value) или # type: ignore[arg-type] для целевых suppressions с обоснованиемpip install types-requests types-pyyaml или используйте mypy --install-types--strict-optional и --warn-return-any постепенно — поэтапная миграция через per-file [mypy-module.*]mypy --strict src/ или интегрируйте в pre-commit hookPython 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 пересоздаёт handlerslogging.getLogger('app.module').propagate — должно быть True (default)logging.getLogger('app').handlers и .level для каждого уровня цепочкиdictConfig для централизации: logging.config.dictConfig(config_dict) гарантирует replicationlist реализован как 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 — антипаттерн.
s = set(lst); for x in items: if x in s: ... — O(1) per checkd = {item.id: item for item in lst} — O(1) getpython -m cProfile -s cumulative script.py | head -30 — раскроет O(n²) циклыcollections.Counter для подсчёта частот: Counter(items).most_common(10) — O(n) totalpython -m timeit -s 'lst=list(range(10000))' '5000 in lst' vs то же для setasyncio модель: только один 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+ — ошибка).
await coro() напрямую (cell поддерживает top-level await) — не оборачивайте asyncio.run()nest-asyncio: pip install nest_asyncio && nest_asyncio.apply() — патчит asyncio для re-entrancyasyncio.create_task(coro()) — добавит task в текущий loopasyncio.run() — фреймворк сам управляет loopasyncio.get_running_loop() (3.7+) кидает RuntimeError если loop неактивен — отличает от get_event_loop()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).
multiprocessing.Pool или concurrent.futures.ProcessPoolExecutor — каждый процесс имеет свой GILpip install py-spy && py-spy record --gil --rate 100 -- python script.py — раскроет % времени GIL заблокирован