Skill 寫法圖鑑

python-error-handling

wshobson | 架構與重構 | 原文 | MIT

設計 Python 的輸入驗證、例外階層與部分失敗處理

中文翻譯

---
name: python-error-handling
description: Python 錯誤處理模式,包含輸入驗證、例外階層與部分失敗的處理。在實作驗證邏輯、設計例外策略、處理批次處理的失敗,或建構穩健的 API 時使用。
---

# Python Error Handling

用適當的輸入驗證、有意義的例外與優雅的失敗處理,建構穩健的 Python 應用程式。好的錯誤處理讓除錯更容易,系統也更可靠。

## When to Use This Skill

- 驗證使用者輸入與 API 參數
- 為應用程式設計例外階層
- 處理批次操作中的部分失敗
- 把外部資料轉換成領域型別
- 建立對使用者友善的錯誤訊息
- 實作 fail-fast 的驗證模式

## Core Concepts

### 1. Fail Fast

在昂貴的操作之前就先驗證輸入。可行的話,一次回報所有驗證錯誤。

### 2. Meaningful Exceptions

使用適當的例外型別並附上 context。訊息應該說明什麼失敗了、為什麼,以及怎麼修正。

### 3. Partial Failures

在批次操作中,不要讓一個失敗就讓全部中止。把成功與失敗分開追蹤。

### 4. Preserve Context

串接例外,保留完整的錯誤軌跡以利除錯。

## Quick Start

```python
def fetch_page(url: str, page_size: int) -> Page:
    if not url:
        raise ValueError("'url' is required")
    if not 1 <= page_size <= 100:
        raise ValueError(f"'page_size' must be 1-100, got {page_size}")
    # Now safe to proceed...
```

## Fundamental Patterns

### Pattern 1: Early Input Validation

在開始任何處理之前,先在 API 邊界驗證所有輸入。

```python
def process_order(
    order_id: str,
    quantity: int,
    discount_percent: float,
) -> OrderResult:
    """Process an order with validation."""
    # Validate required fields
    if not order_id:
        raise ValueError("'order_id' is required")

    # Validate ranges
    if quantity <= 0:
        raise ValueError(f"'quantity' must be positive, got {quantity}")

    if not 0 <= discount_percent <= 100:
        raise ValueError(
            f"'discount_percent' must be 0-100, got {discount_percent}"
        )

    # Validation passed, proceed with processing
    return _process_validated_order(order_id, quantity, discount_percent)
```

### Pattern 2: Convert to Domain Types Early

在系統邊界就把字串與外部資料解析成有型別的領域物件。

```python
from enum import Enum

class OutputFormat(Enum):
    JSON = "json"
    CSV = "csv"
    PARQUET = "parquet"

def parse_output_format(value: str) -> OutputFormat:
    """Parse string to OutputFormat enum.

    Args:
        value: Format string from user input.

    Returns:
        Validated OutputFormat enum member.

    Raises:
        ValueError: If format is not recognized.
    """
    try:
        return OutputFormat(value.lower())
    except ValueError:
        valid_formats = [f.value for f in OutputFormat]
        raise ValueError(
            f"Invalid format '{value}'. "
            f"Valid options: {', '.join(valid_formats)}"
        )

# Usage at API boundary
def export_data(data: list[dict], format_str: str) -> bytes:
    output_format = parse_output_format(format_str)  # Fail fast
    # Rest of function uses typed OutputFormat
    ...
```

### Pattern 3: Pydantic for Complex Validation

用 Pydantic model 做結構化的輸入驗證,並自動產生錯誤訊息。

