Python молча ломает ваш код: 10 ошибок новичков, которых не видно в трейсбеке
Самые неприятные баги в Python не падают с красным трейсбеком. pip уверяет, что пакет установлен, а import его не находит. Вчерашний скрипт вдруг не видит функцию, которая лежит на своём месте. Два списка склеиваются через zip(), и одна запись исчезает без единого предупреждения.
Во всех этих случаях Python сделал ровно то, что его попросили. Баг живёт в зазоре между написанным кодом и тем, что автор держал в голове. Ниже десять таких ошибок: семь взяты из свежего разбора KDnuggets, ещё три я добавил сам, потому что постоянно встречаю их на код-ревью. Для каждой есть причина и первое, что стоит проверить. Похожие разборы с кодом регулярно выходят в Telegram-канале @pythonl, так что если пишете на Python каждый день, загляните.
1. pip ставит пакет, а import его не видит
Классика: pip install requests отрабатывает без ошибок, вы запускаете скрипт и получаете ModuleNotFoundError: No module named 'requests'. Переустановка ничего не меняет. Причина скучная: на машине живут два разных Python. Каждое виртуальное окружение приносит свой интерпретатор и свою папку с пакетами, и pip из PATH легко может принадлежать одному Python, пока скрипт запускается другим.
Лечится дисциплиной. Создайте окружение, активируйте его и ставьте пакеты через тот интерпретатор, который реально запускает код:
Если проблема всё равно всплыла, выведите sys.executable прямо в падающем скрипте и сравните с тем, что показывает python -m pip --version. Разные пути означают, что переустановка никогда бы не помогла. Официальный гайд по pip и venv читается минут за десять и закрывает этот класс проблем навсегда.
2. Имя вашего файла перехватывает импорт
Назовите учебный файл json.py, напишите внутри import json, и Python начнёт сыпать ошибками про отсутствующие атрибуты или циклический импорт. Тот же фокус работает с random.py, csv.py и даже pandas.py.
При импорте Python проходит по пути поиска модулей, и папка вашего скрипта стоит в начале списка. Ваш json.py находится раньше стандартной библиотеки, и весь код, который ждал настоящий модуль, получает три строчки учебного файла. Переименуйте файл, а если проблема не ушла, удалите папку __pycache__ рядом с ним. Узнать, что именно загрузил Python, помогает одна строка: print(module.__file__).
3. Вера в то, что input() вернёт нужный тип
input() всегда возвращает строку. Пользователь ввёл 25, вы получили "25". Падение с TypeError здесь ещё мягкий вариант. Хуже сравнение "9" > "10": для строк оно абсолютно законно и тихо возвращает True. Приводите тип на входе и ловите единственную ошибку, которую может дать конвертация:
4. except Exception: pass стирает улики
Ловить исключения нормально. Беда в том, что этот код выбрасывает единственное доказательство сбоя. Допустим, process() упал на 4000-й записи: программа пожмёт плечами и поедет дальше, а через несколько дней в отчёте не хватит строк, и никто не поймёт почему.
Туториал по ошибкам и исключениям предлагает понятный путь: ловите конкретное исключение, которое умеете обработать, остальное пусть всплывает. Если широкий перехват всё-таки нужен на внешней границе программы, залогируйте исключение и пробросьте его дальше через raise. Когда обработка конкретных ошибок станет привычкой, повторяющийся код можно убрать в небольшие вспомогательные функции.
5. Изменение списка во время обхода
Удаляем неактивных пользователей:
После удаления элементы сдвигаются влево, а внутренний индекс цикла идёт дальше, поэтому элемент сразу за удалённым никто не проверяет. Словари в такой ситуации честнее и падают с RuntimeError. В официальном туториале есть два безопасных варианта: обходить копию или собрать новую коллекцию, что обычно чище.
6. Сохранение результата метода, который меняет объект на месте
list.sort() сортирует список на месте и возвращает None. Так задумано специально, чтобы метод нельзя было перепутать с операцией, создающей копию. В итоге присваивание выбрасывает отсортированный список, а ошибка всплывает позже и совсем в другом месте в виде 'NoneType' object is not iterable.
Выберите одну идиому: numbers.sort() отдельной строкой, если нужно изменить список, или numbers = sorted(numbers), если нужен новый. К append(), reverse() и прочим методам с побочным эффектом стоит относиться так же: если метод мутирует объект, по умолчанию ждите None.
7. zip() молча теряет данные
zip() останавливается на самой короткой последовательности. Ни исключения, ни предупреждения, запись просто отвалилась. Если бы это были метки и предсказания модели, такой баг легко уехал бы в прод. Когда одинаковая длина входит в контракт данных, пропишите это в коде: zip(names, scores, strict=True) выбросит ValueError при несовпадении длин. Флаг strict появился в Python 3.10, на старых версиях длины придётся сравнивать вручную.
8. Изменяемый аргумент по умолчанию
Эту ловушку KDnuggets разбирал в отдельной статье, но без неё список был бы неполным.
Значение по умолчанию вычисляется один раз при определении функции, поэтому все вызовы делят один и тот же список. Используйте None как маркер и создавайте объект внутри:
9. Сравнение через is вместо ==
Оператор is проверяет, что перед вами один и тот же объект в памяти, == сравнивает значения. CPython кэширует маленькие числа и часть строк, поэтому x is 256 может вернуть True, а на 257 результат уже зависит от того, как выполняется код. Такая ошибка годами живёт незаметно и стреляет на реальных данных. Правило простое: is оставляем для None, True, False и явной проверки идентичности, для всего остального используем ==. Свежие версии Python даже выдают SyntaxWarning на сравнение с литералом через is.
10. Копия списка, которая копией не является
Умножение списка копирует только ссылки, поэтому все три строки указывают на один и тот же внутренний список. То же самое происходит при b = a: новое имя, старый объект. Для вложенных структур используйте генератор списка [[0] * 3 for _ in range(3)], а для полной копии со всей вложенностью copy.deepcopy().
Как отлаживать такие баги
Ни одна из этих ошибок не возникла из-за странного поведения Python. Все они растут из допущений, о которых программа ничего не знала. Поэтому когда код ведёт себя нелогично, не торопитесь его переписывать. Сначала проверьте, какой интерпретатор реально запущен (sys.executable), откуда пришёл импорт (module.__file__), какой тип и значение у переменной на самом деле. Не глушите неожиданные исключения, пока осознанно не решите, как должно выглядеть восстановление. Такой короткий допрос закрывает большую часть отладки за несколько минут.
Больше разборов, полезных сниппетов и новостей экосистемы в @pythonl, лучшем Telegram-канале про Python: подписывайтесь, чтобы не наступать на такие грабли в своём коде.