# Dataclasses, Pydantic and Data Validation

## 1. Dataclasses

`@dataclass` generates `__init__`, `__repr__` and `__eq__` (and optionally ordering, hashing and immutability) from annotated fields:

```python
from dataclasses import dataclass, field
from typing import List

@dataclass(frozen=True, order=True)
class Money:
    amount: int            # cents
    currency: str = "EUR"

@dataclass
class Order:
    id: int
    lines: List[str] = field(default_factory=list)   # never a mutable default directly
    def __post_init__(self):
        if self.id <= 0:
            raise ValueError("id must be positive")
```

- `frozen=True` makes instances immutable (and hashable); `order=True` adds comparisons by field order.
- `slots=True` (3.10+) and `kw_only=True` (3.10+) save memory and force keyword arguments.
- `dataclasses.asdict`, `replace(obj, amount=5)` (a copy with changes).
- Dataclasses **don't validate types** at runtime: `Money(amount="ten")` is accepted unless you check in `__post_init__`.

## 2. Pydantic

Pydantic models **parse and validate** untrusted data at runtime from type hints:

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

class CreateUser(BaseModel):
    email: EmailStr
    age: int = Field(ge=13, le=120)
    tags: list[str] = []

    @field_validator("tags")
    @classmethod
    def lowercase(cls, v): return [t.lower() for t in v]

user = CreateUser.model_validate({"email": "ana@x.io", "age": "30"})   # "30" coerced to 30
CreateUser.model_validate({"email": "bad", "age": 5})                  # ValidationError listing both fields
user.model_dump_json()                                                  # serialization
```

Pydantic v2 has a Rust core (fast), strict and lax modes, settings management (`pydantic-settings` reads env vars), and JSON Schema generation, which FastAPI uses for request validation and OpenAPI docs.

## 3. When to Use What

| Need | Choose |
|---|---|
| Internal data containers, no untrusted input | `@dataclass` (or `NamedTuple` for tiny records) |
| Validating external input: API bodies, config, messages | Pydantic models |
| Typed dicts for JSON you don't control | `TypedDict` (static checking only) |
| Domain objects with behaviour and invariants | Classes or dataclasses with validation in constructors |

Validate at the **boundary** (HTTP handler, message consumer, config loader), then pass well-typed objects inward.

## 4. Type Hints Drive Everything

Both tools read annotations. `typing.get_type_hints(cls)` resolves them; `typing.get_origin` / `get_args` decompose generics like `List[int]` or `Optional[str]`. That's how libraries know to coerce `"30"` into an `int` or to validate each list element.

## Common Pitfalls

1. **Mutable defaults** in dataclass fields: use `field(default_factory=list)`.
2. **Assuming dataclasses validate types**.
3. **Validating deep inside business logic** instead of at the boundary.
4. **Lax coercion surprises** (`"1"` → `True` for bools); use strict mode where it matters.
5. **Pydantic v1 vs v2 API differences** (`dict()` → `model_dump()`, `parse_obj` → `model_validate`).

## Quick Reference

```text
@dataclass: __init__/__repr__/__eq__ · frozen · order · field(default_factory=…) · __post_init__ for checks
Pydantic: parse + validate + coerce from type hints · Field constraints · validators · model_dump(_json)
dataclass internally, Pydantic at the boundary · TypedDict for static shapes · get_type_hints / get_origin / get_args
```
