Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 18 из 20 Начальный 15 мин 100 XP

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: делитель не должен быть нулём — и traceback с точной строкой проверки. В песочнице падение не показываем: ниже тот же вызов под ловушкой.

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
python -O script.py
Под -O строка __debug__: напечатает False, а assert False пройдёт молча: Python вырезал проверку целиком. Ради этого и пишут assert — на оптимизацию проверки не тратятся; ради этого же нельзя проверять ими данные пользователей.

Ловушка: 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 - возраст не может быть отрицательным
assertif + 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('ок')
Проверь себя
0 / 7

1. Что произойдёт при ложном условии в assert?

2. Что делает флаг -O при запуске python -O script.py?

3. Почему assert (x > 0, 'x должен быть положительным') — ошибка?

4. Какое сообщение правильное для assert?

5. Данные приходят от пользователя. Что выбрать?

6. Стоит ли ловить AssertionError в рабочем коде?

7. Что напечатает print(__debug__) при обычном запуске?

Карточки терминов
Запомнено: 0 / 8
Практика

Напишите функцию take_last(items), возвращающую последний элемент списка. Поставьте в ней два assert: на входе список не пуст, на выходе результат принадлежит исходному списку (items[-1] in items). Вызовите функцию с пустым списком под try и напечатайте сообщение провалившейся проверки.

practice.py
Вопросы и ответы по уроку

Что делает 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.