Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Исключения
exceptions

Исключения

Иерархия исключений, try/except/finally, пользовательские исключения

Открыть лабораториюv0.6.2Запускается локально из публичного репозитория

Исключения в Python

  • Маршрут: Основы · тема 6 из 7
  • Навыки: J4, M6
  • До урока: control_flow, functions.
  • Результат: предсказуемая политика ошибок, сохраняющая причину и контекст сбоя.
  • Основа: try/except/finally, raise и собственные исключения.
  • Работа в эксплуатации: доменные ошибки, цепочки причин и очистка. Углубление: ExceptionGroup, границы перехвата и политика частичного сбоя.
  • Подтверждение: таблица политики ошибок и тесты путей обработки сбоев.

Исключение сообщает, что текущая операция не может выполнить свой контракт. Ловите его там, где можете восстановиться, перевести ошибку на уровень домена или добавить полезный контекст. Обычное ветвление лучше оставлять в if.

#1. Иерархия исключений

BaseException
├── SystemExit                    # sys.exit() — выход из интерпретатора
├── KeyboardInterrupt             # Ctrl+C
├── GeneratorExit                 # закрытие генератора
└── Exception                     # ← все обычные исключения наследуются отсюда
    ├── ArithmeticError
    │   ├── ZeroDivisionError     # деление на ноль
    │   ├── OverflowError         # число слишком большое
    │   └── FloatingPointError
    ├── LookupError
    │   ├── IndexError            # индекс за пределами
    │   └── KeyError              # ключ не найден
    ├── ValueError                # неверное значение (правильного типа)
    ├── TypeError                 # неверный тип
    ├── AttributeError            # атрибут не найден
    ├── NameError                 # имя не определено
    ├── OSError                   # ошибки ОС (IO, файлы, сеть)
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── TimeoutError
    ├── RuntimeError              # общие ошибки выполнения
    │   └── RecursionError
    ├── StopIteration             # итератор исчерпан
    ├── ImportError               # ошибка импорта
    └── NotImplementedError       # метод не реализован
  • except Exception: — ловит всё кроме SystemExit, KeyboardInterrupt, GeneratorExit
  • except BaseException: — ловит абсолютно всё (очень редко нужно)
  • except: — то же что except BaseException: — антипаттерн

#2. Синтаксис try / except / else / finally

try: result = risky_operation() except ZeroDivisionError as e: print(f"Деление на ноль: {e}") result = 0 except (TypeError, ValueError) as e: print(f"Неверный тип или значение: {e}") raise # перебрасываем исключение else: # Выполняется ТОЛЬКО если исключений не было print(f"Успех: {result}") save_result(result) finally: # Выполняется ВСЕГДА: и при успехе, и при исключении, и при return cleanup()

#Когда что выполняется

Ситуацияexceptelsefinally
Нет исключенийнетдада
Исключение пойманоданетда
Исключение не пойманонетнетда
return в tryнетнетда, до возврата

Имя после except ... as error существует только внутри обработчика. В Python 3 оно удаляется после блока, чтобы локальная переменная не удерживала исключение вместе с traceback и всеми объектами, на которые ссылаются его кадры.

try: int("not-a-number") except ValueError as error: message = str(error) print(message) # доступно # print(error) # NameError: имя удалено после except

#Контекстный менеджер и исключение

with manager as value: вызывает manager.__enter__() перед телом блока и manager.__exit__(exc_type, exc_value, traceback) при выходе. Если ошибки не было, три аргумента с информацией об исключении равны None. Возврат True из __exit__ подавляет ошибку; False или None пропускают её дальше.

class Transaction: def __enter__(self): begin() return self def __exit__(self, exc_type, exc_value, traceback): if exc_type is None: commit() else: rollback() return False
def demo(): try: return 1 finally: print("finally!") # выведется перед возвратом 1 return 2 # перезаписывает return 1! demo() # prints "finally!", returns 2

Осторожно: return в finally перезаписывает return из try. Не делайте это.


#3. Поднятие исключений (raise)

# Создание нового исключения raise ValueError("Значение не в допустимом диапазоне") # С дополнительными атрибутами raise ValueError("Ошибка", "field_name", 400) # Перебросить текущее исключение (внутри except) try: risky() except ValueError: log_error() raise # re-raise — сохраняет оригинальный traceback # Raise None — тоже ре-рейз (редко нужно) raise

#Exception Chaining (raise ... from ...)

