In the previous Chapter 5: RootModel, we learned how to wrap lists and dictionaries into a class to validate them. While RootModel is powerful, it still requires you to define a class.
Sometimes, defining a class feels like overkill.
Imagine you are writing a quick script to process a list of numbers. You don't want to write class NumberList(RootModel): .... You just want to say to Pydantic: "Here is a list. Make sure it contains integers."
Enter the TypeAdapter.
You have a variable data that comes from an API. It is supposed to be a list of integers, but it might contain strings or bad data.
# Raw input data
data = [1, "2", 3]
You want to convert "2" to an integer 2 and ensure everything else is correct. Creating a full class for this simple task feels heavy. You just want a validator that runs on the fly.
Think of TypeAdapter as a Handheld Barcode Scanner.
BaseModel is like a custom-built factory machine. You build it once, and it produces specific objects (User, Transaction).TypeAdapter is a portable tool. You pick it up, dial in the setting (e.g., "Scan for Integers"), and point it at any data.It exposes all of Pydantic's validation logic without forcing you to create a class structure.
Let's validate a list of integers (List[int]) immediately.
Using TypeAdapter involves two steps:
validate_python).
We import TypeAdapter and pass it a standard Python type hint.
from pydantic import TypeAdapter
from typing import List
# 1. Create the adapter. Tell it what to expect.
adapter = TypeAdapter(List[int])
# 2. Validate data
output = adapter.validate_python([1, "200", 3])
print(output)
# Output: [1, 200, 3] (All real integers now!)
Notice that output is just a standard Python list. It is not an instance of a model class.
It works perfectly for dictionaries too. Let's say we need a mapping of item names (string) to prices (float).
from typing import Dict
# Expect a Dict where keys are strings, values are floats
price_adapter = TypeAdapter(Dict[str, float])
# "10.50" string is converted to float
data = {"apple": 1.20, "banana": "10.50"}
result = price_adapter.validate_python(data)
print(result["banana"])
# Output: 10.5 (Float)
Just like Chapter 1: BaseModel, the TypeAdapter has a validate_json method. This is incredibly useful for parsing raw data from files or APIs.
json_data = '[1, 2, 3, 4]'
# Create adapter
adapter = TypeAdapter(List[int])
# Parse string directly
result = adapter.validate_json(json_data)
print(result)
# Output: [1, 2, 3, 4]
You can also go the other way: converting Python data to a JSON string.
adapter = TypeAdapter(List[int])
# Convert list back to JSON string
json_output = adapter.dump_json([1, 2, 3])
print(json_output)
# Output: b'[1,2,3]'
TypeAdapter plays nicely with models you have already defined. This is useful if you have a User model, but you receive a list of users from an API.
from pydantic import BaseModel, TypeAdapter
from typing import List
class User(BaseModel):
name: str
# Create an adapter for a LIST of User models
user_list_adapter = TypeAdapter(List[User])
# Validate a list of dicts
users = user_list_adapter.validate_python([{"name": "Alice"}, {"name": "Bob"}])
print(users[0].name)
# Output: Alice
How does TypeAdapter know how to validate a List[int] without a class definition?
When you create TypeAdapter(List[int]), Pydantic pauses to "compile" that type.
List[int].
When you call validate_python, it simply hands the data to that pre-built Rust validator.
Let's look at the source code in pydantic/type_adapter.py.
The __init__ method is where the setup happens. It doesn't store data; it stores the mechanism to process data.
# pydantic/type_adapter.py (Simplified)
class TypeAdapter(Generic[T]):
def __init__(self, type: Any, config: ConfigDict | None = None):
self._type = type
self._config = config
# 1. Initialize the core validator immediately
self._init_core_attrs(force=False)
The method _init_core_attrs does the heavy lifting. It calls the schema generator to build the Rust object.
def _init_core_attrs(self, ...):
# 2. Generate the Schema (Rules)
core_schema = schema_generator.generate_schema(self._type)
# 3. Create the Validator (The Engine)
self.validator = create_schema_validator(
schema=core_schema,
...
)
Now look at validate_python. It is a thin wrapper around self.validator.
def validate_python(self, object: Any, ...) -> T:
# 4. Delegate to the Rust engine
return self.validator.validate_python(object, ...)
Key Takeaway: The TypeAdapter is essentially a Python wrapper around a compiled Rust validator specific to the type you passed in. This is why it is extremely fast.
| Feature | BaseModel | TypeAdapter |
|---|---|---|
| Structure | Validates a dictionary (Key-Value pairs) | Validates any type (List, Int, Dict, Model) |
| Definition | Requires defining a class |
Created in one line |
| Output | Returns an instance of your Class | Returns standard Python types (list, dict, int) |
| Use Case | Structured business data (User, Product) | Ad-hoc lists, parsing JSON files, simple types |
The TypeAdapter fills the gap between strict Model definitions and raw data processing. It allows you to apply Pydantic's powerful validation engine to any Python type—Lists, Dictionaries, Unions, or simple Integers—without the boilerplate of creating a class.
We have now covered the entire surface of Pydantic:
Throughout these chapters, we kept mentioning that validation happens "in Rust" or "in the core". How does that actually work? What is this "Schema" we keep talking about?
It is time to look into the engine room.
Next Chapter: Pydantic Core Engine
Generated by Code IQ