Исключения: 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}")