```python
from pydantic import BaseModel, Field, field_validator

class CreateUserInput(BaseModel):
    """Input model for user creation."""

    email: str = Field(..., min_length=5, max_length=255)
    name: str = Field(..., min_length=1, max_length=100)
    age: int = Field(ge=0, le=150)

    @field_validator("email")
    @classmethod
    def validate_email_format(cls, v: str) -> str:
        if "@" not in v or "." not in v.split("@")[-1]:
            raise ValueError("Invalid email format")
        return v.lower()

    @field_validator("name")
    @classmethod
    def normalize_name(cls, v: str) -> str:
        return v.strip().title()

# Usage
try:
    user_input = CreateUserInput(
        email="user@example.com",
        name="john doe",
        age=25,
    )
except ValidationError as e:
    # Pydantic provides detailed error information
    print(e.errors())
```

### Pattern 4: Map Errors to Standard Exceptions

適當地使用 Python 內建的例外型別,並視需要加上 context。

| Failure Type | Exception | Example |
|--------------|-----------|---------|
| 無效輸入 | `ValueError` | 錯誤的參數值 |
| 型別錯誤 | `TypeError` | 預期字串,卻拿到 int |
| 缺少項目 | `KeyError` | 找不到 dict 的 key |
| 操作失敗 | `RuntimeError` | 服務無法使用 |
| 逾時 | `TimeoutError` | 操作花太久 |
| 找不到檔案 | `FileNotFoundError` | 路徑不存在 |
| 權限被拒 | `PermissionError` | 禁止存取 |

```python
# Good: Specific exception with context
raise ValueError(f"'page_size' must be 1-100, got {page_size}")

# Avoid: Generic exception, no context
raise Exception("Invalid parameter")
```

## Detailed worked examples and patterns

詳細的章節(從 `## Advanced Patterns` 開始)放在 `references/details.md`。上面的導覽摘要不夠用時,再去讀那個檔案。

## Best Practices Summary

1. **及早驗證** - 在昂貴的操作之前先檢查輸入
2. **使用特定的例外** - 用 `ValueError`、`TypeError`,不要用籠統的 `Exception`
3. **附上 context** - 訊息應該說明是什麼、為什麼,以及怎麼修正
4. **在邊界轉換型別** - 及早把字串解析成 enum/領域型別
5. **串接例外** - 用 `raise ... from e` 保留除錯資訊
6. **處理部分失敗** - 不要因為單一項目的錯誤就中止整批
7. **使用 Pydantic** - 用於複雜的輸入驗證,並取得結構化的錯誤
8. **記錄失敗模式** - Docstring 應該列出可能的例外
9. **記錄時附上 context** - 包含 ID、數量與其他除錯資訊
10. **測試錯誤路徑** - 驗證例外有被正確拋出

英文原文

---
name: python-error-handling
description: Python error handling patterns including input validation, exception hierarchies, and partial failure handling. Use when implementing validation logic, designing exception strategies, handling batch processing failures, or building robust APIs.
---

# Python Error Handling

Build robust Python applications with proper input validation, meaningful exceptions, and graceful failure handling. Good error handling makes debugging easier and systems more reliable.

## When to Use This Skill

- Validating user input and API parameters
- Designing exception hierarchies for applications
- Handling partial failures in batch operations
- Converting external data to domain types
- Building user-friendly error messages
- Implementing fail-fast validation patterns

## Core Concepts

### 1. Fail Fast

Validate inputs early, before expensive operations. Report all validation errors at once when possible.

### 2. Meaningful Exceptions

Use appropriate exception types with context. Messages should explain what failed, why, and how to fix it.

### 3. Partial Failures

In batch operations, don't let one failure abort everything. Track successes and failures separately.

### 4. Preserve Context

Chain exceptions to maintain the full error trail for debugging.

## Quick Start

```python
def fetch_page(url: str, page_size: int) -> Page:
    if not url:
        raise ValueError("'url' is required")
    if not 1 <= page_size <= 100:
        raise ValueError(f"'page_size' must be 1-100, got {page_size}")
    # Now safe to proceed...
```

## Fundamental Patterns

### Pattern 1: Early Input Validation

Validate all inputs at API boundaries before any processing begins.

