Refine Python pattern boundaries and Pydantic guidance
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# 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`
|
||||
|
||||
Reference in New Issue
Block a user