Easy📖Теория5 min

Исключения: try/except

Обработка ошибок в Python: try/except/else/finally, иерархия исключений и ExceptionGroup

Исключения: try/except

Исключения -- основной механизм обработки ошибок в Python. В отличие от языков, возвращающих коды ошибок, Python использует исключения для сигнализации о проблемах. Это делает код чище и позволяет отделить нормальную логику от обработки ошибок.

Что такое исключение

Исключение -- это объект, описывающий ошибку или нештатную ситуацию. Когда возникает ошибка, Python создает объект исключения и ищет обработчик в стеке вызовов.

# Exceptions happen when something goes wrong
print(1 / 0)          # ZeroDivisionError
int("hello")           # ValueError
open("nonexistent")    # FileNotFoundError
{"a": 1}["b"]          # KeyError

Конструкция try/except

Базовый синтаксис обработки исключений:

try:
    result = int(input("Enter a number: "))
    print(f"You entered: {result}")
except ValueError:
    print("That's not a valid number!")

Перехват нескольких типов

def safe_divide(a: float, b: float) -> float | None:
    """Safely divide two numbers."""
    try:
        return a / b
    except ZeroDivisionError:
        print("Cannot divide by zero")
        return None
    except TypeError:
        print("Both arguments must be numbers")
        return None

Группировка исключений

try:
    value = int(input("Number: "))
    result = 100 / value
except (ValueError, ZeroDivisionError) as e:
    # Handle both exceptions the same way
    print(f"Invalid input: {e}")

Доступ к объекту исключения

try:
    data = {"name": "Alice"}
    age = data["age"]
except KeyError as e:
    print(f"Missing key: {e}")        # Missing key: 'age'
    print(f"Exception type: {type(e)}")  # <class 'KeyError'>
    print(f"Args: {e.args}")           # ('age',)

Блоки else и finally

else -- выполняется при отсутствии исключений

def load_config(path: str) -> dict:
    """Load configuration from a JSON file."""
    import json

    try:
        with open(path) as f:
            data = json.load(f)
    except FileNotFoundError:
        print(f"Config file not found: {path}")
        return {}
    except json.JSONDecodeError as e:
        print(f"Invalid JSON: {e}")
        return {}
    else:
        # Runs ONLY if no exception occurred
        print(f"Config loaded successfully from {path}")
        return data

finally -- выполняется всегда

def process_file(path: str) -> list[str]:
    """Read and process a file, ensuring cleanup."""
    file = None
    try:
        file = open(path)
        lines = file.readlines()
        return [line.strip() for line in lines]
    except FileNotFoundError:
        print(f"File not found: {path}")
        return []
    finally:
        # ALWAYS runs — even if an exception occurs
        if file is not None:
            file.close()
            print("File handle closed")

Полная конструкция

def fetch_user_data(user_id: int) -> dict | None:
    """Fetch user data with complete error handling."""
    import json
    import urllib.request

    url = f"https://api.example.com/users/{user_id}"
    try:
        response = urllib.request.urlopen(url, timeout=5)
        data = json.loads(response.read())
    except urllib.error.HTTPError as e:
        print(f"HTTP error {e.code}: {e.reason}")
        return None
    except urllib.error.URLError as e:
        print(f"Connection error: {e.reason}")
        return None
    except json.JSONDecodeError:
        print("Invalid response format")
        return None
    else:
        print(f"Successfully fetched data for user {user_id}")
        return data
    finally:
        print("Request completed")

Иерархия встроенных исключений

Python имеет богатую иерархию исключений:

BaseException
 ├── SystemExit
 ├── KeyboardInterrupt
 ├── GeneratorExit
 └── Exception
      ├── StopIteration
      ├── ArithmeticError
      │    ├── ZeroDivisionError
      │    ├── OverflowError
      │    └── FloatingPointError
      ├── AttributeError
      ├── EOFError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── OSError
      │    ├── FileNotFoundError
      │    ├── PermissionError
      │    └── ConnectionError
      ├── TypeError
      ├── ValueError
      │    └── UnicodeError
      └── RuntimeError
           └── RecursionError

Важные правила

# WRONG: catches too much (including KeyboardInterrupt logic)
try:
    do_something()
