Обработка ошибок в Python: try, except, else и finally

Обработка ошибок в 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 будет понятнее.



Другие статьи