Pydantic AI's job is validating that a model's output and tool arguments match a defined schema before your code touches them. Whether the action a valid, well-typed tool call describes is one the agent should be allowed to take is a separate problem — type safety and authorization are different concerns, and Pydantic AI solves the first one well, leaving the second to the application built around it.
This distinction matters because Pydantic AI's core pitch is reliability: fewer malformed outputs, fewer runtime type errors, more predictable integration with the rest of a typed codebase. That reliability is real and valuable. It is easy to mistake it for safety, and the two are not the same thing.
What Pydantic AI actually validates
Pydantic AI is built on Pydantic, the validation library it shares a name with, applied to LLM interactions specifically. Its core mechanism: define a typed model for what you expect the LLM to return, or for the arguments a tool accepts, and the framework validates the model's output against that schema before passing it further. If the output does not match — wrong type, missing required field, an enum value outside the allowed set — Pydantic AI can catch the mismatch and, depending on configuration, retry the model with feedback about what was wrong.
The same validation applies to tool calls. A tool function's signature, with its typed parameters, becomes the contract the model's tool-call arguments are checked against. This closes a real and common failure mode: a model hallucinating a parameter name, passing a string where a number is expected, or omitting a required field, all of which used to surface as a runtime exception deep in application code rather than a clear, catchable validation error.
Pydantic AI also supports typed dependency injection: you can define a dependency type — a database connection, an API client, a user context object — and the framework passes an instance of it into your tool functions, keeping the tool's dependencies explicit and typed rather than pulled from global state.
Where type safety stops and authorization begins
A schema defines shape, not permission. A tool call to delete_record(record_id: int) that passes a well-formed integer is exactly as valid, from Pydantic AI's perspective, whether that record belongs to the caller's own account or to someone else's. Whether the caller is authorized to delete that specific record is an authorization decision made outside the type check, and it needs an explicit answer before the tool executes.
This gap shows up in three concrete ways.
1. Structurally valid but unauthorized tool calls
A tool that transfers funds, sends a message, or modifies a record will happily accept a schema-valid call regardless of whether the agent's current task, the caller's identity, or the current policy state permits that specific transfer, message, or modification. Pydantic AI's job ends at "this matches the shape I expect." Whether the action should happen is an authorization question that belongs in front of the tool's execution, not inside its type signature.
2. Dependency objects are as broad as you make them
Because dependency injection passes a typed object — say, a database session — into every tool that declares it as a dependency, every tool sharing that dependency type inherits whatever access that object was constructed with. If the injected database session has broad read/write access because it was convenient to set up once, every tool using it has that same broad access, independent of what each individual tool actually needs. This is the same over-provisioning risk described in least privilege for AI agents: scope the dependency to the narrowest access the specific tool requires, rather than sharing one broadly-scoped object across every tool in the agent.
3. A retry loop is not a stop condition for a dangerous action
When output validation fails, Pydantic AI's typical response is to feed the validation error back to the model and let it try again. That is the right behavior for a data-quality problem — a malformed date string, a missing field. It is the wrong mental model to carry into thinking about safety: a validation retry loop exists to get well-typed output, not to evaluate whether a well-typed output represents a safe or authorized action. An agent can retry its way to a perfectly valid, perfectly dangerous tool call.
What good looks like
Treat type validation and authorization as two separate layers, both required, neither substituting for the other.
| Layer | What it checks | Where it lives |
|---|---|---|
| Type validation (Pydantic AI) | Does this output/argument match the expected shape? | Inside the framework, before your code runs |
| Authorization | Is this caller, this agent, this task allowed to take this action? | Outside the framework, evaluated before tool execution |
| Audit | What was actually attempted and executed? | A log independent of the agent's own retry history |
Concretely:
- Add an authorization check between validation success and tool execution. A schema-valid tool call should pass through a policy decision — is this caller, agent, and task combination allowed to invoke this specific tool with these specific arguments — before the tool body runs. This is the same principle covered in continuous authorization: a permission checked once at setup time is not the same as a permission checked on every call.
- Scope dependencies to the tool, not the agent. Construct narrower dependency objects per tool rather than one broadly-scoped object injected everywhere, following the same connection-level access design used for any agent framework's external integrations.
- Log the actual arguments of every executed tool call, not just validation outcomes, so a post-incident review can reconstruct what happened independent of what the model's retry history shows. See audit trails that hold up for what a durable record needs to contain.
- Do not conflate "the model produced valid JSON" with "the model produced a safe instruction." These are evaluated by different systems, at different points in the request lifecycle, and a mature governance posture keeps them that way. The same distinction is covered in guardrails versus policies — a content check and an enforceable authorization decision are not interchangeable.
Pydantic AI is a strong choice for reducing a specific and common class of runtime failure. It was never designed to be, and should not be treated as, the authorization layer for what your agents are allowed to do. That layer has to be added deliberately, on top of the framework, by the team operating it.
Common questions
If a tool's parameters are tightly typed with enums and constrained values, does that limit the harm a call can do?
It limits the shape of the harm, not whether it occurs. A tightly constrained schema — an enum of five allowed statuses, an integer range, a required record-owner field — narrows what a malformed call looks like, which is genuinely useful for catching model errors early. It does not evaluate whether this caller, this agent, or this task should be allowed to set that status, use that range, or act on that specific owner's record. A narrow schema and a narrow authorization are different properties, and a well-designed schema can still describe an unauthorized action precisely.
Does retry-on-validation-failure create a denial-of-service risk?
It can, if left unbounded. A model that repeatedly produces invalid output and repeatedly retries consumes tokens and time on every attempt, and a task that never converges to valid output can loop far longer than expected. This is a cost and availability concern, not a correctness one — cap the retry count independently of whatever default the framework uses, and treat repeated validation failures on the same task as a signal worth alerting on rather than silently retrying indefinitely.
Should authorization checks live inside the tool function itself?
They can, but centralizing them outside individual tool implementations is usually more reliable. A check written inside each tool function has to be remembered and correctly implemented every time a new tool is added, which is the same subtractive-permission problem that affects role design generally. A shared authorization layer that every tool call passes through before execution — rather than trusting each tool's author to have added the check — is harder to accidentally skip and easier to audit as a single, consistent policy surface across the whole agent, not just the tools whose authors remembered to add one.