I used to write if not isinstance(x, str) everywhere. Every endpoint. Every function that touched user data. It starts small: one Flask route, a couple fields. Then a new client shows up and sends "age": "42" (as a string), and now you're arguing with JSON instead of building software.
The problem isn't that validation is hard. It's that validation spreads. It creeps into routes, service layers, random helper functions, and that one file nobody wants to touch because it "works" but they're not sure why.
I want validation that holds up whether I'm awake or not. Whether the last person who touched the code remembered to check for empty strings or not.
Pydantic is one of those tools that keeps your codebase from turning into a pile of one-off checks.
The manual validation trap
Here's a Flask route that looks fine at first:
from flask import request, jsonify
@app.post("/users")
def create_user():
data = request.get_json(silent=True) or {}
name = data.get("name")
age = data.get("age")
email = data.get("email")
if not isinstance(name, str) or not name:
return jsonify({"error": "name is required"}), 400
if not isinstance(age, int) or age <= 0:
return jsonify({"error": "age must be a positive int"}), 400
if not isinstance(email, str) or "@" not in email:
return jsonify({"error": "email looks wrong"}), 400
# ... more fields, more rules, more regret
return jsonify({"ok": True})
This endpoint won't stay this simple. You'll add optional fields. Nested objects. "Required if this flag is true." At that point you either start pasting validation logic everywhere, or you build your own validation framework inside your app. Both options waste time.
Pydantic exists so you can stop doing that.
BaseModel basics (the part you'll use daily)
Pydantic models are typed schemas with parsing and validation built in.
from pydantic import BaseModel, Field, EmailStr
class CreateUser(BaseModel):
name: str = Field(min_length=1, max_length=100)
age: int = Field(gt=0)
email: EmailStr
This buys you a few concrete things:
- Safe coercion.
"42"can become42. - Consistent errors. No random
KeyError/TypeErrorsurprises. - One home for validation. Not scattered across routes and helpers.
Most of the time, simple constraints like Field(gt=0, max_length=100) get you pretty far.
Custom rules with @field_validator
Eventually you need a rule that isn't a simple constraint. Pydantic v2 uses @field_validator.
from pydantic import field_validator
class CreateUser(BaseModel):
name: str = Field(min_length=1, max_length=100)
age: int = Field(gt=0)
email: EmailStr
@field_validator("name")
@classmethod
def no_weird_whitespace(cls, v: str) -> str:
v = v.strip()
if " " in v:
raise ValueError("name contains double spaces")
return v
This is where you put the rules you only learn after real traffic hits your API. You write them once, in one place, and you're done.
Parsing JSON straight into a model
In Pydantic v2, you'll usually use model_validate for dicts and model_validate_json for JSON strings.
payload = request.get_json()
user = CreateUser.model_validate(payload)
Or:
user = CreateUser.model_validate_json(request.data)
Now your route can focus on being a route.
The "after" Flask endpoint
This version is easy to scan and hard to break:
from flask import request, jsonify
from pydantic import ValidationError
@app.post("/users")
def create_user():
try:
user = CreateUser.model_validate(request.get_json() or {})
except ValidationError as e:
return jsonify({"error": e.errors()}), 400
# user.name, user.age, user.email are clean and typed
return jsonify({"ok": True, "user": user.model_dump()})
The route is now a thin layer: parse, validate, call real logic, return.
And you get structured errors back. That's the difference between "the client says it's broken" and "the client can fix it in five minutes."
Pydantic v2 is faster
Pydantic v2 moved core validation into Rust (pydantic-core). It's noticeably faster on larger payloads and higher request volume. Not magic. Just a real improvement that makes the old "Pydantic is slow" complaint less common.
When not to use it
You don't need Pydantic everywhere.
Skip it if:
- You're writing a tiny script that runs once and exits.
- You're in a hot path where input is already validated upstream and you've measured the overhead.
- You control the data end-to-end and it's not messy.
But if you accept JSON from users or external APIs, manual validation turns into a recurring chore. Pydantic lets you centralize it and get back to building the thing you actually care about. If you're shipping fast and dealing with AI-generated MVP code, having centralized validation becomes even more critical when you're patching security holes and adding proper error handling after launch.
-Sethers