Перейти к содержимому
Ступень 00.2 Python с нуля

Ошибки и отладка

Как узнавать частые ошибки Python по сообщению, читать traceback из нескольких блоков, ловить молчаливые логические баги через print, assert и отладчик VS Code и задавать вопросы так, чтобы на них отвечали.

УРОКstage-0/0-2-python/05-errors-debugging.mdx1 ч 50 минУрок 6 из 7
После урока вы сможете
  • Узнавать по сообщению десяток частых ошибок Python и находить по traceback из нескольких блоков строку, которую нужно исправить
  • Искать логические ошибки: print-отладка с f"{x=}", проверки assert, деление кода пополам
  • Ставить точки останова и выполнять код по шагам в отладчике VS Code и в pdb через breakpoint()
  • Сокращать баг до минимального примера, грамотно спрашивать ИИ-помощника и превращать исправленный баг в тест и запись в тетради ошибок

В уроке 0.0.3 вы завели привычку: последняя строка → номер строки → сама строка → гипотеза. Тогда программы были в три строки. Теперь у вас есть функции, файлы и классы, и ошибки стали глубже: сообщение может занимать 30 строк, а причина — прятаться совсем не там, где упало.

А самые опасные ошибки в ML вообще ничего не сообщают. Андрей Карпатый в эссе «A Recipe for Training Neural Networks» формулирует это так: обучение нейросети ломается молча. Перепутали метки, забыли перемешать данные, посчитали loss не по тем токенам — никакого исключения не будет. Модель просто обучится немного хуже, и вы потратите дни, прежде чем заметите. Поэтому отладкаотладкаEN debuggingПоиск и исправление ошибок в программе. Саму ошибку в программе и русские, и китайские разработчики обычно называют просто «баг» (bug). — не досадная помеха, а основная работа инженера: найти, почему программа делает не то, что вы задумали.

В этом уроке — каталог частых ошибок, чтение длинных traceback, три способа ловить молчаливые баги (print, assert, деление пополам), отладчик VS Code и pdb, метод резиновой уточки, минимальный пример для вопроса и запись бага в тетрадь ошибок. В конце — охота на баги в шести коротких программах.

Напомним классификацию из урока 0.0.3 — теперь с точным моментом, когда каждая ошибка проявляется.

ВидКогда обнаруживаетсяЧто видноПример
Синтаксическаясинтаксическая ошибкаEN syntax errorPython не понял запись (скобка, кавычка, двоеточие, отступ). Программа не выполняет ни одной строки.до запуска: Python читает весь файл и не понимает записьSyntaxError без слова Traceback; не выполняется ни одна строказабыта скобка
Ошибка выполненияошибка выполненияEN runtime errorЗапись верна, но действие невыполнимо (деление на ноль, нет ключа). Программа падает на этой строке с traceback.когда выполнение дошло до плохой строкиTraceback ... и тип ошибки; строки выше успели выполнитьсяделение на ноль
Логическаялогическая ошибкаEN logic errorПрограмма работает без сообщений об ошибках, но результат неверный. Ловится только проверками и тестами.никогда — саманичего: программа отработала, ответ неверный= вместо +=

