Good APIs and CLIs report all problems at once with exact locations, and ValidationError gives you that for free.
When input does not fit the model, Pydantic raises pydantic.ValidationError. It does not stop at the first problem: it validates every field and collects all failures.
str(exc) is a readable multi-line report. For programs, use exc.errors(), a list of dicts with:
loc: a tuple path to the failing value, such as('items', 1, 'qty')msg: a human message, such asInput should be a valid integertype: a stable machine code, such asint_parsing,missingorgreater_thaninput: the offending value, plusctxwith details like the limit that was exceeded
exc.error_count() returns the number of errors, and exc.json() gives the same list as JSON. FastAPI's 422 responses are exactly this list.
Branch on type, never on msg: messages are for humans and can change wording between versions, while error types are documented and stable.
ValidationError is a subclass of ValueError. Inside your own validators, raise ValueError (or AssertionError via assert, which is skipped under python -O, so prefer ValueError) and Pydantic turns it into a value_error entry in the collected errors, alongside any other failures.
To silence the URL line at the end of each message, set the environment variable PYDANTIC_ERRORS_INCLUDE_URL=0.
Example
from pydantic import BaseModel, ValidationError
class Line(BaseModel):
sku: str
qty: int
class Cart(BaseModel):
user_id: int
items: list[Line]
try:
Cart.model_validate({"user_id": "x", "items": [{"sku": "a", "qty": 1}, {"sku": "b"}]})
except ValidationError as exc:
print(exc.error_count()) # 2
for e in exc.errors():
print(e["loc"], e["type"])
# ('user_id',) int_parsing
# ('items', 1, 'qty') missingRemember
- All failing fields are reported in one ValidationError
- errors() gives loc, msg, type and input for each failure
- Branch on the error type, not the message text
Watch out for
- Catching broad Exception around validation and losing the structured error list.