# Pydantic vs Python Use this file when a rule *sounds* like ordinary Python data-model advice but changes once you choose Pydantic as the boundary type. For the underlying extracted evidence, see `sources/pydantic.md`. ## Quick decision rule Put the guidance in `python-patterns` when it is mainly about: - small internal value objects - plain dataclasses or attrs-style state carriers - module and API design unrelated to validation frameworks - typing contracts that static tools can understand without runtime schema machinery Put the guidance in this comparison doc when it is mainly about: - validating untrusted input at runtime - coercing raw input into typed fields - explicit serialization rules - field or model validators - aliasing, defaults, and boundary-specific schema behavior ## Where Pydantic deliberately bends the generic Python advice ### 1) Construction: ordinary initialization vs validation-at-construction Generic Python often assumes object construction mostly means assigning already-good values. Pydantic changes that on purpose: model construction is a validation boundary. **Python default:** - constructors usually assume the caller is mostly honest - invariants may live in `__post_init__`, factories, or explicit validation code - field annotations are mostly for humans, editors, and type checkers **Pydantic adjustment:** - constructing a model from external data is itself the validation step - raw inputs may be coerced into the declared field types - bad input should fail immediately with structured validation errors - use `model_validate(...)` when you want the boundary-crossing step to be explicit See: - `python-patterns/patterns/data-models.md` - `python-patterns/sources/pydantic.md` ### 2) Annotations: static contract hints vs runtime schema contract In ordinary Python, type annotations mostly describe intent and power static tooling. In Pydantic, annotations are also runtime instructions for parsing and schema behavior. **Python default:** - annotations are descriptive unless other machinery reads them - changing a type hint usually should not silently change runtime semantics **Pydantic adjustment:** - field annotations participate directly in parsing and validation - changing an annotation may change accepted input, coercion, and generated schema - treat model field types as part of the runtime boundary contract, not just editor sugar See: - `python-patterns/patterns/typing.md` - `python-patterns/sources/pydantic.md` ### 3) Models: internal state carriers vs explicit boundary objects Generic Python guidance says to keep internal state simple and avoid one class doing every job. Pydantic sharpens that into a practical split: models are strongest at I/O boundaries, not automatically everywhere. **Python default:** - use small value-like classes for internal state - split transport, persistence, and domain concerns when they diverge **Pydantic adjustment:** - treat `BaseModel` primarily as a boundary model unless you have a reason not to - use Pydantic aggressively for request payloads, config, parsed files, or other untrusted input - be cautious about making every internal object a `BaseModel` just because the framework makes it convenient See: - `python-patterns/patterns/data-models.md` - `python-patterns/sources/pydantic.md` - `fastapi-conventions/patterns/pydantic-boundaries.md` ### 4) Serialization: ad hoc dict building vs explicit dump methods Generic Python allows almost endless freedom in how objects become dictionaries or JSON. Pydantic narrows that on purpose by giving you named serialization APIs. **Python default:** - serialization may be custom and local - simple internal objects do not need a universal dump method **Pydantic adjustment:** - serialize with `model_dump()` or the corresponding explicit API, not hand-built dicts scattered around the codebase - keep the conversion step visible when leaving the boundary - let the model own field-level serialization behavior where that is the point of the model See: - `python-patterns/patterns/data-models.md` - `python-patterns/sources/pydantic.md` ### 5) Validation hooks: general object methods vs narrow phase-aware validators In ordinary Python, invariants might live in constructors, factory functions, helper functions, or mutator methods. Pydantic offers validator hooks, but they are not a license to hide arbitrary business logic in schema classes. **Python default:** - put validation where the owning abstraction is obvious - prefer straightforward code over magical hook chains **Pydantic adjustment:** - keep validators narrow and local to the field or model concern they actually own - choose validator phase deliberately: `before` for raw input shaping, `after` for parsed-value checks - treat `plain` validators as an escape hatch because they can bypass the normal type-validation path - do not turn validators into a second invisible service layer See: - `python-patterns/sources/pydantic.md` - `fastapi-conventions/patterns/pydantic-boundaries.md` ### 6) Error behavior: normal exceptions vs aggregated boundary errors Generic Python error design usually prefers exception types that map cleanly to caller decisions. Pydantic adds a boundary-specific pattern: multiple field failures can be reported together as structured validation output. **Python default:** - raise exceptions that match the semantic failure - keep error taxonomies useful to the caller **Pydantic adjustment:** - expect invalid boundary input to become `ValidationError` - treat that as part of the parsing contract, not as an incidental implementation detail - do not flatten that structure into vague strings unless the outer boundary has a reason to translate it See: - `python-patterns/patterns/error-handling.md` - `python-patterns/sources/pydantic.md` - `fastapi-conventions/patterns/errors.md`