assert в Python: проверки для разработчика и AssertionError
assert — это заявление: здесь должно быть так. Провал превращается в AssertionError с вашим сообщением, а флаг -O умеет вырезать все проверки разом — поэтому инструмент разработчика, а не валидатор ввода.
Редакция Питоники
До сих пор ловили чужие ошибки — те, что бросает Python или чужой код. У программиста есть и собственный инструмент заявлений: assert. Он не обрабатывает сбои, а проверяет договорённости: делитель не ноль, список не пуст, переменная инициализирована. Проваленная договорённость — это AssertionError, отдельный тип из иерархии урока 9. Разбираемся, как писать проверки, что происходит при провале и почему у assert есть репутация ненадёжного.
Как пишется assert
Форма короткая: слово assert, условие, после запятой — сообщение. Пока условие истинно, строка невидима: ничего не происходит, выполнение идёт дальше. Ложное условие поднимает AssertionError с вашим сообщением:
def divide(a, b):
assert b != 0, 'делитель не должен быть нулём'
return a / b
print(divide(10, 2))
print('программа дошла до конца')
5.0 программа дошла до конца
Условие b != 0 оказалось истинным — и assert сработал как комментарий: один раз прочитан и забыт. Сообщение после запятой — необязательная, но важная часть: без него AssertionError молчит, и из падения не понять, какая именно договорённость нарушена.
Что происходит при провале
Проваленная проверка — обычное исключение: программа без обработки падает, traceback из урока 1 показывает строку assert и сообщение. Вот что случится, если позвать ту же функцию с нулём:
# assert упадёт: делитель нулевой
divide(10, 0)
AssertionError — полноценное исключение: его можно поймать. В песочнице это и обязательное правило — попробуем провал нарочно и посмотрим на сообщение:
def divide(a, b):
assert b != 0, 'делитель не должен быть нулём'
return a / b
try:
divide(10, 0)
except AssertionError as e:
print('проверка провалена:', e)
print('программа продолжает работу')
проверка провалена: делитель не должен быть нулём программа продолжает работу
Механика та же, что у любого except из урока 2, — но ловить AssertionError в рабочем коде обычно не надо. Проверка провалилась — значит, сломана договорённость вашего же кода: это баг, и правильная реакция — чинить код, а не глотать сигнал. У assert есть коварная особенность, из-за которой его вовсе можно не считать надёжным инструментом.
Флаг -O: проверки исчезают
assert-ы живут только в обычном режиме. Запуск python -O script.py включает оптимизацию, и Python выбрасывает все assert-строки из байт-кода, будто их не писали. Внутри программы за это отвечает имя __debug__: в обычном режиме оно True, под -O — False:
print('__debug__:', __debug__)
try:
assert False, 'эта проверка всегда проваливается'
except AssertionError as e:
print('поймали:', e)
__debug__: True поймали: эта проверка всегда проваливается
python -O script.py
Ловушка: assert с кортежем
Классический нож в спину: скобки вокруг проверки. Кажется, что (2 + 2 == 5, 'сообщение') — это условие с текстом. На деле скобки создали кортеж из двух элементов, а непустой кортеж истинен всегда — как любое непустое значение Python. Проверка не сработает никогда, даже когда условие ложно:
try:
assert (2 + 2 == 5, 'дважды два не пять')
print('assert промолчал: скобки сделали кортеж, а он всегда истинен')
except AssertionError:
print('сюда не попадём')
assert промолчал: скобки сделали кортеж, а он всегда истинен
Python даже подсказывает в предупреждении SyntaxWarning: assertion is always true, perhaps remove parentheses — но предупреждение не ошибка, и код выполняется. Правильная форма — без скобок: assert условие, 'сообщение'. Запятая здесь разделяет два аргумента, скобки не нужны.
assert или raise
Оба инструмента поднимают исключение с сообщением — разница в договоре. assert кричит «сломался мой код», ValueError — «сломались данные». Проверим оба на одном отрицательном возрасте:
def set_age_assert(age):
assert age >= 0, 'возраст не может быть отрицательным'
def set_age_raise(age):
if age < 0:
raise ValueError('возраст не может быть отрицательным')
for func in (set_age_assert, set_age_raise):
try:
func(-5)
except Exception as e:
print(type(e).__name__, '-', e)
AssertionError - возраст не может быть отрицательным ValueError - возраст не может быть отрицательным
| assert | if + raise | |
|---|---|---|
| Для кого проверка | Для разработчика: инварианты своего кода | Для данных: чужой ввод, аргументы функций |
| Тип ошибки | AssertionError — всегда один | Любой уместный: ValueError, KeyError, свой класс |
| Запуск с python -O | Вырезается, проверок нет | Работает как написано |
| Сообщение | После запятой, без скобок | Аргумент исключения |
Где assert уместен
- пред-условия функции:
assert items, 'список не должен быть пустым'— договор о том, что функция принимает; - пост-условия:
assert result <= total, 'доля не может быть больше целого'— договор о том, что функция возвращает; - невозможные ветки: в
elseварианта, которого не бывает, —assert False, f'неожиданное значение: {n}'; - тесты: фреймворк pytest строит все проверки на assert — об этом отдельный курс.
В настоящих проектах assert-ы копятся в тихих местах: перед расчётом — что списки одной длины, после загрузки — что в конфигурации все нужные ключи, после цикла — что счётчики сошлись. Каждый такой инвариант стоит одну строку и экономит часы: без него ошибка доезжает до отчёта, с ним программа падает в момент нарушения договора. Так финальный проект раздела сверяет, что при разборе файла ни одна строка не потеряна.
def classify(n):
if n > 0:
return 'положительное'
elif n < 0:
return 'отрицательное'
else:
assert False, f'неожиданное значение: {n}'
print(classify(5))
try:
classify(0)
except AssertionError as e:
print('попали в невозможную ветку:', e)
положительное попали в невозможную ветку: неожиданное значение: 0
Ноль занял ветку, которую считали невозможной, — и assert сразу назвал значение. Это типичная работа инварианта: не обрабатывать случай, а громко заявить, что представление о коде разошлось с реальностью. Для данных пользователя по-прежнему if и raise, а граница между этими решениями — тема следующего урока.
Сначала предскажи ответ в голове — это главный навык программиста.
try:
assert len('') > 0, 'строка пуста'
print('далее')
except AssertionError as e:
print('стоп:', e)
x = -3
try:
assert (x > 0, 'x должен быть положительным')
print('прошло')
except AssertionError:
print('поймано')
total = 10
part = 3
assert part <= total, 'доля больше целого'
print(total - part)
assert total - part >= 0, 'ушли в минус'
print('ок')
1. Что произойдёт при ложном условии в assert?
2. Что делает флаг -O при запуске python -O script.py?
3. Почему assert (x > 0, 'x должен быть положительным') — ошибка?
4. Какое сообщение правильное для assert?
5. Данные приходят от пользователя. Что выбрать?
6. Стоит ли ловить AssertionError в рабочем коде?
7. Что напечатает print(__debug__) при обычном запуске?
Напишите функцию take_last(items), возвращающую последний элемент списка. Поставьте в ней два assert: на входе список не пуст, на выходе результат принадлежит исходному списку (items[-1] in items). Вызовите функцию с пустым списком под try и напечатайте сообщение провалившейся проверки.
Что делает assert в Python?
Проверяет условие и ничего не делает, пока оно истинно. При ложном условии поднимает AssertionError с сообщением, указанным после запятой: assert x > 0, 'x должен быть положительным'. Это инструмент заявлений разработчика о собственном коде — инвариантов.
Урок 7: raiseПочему assert нельзя использовать для проверки ввода?
Потому что assert-ы вырезаются при запуске с флагом -O: python -O script.py убирает все проверки из байт-кода, и мусор проходит в программу молча. Валидация пользовательских данных — это if с raise ValueError или KeyError, она работает при любом запуске.
Чем AssertionError отличается от ValueError?
Смыслом договора: AssertionError — сломалась договорённость разработчика о коде (инвариант), ValueError — данные не подходят по значению. Первое чинят в коде, второе обрабатывают как ожидаемый сбой; assert можно вырезать флагом -O, ValueError — нельзя.
Урок 12: ValueErrorПочему assert со скобками не срабатывает?
Скобки вокруг условия и сообщения создают кортеж из двух элементов: assert (x > 0, 'текст'). Непустой кортеж истинен всегда, поэтому проверка проходит даже при ложном условии. Правильная форма — assert x > 0, 'текст', сообщение пишется после запятой без скобок.
Где assert применяют по-настоящему?
В пред- и пост-условиях функций, в невозможных ветках (assert False, f'неожиданное значение') и в тестах: фреймворк pytest строит все проверки на assert. Для данных пользователя — if с raise, для договорённостей о коде — assert.
Курс pytest: 20 уроковПонравился урок? Сошлитесь на него
«assert — заявление разработчика о собственном коде, и его нельзя ставить на пути чужих данных.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Исключения · Урок 7
raise: бросаем исключение сами
До сих пор ошибки бросал Python. Теперь научимся бросать сами: raise ValueError с понятным текстом, когда данные нарушают правила программы.
Исключения · Урок 9
Иерархия исключений Python: кто чей родитель
ZeroDivisionError, KeyError, FileNotFoundError — не набор случайных имён, а семейное дерево. Разбираемся, кто чей родитель и почему ловля родителя ловит детей.
Исключения · Урок 19
except или if в Python: граница применимости проверки и исключения
Отсутствие ключа: проверить if-ом или поймать KeyError? Разбираем честную границу: обычные варианты — if, редкие сбои — except, и случай, где проверка проигрывает исключению всухую.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Функции · Урок 20
Проект на функциях Python: библиотека утилит для текста
Итоговый проект раздела: своя мини-библиотека утилит для текста — normalize, word_count, top_words. Три куска, каждый запускается и проверяется отдельно.
проект функции pythonpython проект для начинающих функции
json · Урок 19
Мини-клиент API на функциях
URL, разбор ответа, фильтр, файл — каждый шаг уже умещается в одну функцию. Собираем их в клиент, который читается как сценарий.
python клиент api функциифункции python для обработки json
Исключения · Урок 14
EAFP против LBYL в Python: два стиля проверки
Look Before You Leap — смотри, прежде чем прыгнуть; Easier to Ask Forgiveness than Permission — проще извиниться, чем спросить. Одна задача, два стиля, один вердикт.
python try против if