try: config = json.loads(config_text) except json.JSONDecodeError as e: raise ConfigError("Неверный формат конфига") from e # Трейсбэк покажет ОБА исключения с пометкой "The above exception was the direct cause of" # Подавить контекст (скрыть оригинальное исключение): try: ... except Exception: raise NewError("Другая ошибка") from None # Контекст подавлен — трейсбэк только для NewError
# __cause__ — явная цепочка (raise ... from ...) # __context__ — неявная цепочка (исключение в except блоке) # __suppress_context__ — True если использован from None

Когда except перехватывает одно исключение и поднимает новое, Python сохраняет ссылку на оригинал. Это нужно, чтобы не потерять корневую причину сбоя при добавлении контекста.

Когда что использовать:

  • raise X from Y — когда Y является прямой причиной X. Например, не удалось прочитать файл → FileNotFoundError → вы оборачиваете в ConfigError("не удалось загрузить конфиг") from original_error. Трейсбэк покажет оба исключения с пометкой The above exception was the direct cause of the following exception.
  • Неявная цепочка (__context__) — когда Y произошло случайно во время обработки X. Например, в except ValidationError вы пытаетесь залогировать и случайно делите на ноль. Python покажет оба исключения, но без «direct cause».
  • raise X from None — когда вы намеренно хотите скрыть оригинальное исключение. Удобно на границе API, где внутренние детали не должны попасть клиенту. __suppress_context__ становится True и __context__ не отображается в трейсбэке.

На практике raise ... from ... — самый частый паттерн. Неявная цепочка обычно означает баг в обработчике (вы случайно сломали код в except). from None используйте осознанно, чтобы не скрыть полезную диагностику.


#4. Пользовательские исключения

# Базовое исключение для модуля/домена class AppError(Exception): """Базовое исключение приложения.""" pass # Конкретные исключения с метаданными class ValidationError(AppError): def __init__(self, message: str, field: str = None, code: int = 400): super().__init__(message) self.field = field self.code = code def __str__(self): if self.field: return f"[{self.field}] {super().__str__()}" return super().__str__() class NotFoundError(AppError): def __init__(self, resource: str, id: int): super().__init__(f"{resource} with id={id} not found") self.resource = resource self.id = id # Иерархия для API class APIError(AppError): status_code = 500 class AuthenticationError(APIError): status_code = 401 class AuthorizationError(APIError): status_code = 403 class RateLimitError(APIError): status_code = 429 def __init__(self, retry_after: int): super().__init__(f"Rate limit exceeded. Retry after {retry_after}s") self.retry_after = retry_after

#5. EAFP vs LBYL

Два стиля обработки ошибок:

LBYL (Look Before You Leap) — проверяй перед использованием:

if key in d: value = d[key] process(value)

EAFP (Easier to Ask Forgiveness than Permission) — Pythonic стиль:

try: value = d[key] process(value) except KeyError: pass
КритерийLBYLEAFP
Ошибки редкиИзбыточная проверкаОптимально
Ошибки частыОптимальноМного except-блоков
Race conditionsВозможны (проверил → изменилось)Безопасен
ЧитаемостьЯвные условияЧище в happy path

Общее правило: для I/O (файлы, сеть, БД) — EAFP. Для предсказуемых условий — LBYL.


#6. Логирование исключений

import logging import traceback logger = logging.getLogger(__name__) def process_safely(data): try: return heavy_computation(data) except MemoryError: logger.critical("Недостаточно памяти!", exc_info=True) raise except Exception as e: # exc_info=True включает полный traceback в лог logger.exception(f"Ошибка обработки: {e}") # logger.exception эквивалентно logger.error(..., exc_info=True) return None # Получить traceback как строку: try: risky() except Exception: tb_str = traceback.format_exc() send_to_monitoring(tb_str)

#7. contextlib.suppress — тихое подавление

from contextlib import suppress # Вместо: try: os.remove("temp.txt") except FileNotFoundError: pass # Можно: with suppress(FileNotFoundError): os.remove("temp.txt") # Подавляет только указанные типы — остальные пробрасываются with suppress(KeyError, IndexError): value = data["key"][0]

#8. Предупреждения (warnings)

import warnings # Генерация предупреждений (не исключений) def deprecated_function(): warnings.warn( "deprecated_function устарела, используйте new_function()", DeprecationWarning, stacklevel=2 # указывает на вызывающий код, не на эту строку ) # ... # Фильтрация предупреждений: warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("error", category=RuntimeWarning) # превратить в исключение # Временный фильтр: with warnings.catch_warnings(): warnings.simplefilter("ignore") noisy_legacy_function()

