143 lines
5.7 KiB
Markdown
143 lines
5.7 KiB
Markdown
# 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`
|
|
|