```python
def process_order(
    order_id: str,
    quantity: int,
    discount_percent: float,
) -> OrderResult:
    """Process an order with validation."""
    # Validate required fields
    if not order_id:
        raise ValueError("'order_id' is required")

    # Validate ranges
    if quantity <= 0:
        raise ValueError(f"'quantity' must be positive, got {quantity}")

    if not 0 <= discount_percent <= 100:
        raise ValueError(
            f"'discount_percent' must be 0-100, got {discount_percent}"
        )

    # Validation passed, proceed with processing
    return _process_validated_order(order_id, quantity, discount_percent)
```

### Pattern 2: Convert to Domain Types Early

Parse strings and external data into typed domain objects at system boundaries.

```python
from enum import Enum

class OutputFormat(Enum):
    JSON = "json"
    CSV = "csv"
    PARQUET = "parquet"

def parse_output_format(value: str) -> OutputFormat:
    """Parse string to OutputFormat enum.

    Args:
        value: Format string from user input.

    Returns:
        Validated OutputFormat enum member.

    Raises:
        ValueError: If format is not recognized.
    """
    try:
        return OutputFormat(value.lower())
    except ValueError:
        valid_formats = [f.value for f in OutputFormat]
        raise ValueError(
            f"Invalid format '{value}'. "
            f"Valid options: {', '.join(valid_formats)}"
        )

# Usage at API boundary
def export_data(data: list[dict], format_str: str) -> bytes:
    output_format = parse_output_format(format_str)  # Fail fast
    # Rest of function uses typed OutputFormat
    ...
```

### Pattern 3: Pydantic for Complex Validation

Use Pydantic models for structured input validation with automatic error messages.

```python
from pydantic import BaseModel, Field, field_validator

class CreateUserInput(BaseModel):
    """Input model for user creation."""

    email: str = Field(..., min_length=5, max_length=255)
    name: str = Field(..., min_length=1, max_length=100)
    age: int = Field(ge=0, le=150)

    @field_validator("email")
    @classmethod
    def validate_email_format(cls, v: str) -> str:
        if "@" not in v or "." not in v.split("@")[-1]:
            raise ValueError("Invalid email format")
        return v.lower()

    @field_validator("name")
    @classmethod
    def normalize_name(cls, v: str) -> str:
        return v.strip().title()

# Usage
try:
    user_input = CreateUserInput(
        email="user@example.com",
        name="john doe",
        age=25,
    )
except ValidationError as e:
    # Pydantic provides detailed error information
    print(e.errors())
```

### Pattern 4: Map Errors to Standard Exceptions

Use Python's built-in exception types appropriately, adding context as needed.

| Failure Type | Exception | Example |
|--------------|-----------|---------|
| Invalid input | `ValueError` | Bad parameter values |
| Wrong type | `TypeError` | Expected string, got int |
| Missing item | `KeyError` | Dict key not found |
| Operational failure | `RuntimeError` | Service unavailable |
| Timeout | `TimeoutError` | Operation took too long |
| File not found | `FileNotFoundError` | Path doesn't exist |
| Permission denied | `PermissionError` | Access forbidden |

```python
# Good: Specific exception with context
raise ValueError(f"'page_size' must be 1-100, got {page_size}")

# Avoid: Generic exception, no context
raise Exception("Invalid parameter")
```

## Detailed worked examples and patterns

Detailed sections (starting with `## Advanced Patterns`) live in `references/details.md`. Read that file when the navigation summary above is insufficient.

## Best Practices Summary

1. **Validate early** - Check inputs before expensive operations
2. **Use specific exceptions** - `ValueError`, `TypeError`, not generic `Exception`
3. **Include context** - Messages should explain what, why, and how to fix
4. **Convert types at boundaries** - Parse strings to enums/domain types early
5. **Chain exceptions** - Use `raise ... from e` to preserve debug info
6. **Handle partial failures** - Don't abort batches on single item errors
7. **Use Pydantic** - For complex input validation with structured errors
8. **Document failure modes** - Docstrings should list possible exceptions
9. **Log with context** - Include IDs, counts, and other debugging info
10. **Test error paths** - Verify exceptions are raised correctly