#9. Python 3.11+: ExceptionGroup

ExceptionGroup позволяет одновременно выбросить несколько исключений — полезно для asyncio Task Group и Trio.

# ExceptionGroup содержит список исключений eg = ExceptionGroup("multiple errors", [ ValueError("ошибка 1"), TypeError("ошибка 2"), ]) raise eg # Ловить через except*: try: risky_concurrent_task() except* ValueError as eg: for exc in eg.exceptions: print(f"ValueError: {exc}") except* TypeError as eg: for exc in eg.exceptions: print(f"TypeError: {exc}")

#10. Антипаттерны

# Проблема: Голый except — ловит всё, включая KeyboardInterrupt try: something() except: pass # Проблема: except Exception: pass — проглатывает ошибки молча try: critical_operation() except Exception: pass # никто не узнает об ошибке! def bad_fallback(default): # Проблема: широкий except маскирует реальную причину. try: return complex_logic() except Exception: return default def expected_fallback(default): # Вариант: конкретный тип + логирование; неожиданное не перехватывается. try: return complex_logic() except SpecificError as error: logger.error("Expected error: %s", error) return default # Проблема: Использование исключений для обычного управления потоком def get_first(lst): try: return lst[0] except IndexError: return None # Вариант: Явная проверка def get_first(lst): return lst[0] if lst else None

#11. Практические паттерны

#Retry decorator

import time import functools from typing import Type def retry(exceptions=(Exception,), max_attempts=3, delay=1.0, backoff=2.0): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): attempt = 0 current_delay = delay while attempt < max_attempts: try: return func(*args, **kwargs) except exceptions as e: attempt += 1 if attempt >= max_attempts: raise print(f"Попытка {attempt} не удалась: {e}. Повтор через {current_delay}s") time.sleep(current_delay) current_delay *= backoff return wrapper return decorator @retry(exceptions=(ConnectionError, TimeoutError), max_attempts=3, delay=0.5) def fetch_data(url): return requests.get(url).json()

Декоратор с fallback строится так же, но типы ожидаемых ошибок нужно передать явно. Широкий Exception в библиотечном коде может скрыть дефект.

def handle_errors(default_value, exceptions): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except exceptions: return default_value return wrapper return decorator @handle_errors(default_value=None, exceptions=(ValueError, KeyError)) def read_field(payload, key): return int(payload[key])

#Иерархия для доменных ошибок

# Структура для большого проекта: class DomainError(Exception): """Базовый класс для всех бизнес-исключений.""" class UserError(DomainError): pass class UserNotFoundError(UserError): def __init__(self, user_id: int): super().__init__(f"Пользователь {user_id} не найден") self.user_id = user_id class InsufficientBalanceError(UserError): def __init__(self, balance: float, required: float): super().__init__(f"Недостаточно средств: баланс {balance}, требуется {required}") self.balance = balance self.required = required class PaymentError(DomainError): pass def handle_payment(user_id, amount): try: process_payment(user_id, amount) except UserError as error: return 400, str(error) except PaymentError as error: return 402, str(error) except DomainError: return 500, "Внутренняя ошибка" return 204, ""

#12. Модуль traceback

import traceback import sys try: raise ValueError("test error") except ValueError: # Получить traceback как строку tb_str = traceback.format_exc() # полный traceback tb_lines = traceback.format_tb(sys.exc_info()[2]) # только TB (без типа/значения) # Напечатать в stderr (стандартное поведение) traceback.print_exc() # Напечатать в файл или StringIO import io buf = io.StringIO() traceback.print_exc(file=buf) error_report = buf.getvalue() # Форматирование без активного исключения: try: risky() except Exception as e: frames = traceback.extract_tb(e.__traceback__) for frame in frames: print(f"{frame.filename}:{frame.lineno} in {frame.name}") print(f" {frame.line}")

#sys.exc_info() — тройка (type, value, traceback)

import sys def handle_any_exception(): exc_type, exc_val, exc_tb = sys.exc_info() if exc_type is None: return # нет активного исключения # Полезно для обёрток и middleware где нет прямого доступа к `as e` if issubclass(exc_type, ValueError): logger.warning(f"Validation error: {exc_val}") else: logger.exception(f"Unexpected: {exc_val}")

#13. Тестирование исключений