Проверьте первую строку таблицы сами. В файле из трёх строк print("старт"), x = 1, print("конец" слово «старт» не напечатается: незакрытая скобка в третьей строке не даёт запуститься всему файлу.

Сообщения ниже — из Python 3.11. В новых версиях формулировки бывают чуть подробнее, но тип ошибки тот же.

ОшибкаКодСообщениеЧто проверить
SyntaxErrorprint("Привет"'(' was never closedскобки, кавычки, двоеточия; если строка в порядке — строку выше
IndentationErrorif True: и под ним print(1) без отступаexpected an indented block after 'if' statement on line 1после строки с : нужен отступ в 4 пробела
NameErrorprint(y)name 'y' is not definedопечатка (читайте подсказку Did you mean: ...?); переменная создаётся ниже; в Jupyter не выполнена ячейка
TypeError"шаг " + 5can only concatenate str (not "int") to strтипы: строка вместо числа, None вместо списка
ValueErrorint("3.5")invalid literal for int() with base 10: '3.5'тип верный, значение не подходит
IndexError[1, 2, 3][3]list index out of rangeиндексы с нуля; последний — len - 1 или -1
KeyError{"кот": 1}["пёс"]'пёс'ключа нет: .get(ключ, по_умолчанию) или проверка in
ZeroDivisionErrorsum([]) / len([])division by zeroпустой список, нулевой знаменатель
AttributeErrorx = [3, 1].sort(), потом x.append(4)'NoneType' object has no attribute 'append'откуда в переменной None
ModuleNotFoundErrorimport numpiNo module named 'numpi'опечатка; не активировано окружение (урок 0.1.2)

Ещё две вы встречали раньше: FileNotFoundError — нет файла по такому пути (урок 0.2.4) и RecursionError — рекурсия без базы (урок 0.2.2). А если программа не падает, но и не заканчивается, скорее всего, это бесконечный циклбесконечный циклEN infinite loopЦикл, условие выхода из которого никогда не выполняется. Остановить программу — Ctrl+C.. Нажмите Ctrl+C (в Jupyter — кнопку ■ «Interrupt»): Python остановится с KeyboardInterrupt, и traceback покажет, на какой строке он крутился.

Главное в сообщении — текст после двоеточия, а не только тип. TypeError бывает очень разным:

# Запускайте каждый пример отдельно: первая же ошибка останавливает программу
"шаг " + 5 # TypeError: can only concatenate str (not "int") to str
len(5) # TypeError: object of type 'int' has no len()
x = None
x[0] # TypeError: 'NoneType' object is not subscriptable
def f(a, b):
return a + b
f(1) # TypeError: f() missing 1 required positional argument: 'b'

Когда функции вызывают друг друга, traceback показывает всю цепочку. Вот маленький «токенизатор»:

def load_vocab():
return {"<unk>": 0, "кот": 1, "сел": 2, "на": 3}
def encode(words, vocab):
ids = []
for w in words:
ids.append(vocab[w])
return ids
def main():
vocab = load_vocab()
ids = encode("кот сел на мат".split(), vocab)
print(ids)
main()
Traceback (most recent call last):
File "/home/user/tok.py", line 18, in <module>
main()
File "/home/user/tok.py", line 14, in main
ids = encode("кот сел на мат".split(), vocab)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/user/tok.py", line 8, in encode
ids.append(vocab[w])
~~~~~^^^
KeyError: 'мат'

Заголовок most recent call last значит «последний вызов — внизу». Каждый блок — одна функция, которая на момент ошибки ещё не закончила работу. Вместе они образуют стек вызововстек вызововEN call stackЦепочка функций, которые вызвали друг друга и ещё не завершились. Traceback — это снимок стека в момент ошибки.:

flowchart TB
A["основной код (module), строка 18: main()"] --> B["main, строка 14: encode(...)"]
B --> C["encode, строка 8: vocab[w]<br/>💥 KeyError: 'мат'"]
Traceback сверху вниз — путь от запуска программы до места падения

Читаем снизу: KeyError: 'мат' — в словаре нет слова «мат». Упало в encode на строке 8. Но где исправлять — отдельный вопрос, и ответ зависит от того, что считать правильным поведением. Либо словарь неполный (чинить load_vocab), либо неизвестные слова должны превращаться в <unk>, как в практике урока 0.2.1: тогда в encode нужно vocab.get(w, vocab["<unk>"]). Traceback показывает место, где проблема проявилась, а решение принимаете вы.

Самая частая ошибка в PyTorch выглядит так (пути сокращены, номера строк зависят от версии):

import torch
import torch.nn as nn
layer = nn.Linear(4, 5) # слой ждёт 4 признака на входе
x = torch.randn(2, 3) # а мы подаём батч из 2 примеров по 3 признака
y = layer(x)
Traceback (most recent call last):
File "/home/user/model.py", line 6, in <module>
y = layer(x)
^^^^^^^^
File ".../site-packages/torch/nn/modules/module.py", line 1783, in _wrapped_call_impl
return self._call_impl(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../site-packages/torch/nn/modules/module.py", line 1794, in _call_impl
return forward_call(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../site-packages/torch/nn/modules/linear.py", line 134, in forward
return F.linear(input, self.weight, self.bias)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: mat1 and mat2 shapes cannot be multiplied (2x3 and 4x5)

Три нижних блока — код PyTorch, и ошибки там нет: библиотека честно сообщает, что матрицы 2×3 и 4×5 перемножить нельзя (почему — в уроке о матрицах 1.1). Правило: найдите самый нижний блок со своим файлом — здесь model.py, строка 6. Исправлять почти всегда там. В реальном обучении таких библиотечных блоков бывает 20–40, и это правило экономит больше всего времени.

Иногда traceback состоит из двух частей, разделённых фразой The above exception was the direct cause of the following exception (так выглядит raise ... from e из урока 0.2.4) или During handling of the above exception, another exception occurred (ошибка случилась внутри except). Читайте обе части: настоящая причина обычно в первой.

Функция должна посчитать, сколько всего слов в корпусе:

def count_tokens(texts):
total = 0
for text in texts:
words = text.split()
total = len(words)
return total
corpus = ["я люблю python", "python любит меня", "и я"]
print(count_tokens(corpus)) # → 2, а в корпусе 3 + 3 + 2 = 8 слов

Ни одной ошибки, и ответ неверный. План действий:

  1. Воспроизвести на маленьком входе, где правильный ответ известен заранее. Здесь — 8.

  2. Сформулировать гипотезу: «total не накапливается» или «split режет не так».

  3. Проверить гипотезу одним из способов ниже — print, assert, отладчик.

  4. Исправить одну вещь и запустить снова. Две правки сразу — и вы не узнаете, какая помогла.

Самый простой и самый популярный способ — напечатать промежуточные значения. Удобнее всего f-строка со знаком = из урока 0.2.1: она печатает и имя, и значение.

for text in texts:
words = text.split()
total = len(words)
print(f"{text=} {len(words)=} {total=}") # временная строка для отладки
text='я люблю python' len(words)=3 total=3
text='python любит меня' len(words)=3 total=3
text='и я' len(words)=2 total=2

Видно сразу: total не растёт, а перезаписывается длиной текущего текста. Исправление — total += len(words).

Три приёма, которые делают print-отладку сильнее:

  • repr показывает невидимое. print(w) для "кот " и "кот" выглядит одинаково, а print(repr(w)) или f"{w!r}" покажет 'кот ' с пробелом. Так же видны переводы строк: '42\n'. Сравнение "кот " == "кот" даёт False, и без repr эту ошибку можно искать час.
  • type(x) — когда подозреваете, что вместо числа пришла строка.
  • Метка. Начинайте отладочные строки с DEBUG, чтобы потом найти и удалить их все.

Программа длинная, данные проходят через пять шагов: чтение → очистка → токенизация → подсчёт → вывод. Где-то по дороге они портятся. Не нужно проверять каждую строку: поставьте проверку посередине, после токенизации. Данные там правильные — баг дальше; неправильные — раньше. Каждая проверка отбрасывает половину кода: 1000 строк → 500 → 250 → 125 → … → 1. Поэтому в программе из 1000 строк место бага находится примерно за 10 проверок (ведь 210=10242^{10} = 1024, урок 0.0.2). В математике такое число делений пополам записывают как log⁡21000≈10\log_2 1000 \approx 10 — логарифмы ждут вас в уроке 1.0.2.

Тот же принцип работает во времени. «Вчера работало, сегодня нет» — Git умеет искать сломавший коммит делением пополам: команда git bisect среди 100 коммитов найдёт виновника за 7 шагов (Git — урок 0.1.1, пошаговый git bisect run на чужом репозитории — урок 11.1.2). И та же идея — двоичный поискдвоичный поискEN binary searchПоиск делением диапазона пополам. Среди 100 вариантов находит ответ не больше чем за 7 шагов, потому что 2^7 = 128 ≥ 100. — станет стратегией игры «угадай число» в следующем уроке.

assertутверждение (assert)EN assertionassert условие, "сообщение" — проверка, которая падает с AssertionError, если условие ложно. Отключается флагом python -O. вы видели в тестах (урок 0.1.3) и в датаклассе (урок 0.2.3). Это проверка «здесь обязано быть верно»: если условие ложно, программа сразу падает с AssertionError и вашим сообщением.

probs = [0.2, 0.5, 0.2]
assert abs(sum(probs) - 1) < 1e-6, f"сумма вероятностей {sum(probs):.3f}, а должна быть 1"
AssertionError: сумма вероятностей 0.900, а должна быть 1

Зачем специально падать? Затем, что ошибка у источника в сто раз дешевле ошибки, найденной через три часа обучения. В ML-коде assert ставят на «инварианты» — то, что должно быть верно всегда: число входов совпадает с числом меток, вероятности суммируются в 1, в loss нет nan, у тензора нужная формаформа массиваEN shapeКортеж размеров по осям, например (batch, seq_len, d_model).. Китайский учебник Ляо Сюэфэна прямо советует: где вы печатаете значение, чтобы на него посмотреть, можно поставить assert, и он будет проверять за вас каждый запуск.

Две ловушки:

  • Скобки. assert (x > 0, "x должен быть положительным") — это проверка кортежа, а непустой кортеж всегда истинен. Python 3.11 предупредит: SyntaxWarning: assertion is always true, perhaps remove parentheses? — но программа продолжит работать, и проверка не сработает никогда. Пишите без скобок.
  • python -O выключает все assert. Поэтому проверять данные от пользователя или из файла нужно через raise ValueError(...) (урок 0.2.4), а assert оставить для проверок вашей собственной логики.

print нужно дописывать в код и перезапускать программу. ОтладчикотладчикEN debuggerПрограмма, которая останавливает ваш код в нужном месте и показывает значения всех переменных. В Python — pdb, в VS Code — встроенный. позволяет поставить программу на паузу в любом месте, увидеть все переменные сразу и выполнять код по одной строке — ничего не меняя в файле.

  1. Откройте файл с count_tokens и щёлкните слева от номера строки total = len(words). Появится красная точка — точка остановаточка остановаEN breakpointСтрока, перед выполнением которой отладчик ставит программу на паузу. В коде — функция breakpoint(). (то же делает клавиша F9).

  2. Нажмите F5 (Run → Start Debugging). В первый раз VS Code спросит отладчик — выберите Python Debugger, затем Python File (текущий файл). Расширение Python Debugger ставится вместе с расширением Python из урока 0.1.2.

  3. Программа остановится перед выполнением отмеченной строки, строка подсветится. Слева, в панели Variables, — все переменные: text, words, total.

  4. Нажмите F10: строка выполнится, и вы увидите, как изменился total. F5 — продолжить до следующей остановки, то есть до следующего прохода цикла.

  5. В панели Watch можно добавить любое выражение, например len(words), а в Debug Console — вычислить что угодно прямо на паузе: total + len(words). Панель Call Stack — это живой traceback: те же блоки, только программа не упала, а ждёт вас.

КлавишаДействиеКогда нужно
F9поставить или снять точку остановаотметить подозрительную строку
F5запустить отладку / продолжить до следующей точкипробежать до интересного места
F10шаг через (Step Over): выполнить строку целикомидти по своей функции строка за строкой
F11шаг внутрь (Step Into): зайти в вызываемую функциюбаг, похоже, внутри вызова
Shift+F11шаг наружу (Step Out): доработать функцию до концазашли не туда
Shift+F5остановить отладкувсё понятно

Условная точка останова — главный приём для данных. Цикл идёт по 100 000 примеров, а ломается один. Щёлкните правой кнопкой по красной точке → Edit Breakpoint → Expression и впишите условие, например text == "и я" или len(words) == 0. Отладчик остановится только на нужном примере. Там же есть Log Message: точка, которая не останавливает программу, а печатает сообщение вроде total = {total} — print без правки кода.

Если VS Code под рукой нет (например, вы работаете на сервере по SSH), поставьте в код вызов breakpoint(). Программа остановится на этом месте и откроет встроенный отладчик pdb с приглашением (Pdb). Файл dbg.py:

def count_tokens(texts):
total = 0
for text in texts:
words = text.split()
breakpoint() # здесь программа встанет на паузу
total = len(words)
return total
corpus = ["я люблю python", "python любит меня", "и я"]
print(count_tokens(corpus))

Запускаем python dbg.py и вводим после (Pdb) команды: p text, total, n, p total, c и так далее. Python 3.11 отвечает так:

> /home/user/dbg.py(6)count_tokens()
-> total = len(words)
(Pdb) p text, total
('я люблю python', 0)
(Pdb) n
> /home/user/dbg.py(3)count_tokens()
-> for text in texts:
(Pdb) p total
3
(Pdb) c
> /home/user/dbg.py(6)count_tokens()
-> total = len(words)
(Pdb) p text, total
('python любит меня', 3)
(Pdb) q

На втором проходе total равен 3, и следующая строка запишет в него снова 3, а не 6 — баг пойман. В Python 3.13 и новее pdb останавливается прямо на строке breakpoint(), а не на следующей, поэтому там понадобится на одно n больше.

КомандаЧто делает
p выражениенапечатать значение (print)
nвыполнить текущую строку (next)
sзайти внутрь вызываемой функции (step)
cпродолжить до следующего breakpoint() (continue)
lпоказать код вокруг текущей строки (list)
wпоказать стек вызовов (where)
qвыйти; Python напечатает BdbQuit — это нормально

В Jupyter есть приём ещё проще: ячейка упала — выполните в следующей ячейке %debug, и pdb откроется прямо в месте падения, со всеми переменными. Не забудьте убрать breakpoint() из кода, когда закончите. В браузерных заданиях сайта отладчика нет, там работает print.

Метод резиновой уточкиметод резиновой уточкиEN rubber duck debuggingОбъяснить код вслух строка за строкой (хоть игрушечной уточке) — и заметить, где ваше объяснение расходится с кодом. Термин из книги «Программист-прагматик». пришёл из книги «Программист-прагматик» (The Pragmatic Programmer): программист носил с собой резиновую уточку и, застряв, объяснял ей код строка за строкой. Звучит как шутка, но работает. Пока код в голове, вы видите то, что хотели написать. Объясняя вслух, вы вынуждены проговорить каждое допущение: «тут total — сумма слов всех текстов… ой, нет, тут я присваиваю, а не прибавляю».

ИИ-помощник — уточка, которая умеет отвечать. Чтобы он не превратился в решебник (урок 0.0.3), попросите его играть именно уточку:

Я объясню свой код построчно. Не исправляй его и не пиши решение.
Задавай вопросы там, где моё объяснение не совпадает с тем, что код делает на самом деле.

Если 15–20 минут самостоятельных попыток не помогли, пора спрашивать. Самое ценное, что вы можете приложить к вопросу, — минимальный воспроизводимый пример: несколько строк, которые запускаются как есть и показывают ту же ошибку.

Как его сделать:

  1. Скопируйте код в новый файл.
  2. Замените настоящие данные (файл на 2 ГБ, датасет) маленьким значением прямо в коде.
  3. Удаляйте всё, что не нужно для ошибки, по одному куску и запускайте после каждого удаления. Ошибка осталась — удаляйте дальше; пропала — верните кусок, он важен.

Скрипт из 60 строк, который читает корпус, строит словарь и кодирует текст, сжимается до двух:

vocab = {"<unk>": 0, "кот": 1}
print(vocab["мат"]) # KeyError: 'мат'

Часто на этом шаге вопрос задавать уже не нужно: в двух строках причина видна сама. Если нет — шаблон вопроса для форума или ИИ-помощника:

Цель: хочу закодировать текст словарём токенизатора.
Версии: Python 3.11 (python --version), torch 2.x (если вопрос про библиотеку).
Минимальный код (запускается как есть):
vocab = {"<unk>": 0, "кот": 1}
print(vocab["мат"])
Полный traceback:
<скопированный текст ошибки, не скриншот>
Ожидал: id токена <unk>, то есть 0. Получил: KeyError.
Уже пробовал: проверил, что слова «мат» нет в словаре.

Три запрета: не присылайте код скриншотом — его нельзя запустить; не пишите «не работает» без traceback; не вставляйте в вопрос пароли, API-ключи и личные данные.

Исправленный баг — ценный материал. Запишите его в свою тетрадь ошибоктетрадь ошибокEN mistake notebookКитайская школьная практика — переписывать каждую ошибку с правильным решением и регулярно к ней возвращаться. На сайте это страница «Тетрадь ошибок», куда сами попадают ошибки из квизов. (журнал journal/mistakes.md из урока 0.0.3), добавив к прежним четырём полям два новых — как нашёл и тест:

## 2026-10-12 · урок 0.2.5 · тема: циклы, накопление суммы
**Ошибка:** count_tokens вернула 2 вместо 8, без сообщений об ошибке
**Причина:** total = len(words) перезаписывал сумму на каждом шаге
**Как нашёл:** print(f"{total=}") внутри цикла
**Исправление:** total += len(words)
**Правило на будущее:** накопление в цикле — только +=, начальное значение — до цикла
**Тест:** assert count_tokens(["а б", "в"]) == 3

Последнее поле — регрессионный тестрегрессионный тестEN regression testТест, написанный на уже исправленный баг, чтобы он не вернулся незаметно.: проверка, написанная на уже исправленный баг. Положите её в файл с тестами, и если через месяц вы случайно сломаете функцию снова, тест скажет об этом сразу. Через пару месяцев журнал покажет и ваши слабые места: у кого-то это ошибки на единицуошибка на единицуEN off-by-one errorЦикл или срез делает на один шаг больше или меньше нужного, например range(1, n) вместо range(1, n + 1). в срезах и range, у кого-то — строки вместо чисел.

flowchart LR
A["Воспроизвести<br/>на маленьком входе"] --> B["Локализовать<br/>traceback, пополам"]
B --> C["Гипотеза"]
C --> D["Проверить<br/>print, assert, отладчик"]
D -->|"не подтвердилась"| C
D -->|"подтвердилась"| E["Исправить<br/>одну вещь"]
E --> F["Тест + запись<br/>в тетрадь ошибок"]
Цикл отладки: от симптома до теста, который не даст багу вернуться

Шесть коротких функций, в каждой — ошибка, а в последней две. Докстринг говорит, что функция должна делать. Нажмите «▶ Запустить»: внизу служебная часть вызывает каждую функцию и печатает ✓ или ✗, а для упавших — тип ошибки и номер строки. Здесь есть всё из урока: логические ошибки, ошибка на единицу, TypeError, KeyError, None вместо списка и деление на ноль. Чините по одному багу и запускайте снова; когда все строки станут ✓, нажмите «✓ Проверить».

Охота на баги6 тестов
# Охота на баги: шесть коротких функций, в каждой — ошибка (в последней — две).
# Докстринг каждой функции говорит, как она ДОЛЖНА работать. Код делает не то.
#
# Порядок работы:
#   1. Нажмите «▶ Запустить» и посмотрите на вывод внизу: где неверный ответ, где ошибка и в какой строке.
#   2. Для каждой функции: гипотеза → одно исправление → снова «▶ Запустить».
#   3. Когда все ответы совпадут с ожидаемыми, нажмите «✓ Проверить».
# Переписывать функции целиком не нужно: каждый баг чинится правкой в одной-двух строках.

import traceback


# ─── Баг 1 ───────────────────────────────────────────────────────────────────
def count_long(words, min_len):
    """Сколько слов длиной не меньше min_len.

    count_long(["я", "люблю", "python"], 5) -> 2
    """
    count = 0
    for w in words:
        if len(w) >= min_len:
            count += 1
        return count


# ─── Баг 2 ───────────────────────────────────────────────────────────────────
def mean_last(losses, k):
    """Среднее последних k значений loss (k не больше длины списка).

    mean_last([4.0, 3.0, 2.0, 1.0], 2) -> 1.5
    """
    tail = losses[-k:-1]
    return sum(tail) / k


# ─── Баг 3 ───────────────────────────────────────────────────────────────────
def parse_scores(line):
    """Сумма баллов из строки, где числа записаны через пробел.

    parse_scores("10 20 30") -> 60
    """
    total = 0
    for s in line.split():
        total = total + s
    return total


# ─── Баг 4 ───────────────────────────────────────────────────────────────────
def char_freq(text):
    """Частоты символов текста; пробелы не считаются.

    char_freq("ааб") -> {"а": 2, "б": 1}
    """
    counts = {}
    for ch in text:
        if ch != " ":
            counts[ch] += 1
    return counts


# ─── Баг 5 ───────────────────────────────────────────────────────────────────
def top3(scores):
    """Три лучших результата по убыванию. Исходный список НЕ должен меняться.

    top3([5, 1, 9, 7]) -> [9, 7, 5]
    """
    scores = scores.sort(reverse=True)
    return scores[:3]


# ─── Баг 6 (их тут два) ──────────────────────────────────────────────────────
def percent(part, whole):
    """Доля part от whole в процентах. Если whole == 0, вернуть 0.0.

    percent(3, 5) -> 60.0
    """
    return part // whole * 100


# ─── Служебная часть: менять не нужно ────────────────────────────────────────
def show(call, func, *args, expected):
    """Вызывает функцию и печатает результат или ошибку с номером строки."""
    try:
        got = func(*args)
        mark = "✓" if got == expected else "✗"
        print(f"{mark} {call} → {got!r}   (ожидалось {expected!r})")
    except Exception as e:
        line = traceback.extract_tb(e.__traceback__)[-1].lineno
        print(f"✗ {call} → {type(e).__name__}: {e}   (строка {line})")


show('count_long(["я", "люблю", "python"], 5)', count_long, ["я", "люблю", "python"], 5, expected=2)
show("mean_last([4.0, 3.0, 2.0, 1.0], 2)", mean_last, [4.0, 3.0, 2.0, 1.0], 2, expected=1.5)
show('parse_scores("10 20 30")', parse_scores, "10 20 30", expected=60)
show('char_freq("ааб")', char_freq, "ааб", expected={"а": 2, "б": 1})
show("top3([5, 1, 9, 7])", top3, [5, 1, 9, 7], expected=[9, 7, 5])
show("percent(3, 5)", percent, 3, 5, expected=60.0)
show("percent(1, 0)", percent, 1, 0, expected=0.0)
Проверь себя6 вопросов
1 Traceback состоит из пяти блоков: верхний — ваш train.py, ниже — четыре блока из папки site-packages/torch. Где, скорее всего, нужно исправлять?
2 После строк nums = [3, 1, 2] и nums = nums.sort() вызов nums.append(4) падает с AttributeError: 'NoneType' object has no attribute 'append'. Почему?
3 Функция возвращает неверный ответ без всяких ошибок. Какие способы помогут найти причину?Можно выбрать несколько ответов
4 Программа из 1000 строк где-то портит данные. Сколько проверок «посередине» понадобится методом деления пополам в худшем случае?
5 Что произойдёт при x = -1 в строке assert (x > 0, "x должен быть положительным")?
6 Какая клавиша в отладчике VS Code выполняет текущую строку целиком и останавливается на следующей (Step Over)?
  • Синтаксическая ошибка не даёт запуститься всему файлу, ошибка выполнения останавливает программу на своей строке, логическая молчит. В сообщении важен не только тип, но и текст после двоеточия.
  • Traceback читают снизу вверх; исправляют обычно в самом нижнем блоке со своим файлом. NoneType в сообщении — ищите, откуда в переменной взялся None.
  • Логические баги ловят на маленьком входе с известным ответом: print(f"{x=}") и repr, assert на инварианты, деление кода пополам.
  • Отладчик VS Code (F9, F5, F10, F11, условные точки) и breakpoint() с pdb показывают все переменные, не меняя код.
  • Застряли — уточка, потом минимальный пример и вопрос по шаблону. Исправили — запись в тетрадь ошибок и регрессионный тест.

Дальше: урок 0.2.6 «Мини-проекты». Соберём всё вместе в три программы — «угадай число», частотный словарь русского текста и трекер привычек с сохранением в JSON — и научимся разбивать задачу на функции.