Files
python-patterns/comparison/pydantic-vs-python.md
T

5.7 KiB

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