import pytest def divide(a, b): if b == 0: raise ZeroDivisionError("division by zero") return a / b # pytest.raises как контекстный менеджер def test_divide_by_zero(): with pytest.raises(ZeroDivisionError) as exc_info: divide(10, 0) assert "division by zero" in str(exc_info.value) assert exc_info.type is ZeroDivisionError # pytest.raises как декоратор-проверка (Python 3.10+) def test_raises_any(): with pytest.raises((ValueError, TypeError)): raise ValueError("ok") # Проверка match (regex): with pytest.raises(ValueError, match=r"invalid.*value"): raise ValueError("invalid input value") # Проверка что исключение НЕ вызывается: def test_no_exception(): result = divide(10, 2) assert result == 5.0 # просто нет raises = исключение не вызвано

#14. Паттерн: Result type (без исключений)

from dataclasses import dataclass from typing import TypeVar, Generic T = TypeVar('T') E = TypeVar('E', bound=Exception) @dataclass class Ok(Generic[T]): value: T def is_ok(self) -> bool: return True def unwrap(self) -> T: return self.value @dataclass class Err(Generic[E]): error: E def is_ok(self) -> bool: return False def unwrap(self): raise self.error Result = Ok[T] | Err[Exception] def safe_parse(s: str) -> Result: try: return Ok(int(s)) except ValueError as e: return Err(e) result = safe_parse("42") if result.is_ok(): print(result.unwrap()) # 42 # Полезно для: API-responses, pipeline-операций, когда caller решает что делать с ошибкой

#Практика: от пробы к рабочему решению

#Шаг 1. Соберите минимальный пример

Сначала разделите ожидаемые ошибки данных и системные сбои. Результат основы — узкая граница перехвата, стабильный доменный код ошибки и сохранённая исходная причина.

#Шаг 2. Проверьте поведение

class InvalidAmountError(ValueError): pass def parse_amount(raw: str) -> int: try: return int(raw) except ValueError as error: raise InvalidAmountError("amount должен быть целым") from error try: parse_amount("ten") except InvalidAmountError as error: assert isinstance(error.__cause__, ValueError) else: raise AssertionError("ошибка входа должна быть явной")

#Шаг 3. Доведите решение до рабочего сценария

Практика с подсказками: составьте таблицу «источник сбоя → кто обрабатывает → что видит вызывающий код». Не перехватывайте BaseException; отдельно проверьте ошибку данных, отсутствие файла, отказ доступа и сбой записи.

#Шаг 4. Объясните внутренний механизм

Смоделируйте пакетную обработку, где несколько независимых задач завершаются ошибкой. Сравните последовательный сбор ошибок с ExceptionGroup и объясните, где частичный результат допустим, а где операция должна завершиться целиком.

#Упражнения

  1. Напишите safe_divide(a, b) с EAFP-стилем и правильным логированием.
  2. Создайте иерархию исключений для REST API (400/401/403/404/500).
  3. Реализуйте retry(func, n=3, exceptions=(Exception,)) с экспоненциальным backoff.
  4. Объясните разницу: raise e vs raise внутри except Exception as e:.
  5. Напишите декоратор @handle_errors(default_value), возвращающий default при любом Exception.
  6. Используйте ExceptionGroup для сбора нескольких ошибок валидации полей формы и обработайте каждый тип отдельно через except*.
  7. Реализуйте Result[T, E] тип и перепишите через него функцию парсинга конфига.

#Самопроверка

  • safe_divide перехватывает только ожидаемую ошибку деления, пишет контекст без секретных данных и не скрывает TypeError.
  • Иерархия API отделяет клиентские ошибки от внутренних; ответ 500 не должен раскрывать traceback пользователю.
  • Тест retry подменяет ожидание и проверяет число попыток, интервалы и немедленное распространение неожиданной ошибки.
  • Сравните traceback для raise и raise error: первый сохраняет исходную точку повторного возбуждения.
  • Декоратор с «любым Exception» опасен: до реализации сузьте набор ожидаемых исключений или письменно обоснуйте границу, на которой допустим fallback.
  • Для ExceptionGroup проверьте, что необработанный тип продолжает распространяться.

#Исполняемая лаборатория

Закрепите тему в работе «Граница ошибок при приёме событий». В ней ожидаемые ошибки одной строки отделяются от системных и программных сбоев, которые нельзя скрывать частичным результатом.

git clone --branch v0.6.2 --depth 1 https://gitlab.potapov.me/courses/python-labs.git cd python-labs uv sync --group test uv run --group test pytest exceptions/tests

Далее: Тестирование: pytest и фикстуры