except:  # Bare except — catches EVERYTHING
    pass

# WRONG: catches SystemExit, KeyboardInterrupt
try:
    do_something()
except BaseException:
    pass

# CORRECT: catch only Exception and its subclasses
try:
    do_something()
except Exception as e:
    print(f"Error: {e}")

Генерация исключений (raise)

def validate_age(age: int) -> None:
    """Validate that age is within acceptable range."""
    if not isinstance(age, int):
        raise TypeError(f"Age must be int, got {type(age).__name__}")
    if age < 0:
        raise ValueError(f"Age cannot be negative: {age}")
    if age > 150:
        raise ValueError(f"Age seems unrealistic: {age}")

# Usage
try:
    validate_age(-5)
except ValueError as e:
    print(e)  # Age cannot be negative: -5

Перевозбуждение исключений

def process_data(data: list) -> list:
    """Process data with logging on error."""
    try:
        return [item.upper() for item in data]
    except AttributeError:
        print("Error processing data — re-raising")
        raise  # Re-raise the SAME exception with original traceback

Цепочки исключений (from)

def read_config(path: str) -> dict:
    """Read configuration, wrapping low-level errors."""
    import json

    try:
        with open(path) as f:
            return json.load(f)
    except FileNotFoundError as e:
        # Chain exceptions — preserves the original cause
        raise RuntimeError(f"Config not found: {path}") from e
    except json.JSONDecodeError as e:
        raise RuntimeError(f"Invalid config format: {path}") from e

ExceptionGroup (Python 3.11+)

ExceptionGroup позволяет обрабатывать несколько исключений одновременно. Это особенно полезно в асинхронном коде:

# Creating an ExceptionGroup
errors = ExceptionGroup("validation errors", [
    ValueError("Name is required"),
    ValueError("Email is invalid"),
    TypeError("Age must be integer"),
])

# Handling with except* (Python 3.11+)
try:
    raise errors
except* ValueError as eg:
    # eg is an ExceptionGroup containing only ValueErrors
    for err in eg.exceptions:
        print(f"Value error: {err}")
except* TypeError as eg:
    for err in eg.exceptions:
        print(f"Type error: {err}")

Практический пример с валидацией

def validate_user(data: dict) -> None:
    """Validate user data, collecting all errors."""
    errors: list[Exception] = []

    if not data.get("name"):
        errors.append(ValueError("Name is required"))
    if not data.get("email") or "@" not in data.get("email", ""):
        errors.append(ValueError("Valid email is required"))
    if not isinstance(data.get("age"), int):
        errors.append(TypeError("Age must be an integer"))
    elif data["age"] < 0 or data["age"] > 150:
        errors.append(ValueError("Age must be between 0 and 150"))

    if errors:
        raise ExceptionGroup("User validation failed", errors)

# Handle all validation errors at once
try:
    validate_user({"name": "", "email": "bad", "age": "old"})
except* ValueError as eg:
    print(f"Found {len(eg.exceptions)} value errors:")
    for err in eg.exceptions:
        print(f"  - {err}")
except* TypeError as eg:
    print(f"Found {len(eg.exceptions)} type errors:")
    for err in eg.exceptions:
        print(f"  - {err}")

Антипаттерны обработки ошибок

# ANTIPATTERN 1: Silencing all exceptions
try:
    risky_operation()
except Exception:
    pass  # Errors are silently swallowed — very hard to debug

# ANTIPATTERN 2: Catching too broadly
try:
    value = int(user_input)
    result = process(value)
    save_to_db(result)
except Exception:
    print("Something went wrong")  # Which step failed?

# BETTER: Specific handling for each operation
try:
    value = int(user_input)
except ValueError:
    print("Invalid number format")
else:
    try:
        result = process(value)
    except ProcessingError as e:
        print(f"Processing failed: {e}")

Проверь себя

🧪

Что такое ExceptionGroup в Python 3.11+?

🧪

Почему нельзя использовать bare except (except без указания типа)?

🧪

Когда выполняется блок finally?

🧪

Что делает конструкция raise ... from e?

🧪

В каком случае выполняется блок else в конструкции try/except/else?