Python молча ломает ваш код: 10 ошибок новичков, которых не видно в трейсбеке

Самые неприятные баги в Python не падают с красным трейсбеком. pip уверяет, что пакет установлен, а import его не находит. Вчерашний скрипт вдруг не видит функцию, которая лежит на своём месте. Два списка склеиваются через zip(), и одна запись исчезает без единого предупреждения.

Python молча ломает ваш код: 10 ошибок новичков, которых не видно в трейсбеке

Во всех этих случаях Python сделал ровно то, что его попросили. Баг живёт в зазоре между написанным кодом и тем, что автор держал в голове. Ниже десять таких ошибок: семь взяты из свежего разбора KDnuggets, ещё три я добавил сам, потому что постоянно встречаю их на код-ревью. Для каждой есть причина и первое, что стоит проверить. Похожие разборы с кодом регулярно выходят в Telegram-канале @pythonl, так что если пишете на Python каждый день, загляните.

1. pip ставит пакет, а import его не видит

Классика: pip install requests отрабатывает без ошибок, вы запускаете скрипт и получаете ModuleNotFoundError: No module named 'requests'. Переустановка ничего не меняет. Причина скучная: на машине живут два разных Python. Каждое виртуальное окружение приносит свой интерпретатор и свою папку с пакетами, и pip из PATH легко может принадлежать одному Python, пока скрипт запускается другим.

Лечится дисциплиной. Создайте окружение, активируйте его и ставьте пакеты через тот интерпретатор, который реально запускает код:

python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate python -m pip install requests

Если проблема всё равно всплыла, выведите 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() вернёт нужный тип

age = input("Сколько вам лет? ") next_year = age + 1 # TypeError: can only concatenate str (not "int") to str

input() всегда возвращает строку. Пользователь ввёл 25, вы получили "25". Падение с TypeError здесь ещё мягкий вариант. Хуже сравнение "9" > "10": для строк оно абсолютно законно и тихо возвращает True. Приводите тип на входе и ловите единственную ошибку, которую может дать конвертация:

try: age = int(input("Сколько вам лет? ")) except ValueError: print("Введите целое число.")

4. except Exception: pass стирает улики

try: process(records) except Exception: pass

Ловить исключения нормально. Беда в том, что этот код выбрасывает единственное доказательство сбоя. Допустим, process() упал на 4000-й записи: программа пожмёт плечами и поедет дальше, а через несколько дней в отчёте не хватит строк, и никто не поймёт почему.

Туториал по ошибкам и исключениям предлагает понятный путь: ловите конкретное исключение, которое умеете обработать, остальное пусть всплывает. Если широкий перехват всё-таки нужен на внешней границе программы, залогируйте исключение и пробросьте его дальше через raise. Когда обработка конкретных ошибок станет привычкой, повторяющийся код можно убрать в небольшие вспомогательные функции.

5. Изменение списка во время обхода

Удаляем неактивных пользователей:

for user in users: if not user.active: users.remove(user) # пропускает соседа каждого удалённого элемента

После удаления элементы сдвигаются влево, а внутренний индекс цикла идёт дальше, поэтому элемент сразу за удалённым никто не проверяет. Словари в такой ситуации честнее и падают с RuntimeError. В официальном туториале есть два безопасных варианта: обходить копию или собрать новую коллекцию, что обычно чище.

for user in users.copy(): if not user.active: users.remove(user) active_users = [u for u in users if u.active] # чаще всего лучше

6. Сохранение результата метода, который меняет объект на месте

numbers = numbers.sort() # теперь numbers равно None

list.sort() сортирует список на месте и возвращает None. Так задумано специально, чтобы метод нельзя было перепутать с операцией, создающей копию. В итоге присваивание выбрасывает отсортированный список, а ошибка всплывает позже и совсем в другом месте в виде 'NoneType' object is not iterable.

Выберите одну идиому: numbers.sort() отдельной строкой, если нужно изменить список, или numbers = sorted(numbers), если нужен новый. К append(), reverse() и прочим методам с побочным эффектом стоит относиться так же: если метод мутирует объект, по умолчанию ждите None.

7. zip() молча теряет данные

names = ["Amara", "Ben", "Chen", "Dana"] scores = [91, 84, 77] print(list(zip(names, scores))) # [('Amara', 91), ('Ben', 84), ('Chen', 77)] Dana пропала

zip() останавливается на самой короткой последовательности. Ни исключения, ни предупреждения, запись просто отвалилась. Если бы это были метки и предсказания модели, такой баг легко уехал бы в прод. Когда одинаковая длина входит в контракт данных, пропишите это в коде: zip(names, scores, strict=True) выбросит ValueError при несовпадении длин. Флаг strict появился в Python 3.10, на старых версиях длины придётся сравнивать вручную.

8. Изменяемый аргумент по умолчанию

Эту ловушку KDnuggets разбирал в отдельной статье, но без неё список был бы неполным.

def add_tag(tag, tags=[]): tags.append(tag) return tags add_tag("python") # ['python'] add_tag("ml") # ['python', 'ml']

Значение по умолчанию вычисляется один раз при определении функции, поэтому все вызовы делят один и тот же список. Используйте None как маркер и создавайте объект внутри:

def add_tag(tag, tags=None): if tags is None: tags = [] tags.append(tag) return tags

9. Сравнение через is вместо ==

Оператор is проверяет, что перед вами один и тот же объект в памяти, == сравнивает значения. CPython кэширует маленькие числа и часть строк, поэтому x is 256 может вернуть True, а на 257 результат уже зависит от того, как выполняется код. Такая ошибка годами живёт незаметно и стреляет на реальных данных. Правило простое: is оставляем для None, True, False и явной проверки идентичности, для всего остального используем ==. Свежие версии Python даже выдают SyntaxWarning на сравнение с литералом через is.

10. Копия списка, которая копией не является

grid = [[0] * 3] * 3 grid[0][0] = 1 print(grid) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

Умножение списка копирует только ссылки, поэтому все три строки указывают на один и тот же внутренний список. То же самое происходит при b = a: новое имя, старый объект. Для вложенных структур используйте генератор списка [[0] * 3 for _ in range(3)], а для полной копии со всей вложенностью copy.deepcopy().

Как отлаживать такие баги

Ни одна из этих ошибок не возникла из-за странного поведения Python. Все они растут из допущений, о которых программа ничего не знала. Поэтому когда код ведёт себя нелогично, не торопитесь его переписывать. Сначала проверьте, какой интерпретатор реально запущен (sys.executable), откуда пришёл импорт (module.__file__), какой тип и значение у переменной на самом деле. Не глушите неожиданные исключения, пока осознанно не решите, как должно выглядеть восстановление. Такой короткий допрос закрывает большую часть отладки за несколько минут.

Больше разборов, полезных сниппетов и новостей экосистемы в @pythonl, лучшем Telegram-канале про Python: подписывайтесь, чтобы не наступать на такие грабли в своём коде.

11