Pydantic v2 — Defining and Validating Data Models
Think of Pydantic's validation as a bouncer at the entrance of a club. It checks everyone coming in (during __init__), but once inside, there's no continuous monitoring unless you specifically hire another bouncer to watch for rule-breaking behavior on the dance floor.
validate_assignment=True in your Pydantic model config if your application logic mutates model fields post-instantiation.The Setup
You are building an e-commerce checkout service. You leverage a Pydantic model to guarantee that items have positive integer values for quantities to prevent negative balances. An internal function updates the shopping cart item dynamic count based on user inputs.
What Does This Print?
from pydantic import BaseModel, Field
class CartItem(BaseModel):
item_id: int
quantity: int = Field(gt=0)
item = CartItem(item_id=101, quantity=1)
# Modifying the attribute during a checkout adjustment flow
item.quantity = -5
print(item)
The Output
The code executes without errors and prints a configuration containing a negative quantity! Even though quantity was defined with a strict validation constraint of gt=0 (greater than zero), Pydantic's default behavior only validates data at the instantiation boundary (during __init__). Direct mutations to attributes are unchecked, introducing potential state corruption.
Why Python Does This
Pydantic's core validation logic is written in Rust (via pydantic-core) to maximize speed. During instantiation, the engine parses parameters through specialized deserializers. However, to keep standard attribute lookup and assignment as fast as pure Python classes, Pydantic's default __setattr__ wrapper behaves like standard object assignment, bypassing the validation pipeline. Enabling validation on every assignment requires explicit opt-in config so that __setattr__ routes the new value through the Rust validation layer, which carries a performance overhead.
The Fix
from pydantic import BaseModel, Field, ConfigDict
class CartItem(BaseModel):
# Enable validate_assignment to protect against dynamic mutations
model_config = ConfigDict(validate_assignment=True)
item_id: int
quantity: int = Field(gt=0)
try:
item = CartItem(item_id=101, quantity=1)
item.quantity = -5
except Exception as e:
# This block successfully captures a ValidationError
print(f"Validation intercepted: {e}")
Setting validate_assignment=True in model_config = ConfigDict(validate_assignment=True) makes Pydantic override __setattr__ to route every field assignment through the Rust validation layer. This re-triggers the constraint checks (gt=0) on every write, so any attempt to set an invalid value raises a ValidationError immediately.
How This Fails in Real Systems
An inventory allocation engine mutated parsed models inside an asynchronous consumer loop. A bug set transaction prices to negative values during currency conversions. Because validation did not run on assignment, raw negative balances passed directly into database writes, resulting in $12,000 of ledger discrepancy before it was detected by end-of-day accounting scripts.
Key Takeaway
validate_assignment=True in your Pydantic model config if your application logic mutates model fields post-instantiation.