Ошибки в циклах Python: от отступа до зависания
Ошибка в цикле повторяется на каждой итерации. Три полки цикловых ошибок — падение, молчаливая логика, зависание — и быстрый способ найти каждую.
Редакция Питоники
Циклы собирают в себе всё, что вы уже умеете: условия, переменные, списки — и все их ошибки тоже. Ошибка в цикле стоит дороже обычной: она повторяется. Десять итераций — десять повторов, тысяча — тысяча. Разложим цикловые ошибки по трём полкам: падение (программа останавливается), молчаливая логика (считает не то), зависание (не останавливается никогда) — и для каждой полки своё лечение.
IndentationError: отступ — часть синтаксиса
for i in range(3):
print(i)
Тело цикла без отступа — падение на старте. Python читает отступы как структуру программы: четыре пробела после двоеточия означают «это тело». Забыли — интерпретатор честно останавливается и показывает строку. Отступ и двоеточие — два знака, из-за которых падает половина первых циклов; полный разбор — в первом уроке раздела.
Отступ есть, но не тот: строка вне цикла
for i in range(3):
print('внутри:', i)
print('снаружи:', i)
внутри: 0 внутри: 1 внутри: 2 снаружи: 2
Ошибки нет — а замысел сломан. Строка снаружи: 2 выполнена один раз после цикла: переменная i пережила перебор и хранит последнее значение. Строка с отступом тела — внутри цикла; строка на уровне for — уже после него. Один уровень отступа меняет судьбу строки.
Off-by-one: на один шаг мимо
Классика молчаливых ошибок: цикл делает на шаг меньше или больше. Напечатаем дни недели с номерами — с первого взгляда всё правильно:
days = ['пн', 'вт', 'ср', 'чт', 'пт']
for i in range(1, len(days)):
print(i, days[i])
1 вт 2 ср 3 чт 4 пт
Понедельник потерян: range(1, len(days)) начал с индекса 1, а индекс 0 — это и есть пн. Ошибка off-by-one не падает — она молча пропускает данные. Ловится просто: прогоните цикл на маленьком списке из трёх элементов — промах виден мгновенно.
days = ['пн', 'вт', 'ср', 'чт', 'пт']
for i, day in enumerate(days):
print(i, day)
0 пн 1 вт 2 ср 3 чт 4 пт
enumerate сам раздаёт номера без ручных границ range — ошибиться в них невозможно. Если индексы всё же нужны руками, держите в голове правило range: правая граница не включается, поэтому range(len(days)) даёт ровно все индексы списка.
Изменение списка во время перебора
Самая коварная логическая ошибка: удалять элементы из того списка, по которому идёт for. Отсеем из очереди двух человек:
queue = ['Аня', 'Борис', 'Вера', 'Глеб']
for person in queue:
if person == 'Борис' or person == 'Вера':
queue.remove(person)
print(queue)
['Аня', 'Вера', 'Глеб']
Вера должна была уйти — но осталась. for держится за номера позиций: после удаления Бориса список сдвинулся, Вера встала на его место, а курсор перебора шагнул дальше — прямо мимо неё. Python не ругается: формально всё законно, вот почему ошибка так долго живёт в коде.
queue = ['Аня', 'Борис', 'Вера', 'Глеб']
stay = []
for person in queue:
if person == 'Борис' or person == 'Вера':
continue
stay.append(person)
print(stay)
['Аня', 'Глеб']
Лечение — не трогать перебираемый список: решения складываем в новый. continue отсеивает уходящих, stay собирает оставшихся — исходный список цел, никто не пропущен. Тот же фильтр одной строкой — генератор списка: stay = [p for p in queue if p not in ('Борис', 'Вера')] — двенадцатый урок о нём.
Зависание: условие, которое не меняется
battery = 40
while battery > 0:
print('Разряд:', battery)
battery == battery - 20 # двойное равно ничего не меняет
Тихая разновидность вечного цикла: код выглядит рабочим, отступы верны, печать идёт — и не останавливается. Причина — условие выхода зависит от переменной, которая не меняется: забыли шаг, поменяли == и =, уменьшается не та переменная. Страховка та же, что в уроке про while True: лимит итераций и печатный контроль; правила while с условием — в пятом уроке.
Как отлаживать цикл за минуту
Первый приём — печатный контроль: печать ключевых переменных внутри каждой итерации показывает, где счёт пошёл не так.
prices = [89, 42, 590]
total = 0
for p in prices:
total += p
print('p =', p, '| total =', total)
print('Итог:', total)
p = 89 | total = 89 p = 42 | total = 131 p = 590 | total = 721 Итог: 721
Видно всё: какой элемент пришёл и что стало с total после него. Логическая ошибка живёт на конкретной итерации — печать её подсвечивает. Второй приём — уменьшить данные: список из трёх элементов проверяется глазами. Третий — проверить края: первый и последний шаги цикла почти всегда и есть место off-by-one.
Ошибки в циклах — не случайность, а этап, который проходят все. Разница между новичком и опытным разработчиком не в числе ошибок, а в скорости поиска: печатный контроль, маленькие данные, глаза на границах — и цикл снова честный.
IndexError: выход за границу списка
От off-by-one в границах range есть ещё один родственник — падение с IndexError. Оно выглядит как обвинение Python, но на самом деле виновата арифметика: цикл зашёл на индекс, которого в списке нет.
marks = [4, 5, 3]
for i in range(1, len(marks) + 1):
print(marks[i])
marks = [4, 5, 3]
for i in range(len(marks)):
print(i, marks[i])
0 4 1 5 2 3
len(marks) даёт тройку, и range(3) даёт индексы 0, 1, 2 — ровно столько, сколько элементов в списке. Тот же список через enumerate даст то же самое, но без ручной арифметики: enumerate сам раздаёт правильные номера. Если в коде встречается выражение len(...) рядом с числом, остановитесь и подумайте: правильно ли оно на месте — в девяти случаях из десяти ошибка именно там.
RuntimeError: словарь изменился во время перебора
Со списком Python тихо прощает правку во время перебора — элемент просто пропускается. Со словарём он не прощает: меняется размер, и интерпретатор падает с RuntimeError. Ошибка громкая, а значит — безопасная: заметили сразу.
prices = {'хлеб': 50, 'сыр': 300, 'молоко': 80}
for name in prices:
if prices[name] > 100:
del prices[name]
prices = {'хлеб': 50, 'сыр': 300, 'молоко': 80}
cheap = {}
for name, price in prices.items():
if price <= 100:
cheap[name] = price
print(cheap)
{'хлеб': 50, 'молоко': 80}Тот же приём, что со списком: не трогаем перебираемую структуру, решаем копить в новую. Ключи в новом словаре стоят в порядке первого появления, поэтому вывод предсказуем — и это ещё один повод не полагаться на порядок ключей при переборе исходного словаря.
Тихая ошибка: цикл не выполнился ни разу
Самая коварная полка — та, где цикл просто не начался. Никакой ошибки, никакого зависания: программа отработала и выдала пустоту, а человек ищет проблему в данных.
orders = []
total = 0
for order in orders:
total += order['sum']
print('Заказов:', len(orders))
print('Сумма:', total)
Заказов: 0 Сумма: 0
Ничего не сломалось — так и задумано: тело цикла не выполнилось ни разу, потому что перебирать было нечего. Пустой список — законное значение, и отчёт честно сказал «Заказов: 0». Беда начинается там, где источник данных пуст по ошибке: файл прочитали не так, запрос вернул не то поле, переменная перезаписалась раньше. Приём, который спасает: печатать длину того, что собрались перебирать, до цикла.
Сложение или присваивание: сбитый счётчик
Ошибка с накопителем выглядит не как падение, а как неправильное число — и поэтому её находят последней. Внутри цикла кто-то ставит total = 0 вместо total += x, и на каждой итерации накопленное стирается.
prices = [89, 42, 590]
total = 0
for p in prices:
total = 0
total += p
print(total)
590
590 вместо 721. Число не выглядит подозрительно — оно похоже на настоящую сумму, просто неправильную. Обратите внимание, где стоит обнуление: если перенести total = 0 перед цикл, всё заработает. Отсюда правило: накопитель инициализируют перед циклом и внутри тела только вносят вклад. Строка total = 0 внутри тела — почти всегда ошибка.
Цикл считает не то: проверка инварианта
Самый дорогой класс ошибок — тот, где программа не падает и не зависает, а считает. Чтобы поймать такое, нужна опора, на которую можно опереться: утверждение, которое верно после каждой итерации.
marks = [4, 5, 3]
count = 0
for m in marks:
count += 1
if count <= len(marks):
print('обработано', count, 'из', len(marks))
print('всего:', count)
обработано 1 из 3 обработано 2 из 3 обработано 3 из 3 всего: 3
Это и есть инвариант: после каждой итерации обработано ровно столько элементов, сколько шагов сделано. Печать внутри цикла показывает, что счётчик и длина списка идут в ногу. Когда числа в отчёте не сходятся, такая сверка обычно находится первой: расхождение видно на конкретном шаге, а не «где-то в конце».
| Полка | Симптом | Лечение |
|---|---|---|
| Падение | IndentationError, NameError | отступ тела, проверка имён переменных |
| Молчаливая логика | пропуски и лишние шаги в выводе | enumerate вместо ручных range, новый список вместо правки старого |
| Зависание | печать не кончается | изменяемая переменная условия, лимит итераций |
Сначала предскажи ответ в голове — это главный навык программиста.
for i in range(2):
print(i)
print('после:', i)
items = [10, 20, 30, 40]
for x in items:
if x == 20:
items.remove(x)
print(items)
total = 0
for p in [50, 25]:
total = 1
total += p
print(total)
1. Что произойдёт при запуске цикла for, тело которого написано без отступа?
2. Что такое off-by-one в циклах?
3. Почему удалять элементы из списка прямо во время for опасно?
4. Как правильно отфильтровать список во время перебора?
5. В теле while написано battery == battery - 20. Что произойдёт?
6. Что произойдёт, если удалять ключи из словаря прямо во время перебора по нему?
7. Почему цикл по пустому списку — это проблема, а не мелочь?
8. Цикл печатает 590 вместо правильной суммы 721. Где искать причину?
Цикл в стартер-коде пропускает первый элемент списка items = ['а', 'б', 'в'] из-за off-by-one. Перепишите его через enumerate так, чтобы печаталась каждая строка вида «0 а» — индекс и элемент.
Почему цикл for не печатает первый или последний элемент?
Это off-by-one — ошибка границ range. Правая граница range не включается, а range(1, ...) ещё и пропускает индекс 0. Самое надёжное решение — enumerate: он сам раздаёт правильные номера без ручных границ.
Урок 4: enumerateМожно ли удалять элементы из списка в цикле for?
Нельзя: перебор идёт по номерам позиций, и после удаления соседний элемент сдвигается на место удалённого — курсор его перескакивает. Соберите нужные элементы в новый список через append или генератор списка, а старый не трогайте.
Почему цикл while зависает, хотя условие написано правильно?
Скорее всего, переменная условия не меняется в теле: забыли шаг, или вместо присваивания записано сравнение (двойное равно). Проверьте: в каждой итерации должно быть действие, приближающее условие к ложности.
Урок 5: цикл whileКак быстро найти логическую ошибку в цикле?
Три приёма: печатный контроль — print ключевых переменных внутри каждой итерации; уменьшенные данные — список из трёх элементов проверяется глазами; проверка краёв — первый и последний шаги, где чаще всего живёт off-by-one.
Что такое IndexError и как его избежать в цикле?
Выход за границу списка: цикл обращается к индексу, которого нет. Обычно виновата арифметика в границах range. Лечится через len(list) — range(len(marks)) даёт ровно все существующие индексы, а enumerate делает это без ручного счёта.
Урок 4: enumerateПочему удаление элемента из списка во время for не вызывает ошибки, а из словаря — вызывает?
Список после удаления сжимается, и курсор перебора просто перескакивает соседа — Python считает это допустимым. Словарь при удалении меняет размер, а перебор идёт по внутренней таблице: интерпретатор бросает RuntimeError. В обоих случаях лечение одно — собирать результат в новую структуру, не трогая перебираемую.
Цикл отработал, но обработал ноль элементов. Как это понять?
Напечатайте len(data) перед циклом: если длина нулевая, источник данных пуст — ищите ошибку не в теле цикла, а там, где данные взялись. Пустой список сам по себе не ошибка, но в отчёте он выглядит как «всё посчитано».
Понравился урок? Сошлитесь на него
«Ошибка в цикле стоит дороже обычной: она повторяется.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Циклы · Урок 1
Цикл for в Python: первый цикл
Первый цикл for: компьютер повторяет одну строку за вас. Перебираем список, разбираем переменную цикла и тело с отступами — и видим, зачем цикл вместо копипасты.
Циклы · Урок 4
enumerate в Python: индекс и значение вместе
Нужен номер элемента вместе с самим элементом? enumerate отдаёт циклу пары «индекс — значение»: без ручного счётчика и с нумерацией с единицы через start=1.
Циклы · Урок 5
Цикл while в Python: повторять, пока условие
while повторяет тело, пока условие верно: число шагов заранее неизвестно. Разбираем проверку условия, шаг к выходу и главный риск — бесконечный цикл.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Исключения · Урок 16
Ошибки в цикле Python: try внутри for и продолжение после сбоя
Сто строк данных, одна битая — и без защиты падает весь проход. Ставим try вокруг одного элемента: сбой обрабатывается, цикл идёт дальше, сбои считаются и попадают в журнал.
python ошибки в циклеpython продолжить цикл после ошибки
Исключения · Урок 11
KeyError в Python: ошибки словарей
Словарь не нашёл ключ — и честно сказал об этим KeyError. Разбираем объект ошибки, читаем из него ключ и выбираем между get, in и честным исключением.
python ошибка keyerror
Циклы · Урок 9
else у циклов в Python: поиск без флагов
У циклов for и while есть собственный else: он срабатывает, если перебор дошёл до конца без break. Поиск «нашли или нет» — в один заход и без флагов.
python цикл elsewhile else python