sethserver.com Subscribe
A cartoon robot with a red rectangular head featuring spiral eyes and yellow ear pieces stands against a mint green background, holding a yellow banana in its left hand and an orange in its right hand. The robot has a blue rectangular body, blue legs, red feet, and yellow hands.

Python Pydantic Validation: Stop Writing Manual Checks

By Seth Black • Updated: March 03, 2026

Python · 2 min read

Like this kind of writing? Get one email a week with notes on startups, AI, and the occasional strong opinion about Python: subscribe to the newsletter.

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:

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:

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

Share this post
Newsletter

One email, once a week.

Notes on databases, systems, and the occasional strong opinion about Python. No spam, unsubscribe anytime.

Seth Black
Written by

Seth Black

Engineer and founder based in Texas. Writes about databases, AI, and running things in production. Embeds in small teams as lead engineer.

More from Python

View all →
Python

OpenAI Bought Astral - and my fav tool uv

Mar 23, 2026
Python

Python asyncio Recipes

Mar 04, 2026
Python

Mastering Python's itertools: Efficient Data Processing and Manipulation

Mar 03, 2026