Area 08 // BackendLesson 127 / 14013 min read
Input validation with Pydantic, and config
Pydantic lets you describe the exact shape and rules of data with Python classes, so every request is checked and cleaned before your code runs, and Pydantic Settings applies the same idea to configuration loaded from environment variables.
Think of it like this
The problem it solves
Requests arrive with missing fields, wrong types, huge strings, made-up model names and negative numbers. If your code trusts them, you get crashes deep inside, confusing 500 errors, security holes and surprise bills (someone asks for max_tokens: 1000000). Config has the same problem: a typo in an environment variable silently becomes the wrong behaviour in production.
How it works: input validation with Pydantic
- Describe the data as a class inheriting
BaseModel, with type hints for each field. - Add constraints with
Field: lengths, ranges (ge,le), patterns;Literalfor allowed values; optional fields with defaults. - Add custom rules with
@field_validator(one field) or@model_validator(several fields together). - FastAPI uses it automatically: declare the model as a parameter; invalid requests get a 422 listing each problem, and your function only runs with clean data.
- Normalise as you validate: strip whitespace, convert types (
"5"→5unless strict mode). - Validate outputs too: response models hide internal fields; LLM structured outputs and tool-call arguments get the same treatment (output validation).
How it works: environment variables and config
- Config lives in the environment (12-factor), different per environment (environments and config).
BaseSettings(packagepydantic-settings) reads fields from environment variables, optionally with a prefix (APP_) and a.envfile for local work.- Types are enforced:
"true"becomesTrue,"800"becomes800, an invalid value stops start-up with a clear error. - Secrets can use
SecretStrso they don't print in logs (secrets). - Create settings once at start-up and pass them where needed (a dependency in FastAPI).
See it in code
- Install
pip install fastapi httpx pydantic-settings- Needs
- Python 3.10+.
A chat request model with limits and a custom rule, settings from environment variables, and what a bad request gets back.
import os
from typing import Literal
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
# INPUT VALIDATION: describe the request once; FastAPI rejects anything that doesn't fit with a 422.
class ChatRequest(BaseModel):
message: str = Field(min_length=1, max_length=4_000)
model: Literal["small", "large"] = "small"
max_tokens: int = Field(default=500, ge=1, le=2_000)
@field_validator("message")
@classmethod
def not_blank(cls, v: str) -> str:
if not v.strip():
raise ValueError("message is blank")
return v.strip()
# CONFIG: typed settings read from environment variables (and an optional .env file).
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env")
default_model: str = "small"
max_tokens_cap: int = 1_000
debug: bool = False
os.environ.update(APP_MAX_TOKENS_CAP="800", APP_DEBUG="true")
settings = Settings()
print("settings:", settings.model_dump())
app = FastAPI()
@app.post("/chat")
def chat(req: ChatRequest):
return {"model": req.model, "max_tokens": min(req.max_tokens, settings.max_tokens_cap), "message": req.message}
c = TestClient(app)
print(c.post("/chat", json={"message": " Hello ", "max_tokens": 1500}).json())
r = c.post("/chat", json={"message": " ", "model": "huge", "max_tokens": 0})
print(r.status_code, [(e["loc"][-1], e["msg"]) for e in r.json()["detail"]])settings: {'default_model': 'small', 'max_tokens_cap': 800, 'debug': True}
{'model': 'small', 'max_tokens': 800, 'message': 'Hello'}
422 [('message', 'Value error, message is blank'), ('model', "Input should be 'small' or 'large'"), ('max_tokens', 'Input should be greater than or equal to 1')]All three problems are reported at once, so the client can fix everything in one go. The 1,500 tokens requested passed validation (it's under 2,000) but the config cap of 800 still applies.
A real example
Example
The model name from the request
An internal API let clients pass "model": "..." straight through to the provider. Someone's script started sending the most expensive model for bulk jobs, and another client sent a misspelled name that produced 500 errors deep in the SDK. A Literal["fast", "smart"] field mapped to approved model IDs in config fixed both: unknown values got a clear 422, and the mapping (in settings, per environment) meant the team could change the actual model behind "smart" without touching clients.
The easy way
Start here
Write one Pydantic model per endpoint's input with sensible Field limits, use Literal for choices, and load config with one BaseSettings class. FastAPI does the rest.
When you grow: the production setup
Trade-offs
What you gain
- Bad data is stopped at the edge with clear errors.
- One declaration gives validation, conversion and docs.
- Config errors fail at start-up, not in production traffic.
What it costs you
- Very complex rules can make models hard to read.
- Lenient coercion can surprise (strings to numbers).
- Schemas need versioning as APIs evolve.
When not to use it
- There's no good reason to skip validation on external input.
Common mistakes
- Accepting
dictbodies and validating by hand. - No length limits on text fields sent to the model.
- Passing user-chosen model names or parameters straight to providers.
- Reading
os.environall over the code instead of one settings object. - Returning internal objects without a response model.
Interview questions
Try answering out loud first, then open the model answer.
How does request validation work in FastAPI?
Model answer
You declare a Pydantic model as an endpoint parameter; FastAPI parses the JSON body, validates and converts it against the model's types and constraints, and either calls your function with a typed object or returns 422 with a list of field errors. The same models feed the OpenAPI docs.
What should you validate in an LLM-backed API?
Model answer
Request inputs (lengths, allowed models and options, numeric limits like max_tokens, file types and sizes), user identity and permissions, LLM structured outputs against schemas and business rules, tool-call arguments before execution, and configuration at start-up.
How do you manage configuration in a Python service?
Model answer
A single typed settings class (Pydantic Settings) that reads environment variables with a prefix and optional .env file for local development, validates types and required values at start-up, keeps secrets as SecretStr from a secrets manager, and is injected where needed.
Quiz
Pick an answer to see whether it’s right and why.
0 of 5 answered
Key terms
- Pydantic
- A Python library for defining data shapes as classes and validating data against them. Widely used for LLM outputs and APIs.
- Validation
- Checking that data has the right shape, types and values before using it, and rejecting it with a clear error if not.
- Schema
- A precise description of what valid data looks like (fields, types, limits), such as a Pydantic model or a JSON Schema.
- Environment variable
- A named value set outside your code (in the shell, a .env file or a server's settings), used for secrets and config.
- Pydantic Settings
- A Pydantic add-on that loads typed, validated configuration from environment variables and .env files.
Before and after
Progress is saved in this browser only.