Обработка ошибок в Python: try, except, else и finally
Коротко. Исключение в Python — это объект-ошибка, который возникает во время работы программы и прерывает ее выполнение. Чтобы программа не падала, потенциально опасный код помещают в блок try, а реакцию на ошибку описывают в блоке except. К ним можно добавить else — он сработает, только если ошибок не было, и finally — он выполнится в любом случае. Главное правило: всегда указывайте конкретный тип исключения после except.
Зачем нужна обработка ошибок
Программа, которая работает только на идеальных данных, в реальности не работает вообще. Пользователь вместо числа введет слово, файл окажется удален, интернет отвалится посреди запроса, в словаре не найдется нужного ключа. Каждая из этих ситуаций по умолчанию завершает программу аварийно.
number = int(input("Введите число: "))
print(number * 2)
Три строки, и программа падает, стоит пользователю ввести «пять» вместо «5». Обработка ошибок — это способ предусмотреть такие ситуации: показать понятное сообщение, попросить ввести данные заново, подставить значение по умолчанию или хотя бы записать проблему в лог, а не оборвать работу на середине.
Чем исключение отличается от синтаксической ошибки
Ошибки в Python бывают двух видов, и путать их не стоит.
Синтаксическая ошибка (SyntaxError) — это нарушение правил языка: забытое двоеточие, незакрытая скобка, лишний отступ. Такой код не запустится вообще: интерпретатор находит проблему до начала выполнения. Обработать синтаксическую ошибку через try нельзя, ее можно только исправить.
Исключение (exception) возникает уже во время работы программы. Код синтаксически верен, но в конкретной ситуации выполнить его невозможно: делим на ноль, обращаемся к несуществующему файлу, складываем число со строкой. Именно исключения и перехватывают через try/except.
print(10 / 0)
# ZeroDivisionError: division by zero
Как читать traceback
Когда исключение никто не перехватил, Python печатает трассировку — traceback. Новички обычно пугаются этой простыни текста и пролистывают ее, хотя там прямо написано, что и где сломалось.
Traceback (most recent call last):
File "app.py", line 8, in <module>
result = average([])
File "app.py", line 4, in average
return sum(numbers) / len(numbers)
ZeroDivisionError: division by zero
Traceback читается снизу вверх. Самая нижняя строка — самое важное: тип исключения и описание причины. Здесь это деление на ноль. Строка выше показывает, где именно произошла ошибка: файл, номер строки и имя функции. Еще выше — цепочка вызовов, которая к этому привела.
На практике почти всегда достаточно последних двух-трех строк. Начиная с версии 3.11 Python еще и подчеркивает в строке конкретное место сбоя, что сильно ускоряет поиск проблемы.
Конструкция try и except
Базовый синтаксис выглядит так:
try:
# код, который может сломаться
except ValueError:
# что делать, если сломался
Python выполняет тело try. Если все прошло гладко, блок except пропускается. Если внутри возникло исключение, выполнение try немедленно прерывается на месте ошибки, а управление переходит в подходящий except.
try:
number = int(input("Введите число: "))
print("Ваше число:", number)
except ValueError:
print("Это не похоже на число")
print("Программа продолжает работу")
Обратите внимание на важную деталь: строки внутри try, идущие после места ошибки, не выполнятся. Если пользователь ввел текст, сообщение «Ваше число» не появится — Python сразу уйдет в except.
Почему нужно указывать тип исключения
Формально Python разрешает написать except без типа. Формально — но не по-хорошему.
try:
avatar = open(filepath)
except:
avatar = default_avatar
Такой обработчик ловит вообще все: и отсутствие файла, и опечатку в имени переменной, и нехватку прав, и даже нажатие Ctrl+C пользователем. Программа сделает вид, что просто не нашла файл, реальная ошибка исчезнет без следа, а искать проблему придется вслепую.
Правильный вариант — назвать конкретный тип:
try:
avatar = open(filepath)
except FileNotFoundError:
avatar = default_avatar
Если нужно поймать «все ошибки программы», используйте except Exception. В отличие от голого except, он не перехватывает служебные сигналы вроде KeyboardInterrupt и SystemExit, и программа сохранит возможность нормально завершиться.
Несколько блоков except и их порядок
На один try можно повесить сколько угодно обработчиков — Python пройдет по ним сверху вниз и выполнит первый подходящий.
try:
number = int(input("Введите число: "))
print(100 / number)
except ValueError:
print("Нужно ввести целое число")
except ZeroDivisionError:
print("На ноль делить нельзя")
Порядок блоков критичен. Специфические типы ставят выше, общие ниже. Если написать except Exception первым, все остальные обработчики станут мертвым кодом: более общий тип перехватит исключение раньше, чем до них дойдет очередь.
Если реакция на разные ошибки одинаковая, перечислите типы в скобках через запятую:
try:
number = int(input("Введите число: "))
print(100 / number)
except (ValueError, ZeroDivisionError):
print("Некорректный ввод")
Объект исключения: конструкция as
Ключевое слово as привязывает объект исключения к переменной. Через нее доступно исходное сообщение об ошибке и дополнительные атрибуты.
try:
number = int("сто")
except ValueError as e:
print("Что-то пошло не так:", e)
# Что-то пошло не так: invalid literal for int() with base 10: 'сто'
Это полезно, когда нужно записать причину в лог или показать техническую деталь в отладочном режиме. Пользователю обычно выводят свое понятное сообщение, а оригинальный текст отправляют в журнал.
Блок else: код для успешного случая
else выполняется, только если в try не возникло ни одного исключения. Он нужен, чтобы не смешивать рискованный код с обычной логикой.
try:
number = int(input("Введите число: "))
except ValueError:
print("Это не число")
else:
print("Квадрат числа:", number ** 2)
Можно ли просто дописать вычисление внутрь try? Можно, но тогда любая ошибка из этой строки тоже попадет в обработчик ValueError и будет истолкована как ошибка ввода. else держит блок try минимальным: там остается только та операция, ошибку которой вы действительно ждете.
Блок finally: код, который выполнится всегда
finally срабатывает при любом исходе: при успехе, при ошибке, при перехваченном исключении и даже при return внутри try. Его главное назначение — освободить ресурсы.
file = open("data.txt")
try:
data = file.read()
finally:
file.close() # закроется в любом случае
Важная деталь: finally не обрабатывает исключение, а только гарантирует выполнение кода. Если в try возникла ошибка и подходящего except нет, блок finally отработает, а после этого программа все равно завершится с traceback.
Когда finally, а когда with
Для файлов, соединений с базой и сетевых сокетов в Python есть более короткий инструмент — контекстный менеджер with. Он закрывает ресурс сам, и забыть об этом невозможно.
with open("data.txt") as file:
data = file.read()
# файл закрыт автоматически
Правило простое: если задача сводится к «открыть, поработать, закрыть», берите with. finally оставьте для случаев, когда завершающая логика сложнее: несколько разнородных ресурсов, откат транзакции, отправка уведомления.
Иерархия исключений
Все исключения в Python выстроены в дерево наследования. На вершине стоит BaseException, от него наследуется Exception, а от нее — почти все привычные ошибки. Служебные SystemExit и KeyboardInterrupt наследуются напрямую от BaseException и в Exception не входят — именно поэтому перехват Exception безопаснее.
Внутри дерева есть промежуточные группы. ArithmeticError объединяет ZeroDivisionError и OverflowError. LookupError — родитель IndexError и KeyError. OSError — родитель FileNotFoundError и PermissionError.
Знание иерархии дает практическую пользу: можно поймать целую группу ошибок одной строкой. Например, except LookupError перехватит и обращение к несуществующему индексу списка, и обращение к несуществующему ключу словаря.
Частые исключения и что они означают
- ValueError — тип верный, а значение нет. Возникает при int("abc")
- TypeError — операция над несовместимыми типами. Возникает при "1" + 1
- ZeroDivisionError — деление на ноль
- IndexError — индекс вышел за границы списка
- KeyError — в словаре нет такого ключа
- AttributeError — у объекта нет такого метода или атрибута, классика — вызов метода у None
- FileNotFoundError — файл по указанному пути не найден
- PermissionError — нет прав на чтение или запись
- ModuleNotFoundError — модуль не установлен или имя написано с ошибкой
- NameError — используется переменная, которой не существует, обычно опечатка
Оператор raise: генерируем ошибку сами
Иногда ошибку нужно создать вручную — например, когда данные формально корректны, но не подходят по смыслу бизнес-логики.
def set_age(age):
if age < 0:
raise ValueError("Возраст не может быть отрицательным")
return age
Вызов raise немедленно прерывает функцию и передает исключение вызывающему коду. Это гораздо лучше, чем вернуть None или напечатать сообщение: вызывающая сторона не сможет случайно проигнорировать проблему.
У raise есть еще одна форма — без аргументов внутри блока except. Она повторно выбрасывает пойманное исключение, сохраняя исходный traceback:
try:
process_data()
except ValueError as e:
logging.error("Ошибка обработки: %s", e)
raise # передаем ошибку выше
Это стандартный прием «залогировать и передать дальше»: запись о проблеме остается, но ошибка не проглатывается.
Цепочка исключений: raise from
Когда одна ошибка становится причиной другой, эту связь стоит сохранить. За это отвечает конструкция raise ... from ...:
try:
config = json.loads(raw_text)
except json.JSONDecodeError as e:
raise ValueError("Не удалось прочитать настройки") from e
В traceback появится и новое исключение, и исходное, с пометкой о том, что второе стало прямой причиной первого. Для отладки это бесценно: видно и что сломалось на уровне логики, и что именно к этому привело на низком уровне.
Свои классы исключений
Встроенных типов хватает не всегда. Когда в проекте появляются собственные правила, для них заводят собственные исключения. Достаточно унаследоваться от Exception:
class NegativeValueError(Exception):
"""Возникает при передаче отрицательного значения."""
pass
raise NegativeValueError("Значение должно быть больше нуля")
Если нужно передать вместе с ошибкой дополнительные данные, переопределите конструктор:
class InsufficientFundsError(Exception):
def __init__(self, requested, available):
super().__init__(
f"Запрошено {requested}, доступно {available}"
)
self.requested = requested
self.available = available
Правила именования простые: имя класса заканчивается на Error, у класса есть докстринг с описанием ситуации. В крупных проектах обычно делают один базовый класс приложения, а от него наследуют частные — тогда можно ловить как конкретную ошибку, так и всю группу разом.
assert: проверка, а не обработка
Оператор assert проверяет условие и выбрасывает AssertionError, если условие ложно.
def divide(a, b):
assert b != 0, "Делитель не может быть нулем"
return a / b
Важное ограничение: assert — инструмент разработчика, а не механизм обработки пользовательского ввода. При запуске Python с флагом оптимизации все проверки assert просто вырезаются из кода. Поэтому для валидации данных, приходящих извне, используйте обычный if и raise.
Логирование вместо print
Печатать ошибку через print можно в учебном скрипте, но в реальном проекте для этого есть модуль logging из стандартной библиотеки. Он добавляет к сообщению время, уровень важности и контекст, а записи уходят в файл и не теряются.
import logging
logging.basicConfig(
filename="app.log",
level=logging.ERROR,
format="%(asctime)s %(levelname)s %(message)s"
)
try:
result = 10 / 0
except ZeroDivisionError as e:
logging.error("Ошибка деления: %s", e)
У логгера пять уровней важности: DEBUG для отладочных деталей, INFO для хода работы, WARNING для предупреждений, ERROR для ошибок и CRITICAL для критических сбоев. Для записи исключения вместе с полным traceback есть отдельный метод logging.exception(), который вызывают прямо внутри except.
EAFP: почему в Python ловят, а не проверяют
В Python принят подход EAFP — «проще попросить прощения, чем разрешения». Вместо того чтобы заранее проверять все условия, код просто пытается выполнить операцию и обрабатывает ошибку, если она случилась.
# Стиль LBYL — сначала проверить
if "name" in data and isinstance(data["name"], str):
print(data["name"].upper())
# Стиль EAFP — попробовать и поймать
try:
print(data["name"].upper())
except (KeyError, AttributeError):
print("Имя не указано")
У второго варианта два плюса. Он короче и не смешивает основную логику с проверками. И он быстрее при редких ошибках: условие if вычисляется при каждом запуске, а блок except активируется только тогда, когда ошибка реально произошла.
Это не значит, что проверки не нужны вообще. Если ошибочная ситуация не редкая, а обычная — например, вы штатно ожидаете, что половина записей будет неполной, — обычный if понятнее.
Антипаттерны: как делать не надо
Голый except. Перехватывает все подряд, включая Ctrl+C, и прячет настоящие ошибки. Всегда указывайте тип или хотя бы Exception.
Молчаливое проглатывание. Конструкция except Exception: pass — самый вредный шаблон в языке. Программа продолжает работу в неизвестном состоянии, а причина сбоя исчезает бесследно. Как минимум залогируйте ошибку.
Try вокруг всего. Когда в блок try завернуты сорок строк, невозможно понять, какая именно операция может упасть, и обработчик неизбежно ловит не то, что задумано. Держите try коротким — в идеале одна-две строки.
Обработка везде. Не нужно оборачивать каждую функцию. Исключение обрабатывают там, где есть контекст для решения: показать сообщение, взять значение по умолчанию, повторить запрос. В промежуточных функциях ошибке лучше дать всплыть выше.
Исключение вместо ветвления. try/except не заменяет обычную логику. Если ситуация штатная и ожидаемая, пишите if.
Практический пример: надежный ввод
Соберем изученное в задачу, которая встречается в каждом учебном проекте: запросить у пользователя число и не сдаваться, пока он не введет корректное значение.
import logging
def get_number():
while True:
try:
value = int(input("Введите число больше нуля: "))
if value <= 0:
raise ValueError("Число должно быть положительным")
except ValueError as e:
print("Ошибка:", e)
logging.warning("Некорректный ввод: %s", e)
else:
return value
number = get_number()
print("Принято:", number)
Здесь работают сразу несколько приемов: цикл повторяет запрос до успеха, except ловит и ошибку преобразования, и нашу собственную проверку через raise, else возвращает результат только при полном успехе, а логгер сохраняет историю неудачных попыток.
Что появилось в свежих версиях Python
Начиная с версии 3.11 в языке есть несколько удобных дополнений, о которых стоит знать.
Точные указатели в traceback. Python подчеркивает в строке конкретное выражение, вызвавшее ошибку. В длинной цепочке вычислений это экономит массу времени.
Метод add_note. Позволяет прикрепить к исключению дополнительную заметку, которая попадет в traceback:
try:
process(item)
except ValueError as e:
e.add_note(f"Проблемная запись: {item}")
raise
Группы исключений и except star. Когда операция порождает сразу несколько независимых ошибок — например, при параллельных запросах, — их объединяют в ExceptionGroup и разбирают конструкцией except*. Новичку это пока не понадобится, но знать о существовании инструмента полезно.
Чек-лист по обработке ошибок
- Всегда указывайте тип после except, голый except не используйте
- Специфические обработчики ставьте выше общих
- Держите блок try максимально коротким
- Не оставляйте pass в обработчике — как минимум логируйте
- Для ресурсов берите with, а не try/finally
- Ловите ошибку там, где можете на нее осмысленно среагировать
- Для проверки входных данных используйте raise, а не assert
- В логи пишите через logging, а не через print
- Если не можете обработать ошибку полностью — залогируйте и передайте выше через голый raise
Коротко о главном
Обработка ошибок в Python строится на четырех блоках. try содержит рискованный код, except описывает реакцию на конкретный тип ошибки, else выполняется только при успехе, а finally — при любом исходе. Оператор raise позволяет выбросить исключение самому, в том числе собственного класса, унаследованного от Exception.
Главные правила короткие: указывайте конкретный тип исключения, держите блок try маленьким, никогда не оставляйте обработчик пустым и обрабатывайте ошибку там, где есть чем на нее ответить. Traceback читайте снизу вверх — нижняя строка почти всегда содержит ответ.
Умение писать код, который не падает от первого неожиданного ввода, отличает учебный скрипт от рабочей программы. Освоить Python с нуля до уровня реальных проектов можно на курсе Python в PROWEB — 8 месяцев, с 14 лет, с практикой и поддержкой в трудоустройстве. Если вы еще выбираете направление в IT, пройдите бесплатный IT-тест — он подскажет, что вам ближе.
Часто задаваемые вопросы
Что такое исключение в Python?
Это объект-ошибка, возникающий во время выполнения программы и прерывающий ее нормальную работу. Если исключение никто не перехватил, интерпретатор печатает traceback и завершает программу. От синтаксической ошибки исключение отличается тем, что код формально верен и запускается, а проблема проявляется только в конкретной ситуации.
Как правильно писать try и except?
В блок try помещают только тот код, который может вызвать ошибку, а после except обязательно указывают конкретный тип исключения. Если типов несколько и реакция на них разная, пишут несколько блоков except, размещая более специфические выше общих.
Чем отличаются блоки else и finally?
else выполняется только тогда, когда в try не возникло ни одного исключения. finally выполняется всегда: и при успехе, и при ошибке, и даже при выходе из функции через return. Первый нужен для логики успешного случая, второй — для освобождения ресурсов.
Можно ли писать except без указания типа?
Технически можно, но это антипаттерн. Голый except перехватывает вообще все, включая служебные сигналы вроде нажатия Ctrl+C, и скрывает настоящие причины сбоев. Если нужно поймать любую программную ошибку, пишите except Exception.
Как прочитать traceback?
Снизу вверх. Нижняя строка содержит тип исключения и описание причины — это самое важное. Строка над ней показывает файл, номер строки и функцию, где произошел сбой. Выше идет цепочка вызовов. В большинстве случаев хватает последних двух-трех строк.
Что делает raise без аргументов?
Повторно выбрасывает уже пойманное исключение, сохраняя исходный traceback. Используется внутри except в связке с логированием: сначала записать ошибку в журнал, затем передать ее выше по стеку, чтобы ее обработал более подходящий код.
Чем ValueError отличается от TypeError?
TypeError возникает, когда операция применена к объекту неподходящего типа, например при сложении числа со строкой. ValueError возникает, когда тип правильный, а само значение некорректно: строка "abc" передана верно, но превратить ее в число нельзя.
Как создать собственное исключение?
Создайте класс, унаследованный от Exception, и дайте ему имя с окончанием Error. Если нужно передавать вместе с ошибкой дополнительные данные, переопределите метод __init__ и сохраните их в атрибутах объекта.
Нужно ли оборачивать каждую функцию в try и except?
Нет. Это загромождает код и создает ложное ощущение надежности. Исключение обрабатывают там, где есть контекст для осмысленного решения. В промежуточных функциях ошибке лучше дать всплыть выше по цепочке вызовов.
Что лучше: проверять условия через if или ловить исключения?
В Python принят подход EAFP — попробовать выполнить операцию и обработать ошибку. Он короче, не смешивает логику с проверками и быстрее при редких сбоях. Но если ошибочная ситуация штатная и происходит часто, обычная проверка через if будет понятнее.



