Introduction to Pydantic: Basics and Advanced Features
Introduction
What Is Pydantic?
In modern programming, particularly in Python development, effective data handling is essential. The stability and security of your applications often hinge on how well you manage and validate the data flowing through them. This is where Pydantic comes into play—a library that provides a straightforward approach to data validation and serialization.
Pydantic was created to simplify working with data, making it more predictable and secure. Its primary goal is to offer strict type enforcement and validation with minimal effort from the developer. By leveraging Python’s type annotations to define data models, Pydantic automatically generates validators that ensure the provided data matches these type specifications.
Key Features of Pydantic:
Data Models: Pydantic makes it convenient to describe data models using Python classes. Each model represents a collection of fields with specified data types and optional default values. This approach lets you explicitly define the structure of your data and the constraints on each field.
from pydantic import BaseModel class User(BaseModel): id: int name: str age: int = 18
Data Validation: One of Pydantic’s core strengths is automatic data validation. When you create a model instance, all incoming data is checked against the types defined in the annotations. If the data doesn’t meet the criteria, Pydantic returns informative error messages.
user = User(id='first', name='Alice', age=25)
Data Conversion: Along with validation, Pydantic automatically converts data to the expected types. For instance, if a field requires an
int
but receives a string representing a number, Pydantic will attempt a suitable conversion.user = User(id='1', name='Alice', age='25') print(user.id) # 1 print(user.age) # 25
Nested Models: Pydantic supports nested models, allowing you to construct complex data structures. This capability is particularly helpful when dealing with JSON or other data formats that often include nested objects.
class Address(BaseModel): street: str city: str class UserWithAddress(BaseModel): id: int name: str address: Address address = Address(street='123 Main St', city='New York') user = UserWithAddress(id=1, name='Alice', address=address)
Custom Data Validators: Pydantic lets you define and use custom validators for data checks, giving you greater flexibility and control.
from pydantic import validator class UserWithValidation(BaseModel): id: int name: str age: int @validator('age') def check_age(cls, v): if v < 18: raise ValueError('Age must be at least 18') return v user = UserWithValidation(id=1, name='Alice', age=25)
- Integration with Other Frameworks: Pydantic integrates seamlessly with popular frameworks like FastAPI. In FastAPI, Pydantic models are used to validate both incoming requests and outgoing responses, streamlining development and improving code quality.
A Brief History of Pydantic
Pydantic was born out of a desire to simplify and enhance data validation in Python projects. Samuel Colvin, the library’s creator and lead developer, released the initial version in 2018. It rapidly gained traction within the developer community thanks to its simplicity and efficiency.
The inspiration behind Pydantic arose from the need for more rigorous and reliable data validation in projects where poor data handling could lead to critical failures. Against the backdrop of increasing adoption of type annotations in Python, Pydantic introduced a convenient way to harness these annotations for data modeling and automatic validation.
Since its inception, Pydantic has undergone numerous changes and improvements. Some noteworthy milestones include:
Version 1.0 (2018)
- The first stable release, featuring fundamental data validation and serialization.
- Support for basic data types and type annotations.
- The ability to create simple, validated data models.
Version 1.4 (2019)
- Introduced nested model support and more complex data structures.
- Improved performance and expanded support for custom validators.
- Integrated with FastAPI, enabling automatic validation for APIs.
Version 1.7 (2020)
- Added new data types like UUID, Decimal, and IPvAnyAddress.
- Enhanced validation error messages to be more informative and user-friendly.
- Optimized performance for faster data processing.
Version 1.8 (2021)
- Expanded configuration options for models and settings files.
- Improved handling of datetime objects and support for various date/time formats.
- Introduced
create_model
, allowing dynamic model generation.
Version 2.2 (2023)
- Rewrote the
pydantic-core
module in Rust, significantly boosting performance. - Supported Python 3.8 and above.
- Introduced a new interface for configuring models.
- Further improved error messaging for clearer validation feedback.
Version 2.7 (2024)
- Introduced
serialize_as_any
to handle serialization of objects with arbitrary fields. - Enabled passing context during serialization for more flexible data handling.
- Added performance enhancements and new features like expanded type annotation capabilities and validation optimizations.
The latest versions of Pydantic offer an array of new features and improvements, making the library even more powerful and versatile. Some standout enhancements include:
- Refined Validation Mechanisms: New types of validators and improved validation logic let you validate data more accurately and flexibly.
- Performance Optimization: Significant speed improvements, especially for large and complex models, make Pydantic suitable for high-load applications and services.
- Enhanced Configuration Options: More ways to customize model and validator behavior, giving developers greater control over validation and serialization processes.
- Revamped Documentation and Examples: Updated docs with detailed examples make it easier than ever for developers to get started and leverage new capabilities.
The story of Pydantic’s growth and development is one of continued success and refinement. From its early days, the library has steadily evolved to meet developers’ needs, all while maintaining its core promise of simplicity and effectiveness. Today, Pydantic stands as one of the most popular and widely used solutions for data validation in Python, offering a rich feature set and robust performance that developers rely on daily.
Fundamentals of Working
Basic Data Types
As a tool for data validation and serialization, Pydantic supports a wide range of basic data types, enabling developers to define and validate incoming data with precision. These data types cover the most commonly used structures, giving you the flexibility and reliability you need to handle various forms of data.
1. Numeric Types
- int — Represents integer values.
- float — Represents floating-point numbers, useful for fractional values.
- Decimal — High-precision decimal numbers, often used in financial applications where exactness is crucial.
from pydantic import BaseModel, Decimal
class Product(BaseModel):
id: int
price: float
tax: Decimal
2. String Types
- str — Represents textual data.
class User(BaseModel):
username: str
first_name: str
last_name: str
3. Boolean Types
- bool — Represents logical values (True or False).
class FeatureFlag(BaseModel):
is_enabled: bool
4. Lists and Tuples
- list — A collection (list) of elements of a single type.
- tuple — A tuple that can hold elements of one or multiple types.
class Order(BaseModel):
items: list[str]
quantities: tuple[int, int, int]
5. Dictionaries
- dict — Represents key-value pairs.
class Config(BaseModel):
settings: dict[str, str]
6. Dates and Times
- datetime — Represents both date and time.
- date — Represents a date.
- time — Represents a time of day.
- timedelta — Represents the difference between two points in time.
from datetime import datetime, timedelta
class Event(BaseModel):
start_datetime: datetime
end_datetime: datetime
duration: timedelta
7. UUID
- UUID — A universally unique identifier.
from uuid import UUID
class Item(BaseModel):
id: UUID
name: str
8. EmailStr and AnyUrl
- EmailStr — Validates that a given string is a valid email address.
- AnyUrl — Validates that a given string is a well-formed URL.
from pydantic import EmailStr, AnyUrl
class Contact(BaseModel):
email: EmailStr
website: AnyUrl
Data Models
Data models are at the core of Pydantic’s validation and serialization capabilities. They allow developers to describe data structures using Python classes, with type annotations defining each field and its attributes. This approach ensures strong typing, automatic validation, and data conversion, significantly simplifying application development.
To create a data model in Pydantic, you inherit from BaseModel
and define the model’s fields with type annotations. Each attribute of the class represents a data field, including its type and, optionally, a default value.
from pydantic import BaseModel, EmailStr
from datetime import datetime
from uuid import UUID
class User(BaseModel):
id: UUID
username: str
email: EmailStr
is_active: bool = True
created_at: datetime = datetime.now()
In this example, the
User
model includes several fields:id
,username
,is_active
, andcreated_at
. The fieldsis_active
andcreated_at
have default values.
You create a model instance by passing the appropriate values to the model’s constructor. Pydantic automatically validates and converts the data based on the specified types.
from uuid import uuid4
user = User(
id=uuid4(),
username="alice",
email="alice@example.com"
)
print(user)
One of Pydantic’s key features is automatic data validation upon instance creation. Pydantic checks each provided value against the expected type and raises a validation error if the data does not meet the requirements.
try:
user = User(
id="not-a-uuid",
username="alice",
email="alice@example.com"
)
except ValueError as e:
print(e)
In this example, Pydantic raises an error because the
id
field does not receive a valid UUID.
Pydantic also makes it easy to serialize model data into formats like JSON. This is especially useful in web applications and APIs, where data is frequently transmitted as JSON.
user = User(
id=uuid4(),
username="alice",
email="alice@example.com"
)
user_json = user.model_dump_json()
print(user_json)
The
model_dump_json()
method converts the model’s data into a JSON-formatted string, ready for transmission or storage.
Pydantic supports nested models, allowing you to create complex data structures. Nested models can serve as field types, enabling hierarchies of objects.
class Address(BaseModel):
street: str
city: str
country: str
class UserWithAddress(BaseModel):
id: UUID
username: str
email: str
address: Address
address = Address(street="123 Main St", city="New York", country="USA")
user = UserWithAddress(
id=uuid4(),
username="alice",
email="alice@example.com",
address=address
)
print(user)
By using type annotations and automatic data validation, your code becomes more reliable and readable. Nested models make it possible to work with complex data structures—an essential capability when developing modern web applications and APIs. Pydantic simplifies data handling while maintaining high performance and flexibility.
Model Fields and Annotations
In Pydantic, model fields are key elements that describe the structure and types of the data within the model. Each field is defined as a class attribute, using type annotations to specify the data type.
Type annotations in Python indicate the expected data types for model fields. Pydantic leverages these annotations to automatically validate and transform input data. These annotations can range from simple (e.g., int
, str
) to more complex types (List[int]
, Dict[str, Any]
).
Example: Defining Fields with Type Annotations
from pydantic import BaseModel
from typing import List, Dict
from uuid import UUID
class Product(BaseModel):
id: UUID
name: str
price: float
tags: List[str] = []
metadata: Dict[str, str] = {}
In this example, the
Product
model includes multiple fields (id
,name
,price
,tags
, andmetadata
). The fieldstags
andmetadata
have default values, making them optional when creating an instance.
Common Field Attributes
Default Values
Default values let you specify standard values for fields not provided at model instantiation.class User(BaseModel): username: str is_active: bool = True
Aliases (Alternative Field Names)
Aliases allow you to use different field names, which can be helpful when interacting with external APIs or systems that use different naming conventions.from pydantic import BaseModel, Field class User(BaseModel): username: str email: str = Field(alias='user_email')
Validators
Validators enable custom validation rules for fields. Defined using decorators, they let you check field values before they’re saved in the model.from pydantic import field_validator class User(BaseModel): username: str age: int @field_validator('age') def check_age(cls, v): if v < 18: raise ValueError('Age must be at least 18') return v
Configuration Parameters
Configuration parameters let you customize the behavior of the entire model or specific fields. They are defined in themodel_config
attribute and passed as dictionary settings.from pydantic import ConfigDict class User(BaseModel): username: str email: str model_config = ConfigDict( str_strip_whitespace=True, str_min_length=1 )
Examples of Using Type Annotations and Field Attributes
Example 1: A Model with Various Field Types and Attributes
from pydantic import BaseModel, Field
from uuid import UUID
from typing import List
class Item(BaseModel):
id: UUID
name: str
description: str = None
price: float
tags: List[str] = Field(default_factory=list)
item = Item(
id="123e4567-e89b-12d3-a456-426614174000",
name="Laptop",
price=999.99
)
print(item)
Example 2: A Model with Aliases and Validators
from pydantic import BaseModel, Field, field_validator
class User(BaseModel):
username: str
email: str = Field(alias='user_email')
age: int
@field_validator('age')
def check_age(cls, v):
if v < 18:
raise ValueError('Age must be at least 18')
return v
user = User(
username="alice",
user_email="alice@example.com",
age=25
)
print(user)
By using model fields and type annotations, Pydantic provides a flexible mechanism for defining data structures and validating their contents. Type annotations precisely define the expected data types, while field attributes let you set additional parameters and validation rules. This combination ensures both robustness and clarity in your code, streamlining the development process and improving overall quality.
Validation and Data Handling
In this section, we’ll deepen and expand your understanding of how Pydantic validates data and the principles behind these validation mechanisms.
Validating Input Data
Data validation is one of Pydantic’s core functions, ensuring a high level of reliability and security when working with data. Pydantic’s validation principles rely heavily on Python’s type annotations and built-in validation mechanisms, enabling automatic checks to ensure that input data meets specified requirements.
Key Principles of Data Validation:
Type Annotations:
Type annotations define the expected data types for each model field. When a model instance is created, Pydantic automatically checks the provided data against these annotations. If the data doesn’t match the expected types, Pydantic raises validation errors.from pydantic import BaseModel class User(BaseModel): id: int name: str age: int # Correct data user = User(id=1, name="Alice", age=30) # Incorrect data try: user = User(id="one", name="Alice", age="thirty") except ValueError as e: print(e)
Built-In Validators:
Pydantic includes a range of built-in validators to automatically verify that data matches the expected types and formats. For instance, numeric fields must be numbers, and string fields may be checked for length or invalid characters.from pydantic import BaseModel, EmailStr class User(BaseModel): id: int email: EmailStr # Correct email user = User(id=1, email="user@example.com") # Incorrect email try: user = User(id=2, email="not-an-email") except ValueError as e: print(e)
Custom Validators:
You can create custom validation rules to handle unique or complex data requirements. This is achieved using the@field_validator
decorator, which can be applied to model fields.from pydantic import BaseModel, field_validator class User(BaseModel): id: int name: str age: int @field_validator('age') def check_age(cls, v): if v < 0: raise ValueError('Age must be a positive integer') return v try: user = User(id=1, name="Alice", age=-1) except ValueError as e: print(e)
Data Validation Mechanisms:
- Validation at Model Creation:
The primary validation mechanism in Pydantic occurs when you instantiate a model. All data passed to the model’s constructor is checked against the specified type annotations and validation rules. If the data fails validation, Pydantic raises detailed errors indicating which fields failed and why. Validating Nested Models:
If your model includes nested objects, these are also validated according to their own type annotations and rules.from pydantic import BaseModel class Address(BaseModel): street: str city: str zip_code: str class User(BaseModel): id: int name: str address: Address address = Address(street="Main St", city="New York", zip_code="10001") user = User(id=1, name="Alice", address=address)
Validating Complex Data Types:
Pydantic supports validating complex data types like lists, dictionaries, and other collections. Each element in the collection is validated based on the given type annotations.from typing import List from pydantic import BaseModel class User(BaseModel): id: int name: str friends: List[int] user = User(id=1, name="Alice", friends=[2, 3, 4]) try: user = User(id=1, name="Alice", friends=["two", "three"]) except ValueError as e: print(e)
Custom Validators
Create custom validators in Pydantic using the @field_validator
decorator. This decorator is applied to a method that performs validation on a specific field. The method receives the field’s value and may modify it if necessary. If the value fails validation, the validator should raise a ValueError
with a descriptive message.
from pydantic import BaseModel, field_validator
class User(BaseModel):
username: str
age: int
@field_validator('age')
def check_age(cls, value):
if value < 18:
raise ValueError('Age must be at least 18')
return value
Validators can also access the values of other fields in the model using the values
argument, which allows for context-dependent validation rules.
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
age: int
email: str
@field_validator('email')
def validate_email(cls, value, values):
age = values.data.get('age')
if age and age < 18:
if not value.endswith('@example.com'):
raise ValueError('Users under 18 must have an @example.com email')
return value
try:
user = User(name='Jose', age='15', email='jose@not_example.com')
except ValueError as e:
print(e)
Custom validators enable you to define complex validation logic—such as regex checks, database lookups, or external API requests—far beyond what built-in validators offer.
import re
from pydantic import BaseModel, field_validator
class User(BaseModel):
username: str
@field_validator('username')
def username_alphanumeric(cls, value):
if not re.match(r'^[a-zA-Z0-9_]+$', value):
raise ValueError('Username must be alphanumeric')
return value
try:
user = User(username='Jose@')
except ValueError as e:
print(e)
Custom validators can also apply to nested models, allowing you to validate complex data structures in detail.
from pydantic import BaseModel, field_validator
class Address(BaseModel):
street: str
city: str
class User(BaseModel):
name: str
address: Address
@field_validator('address')
def validate_address(cls, value):
if not value.street or not value.city:
raise ValueError('Both street and city must be provided')
return value
try:
user = User(name='Jose', address=Address(street='123 Main St', city=''))
except ValueError as e:
print(e)
Custom validators in Pydantic provide developers with a powerful tool to implement complex data validation rules, offering a high degree of flexibility and accuracy.
Error Handling
Data validation errors are inevitable in any application that deals with external input or user-supplied data. Pydantic offers convenient mechanisms for handling these errors, allowing developers to efficiently respond to invalid input and ensure proper data handling.
The primary exception type for signaling validation errors is ValidationError
. This exception is raised when input data does not match the model’s type annotations or validation rules.
from pydantic import BaseModel, ValidationError
class User(BaseModel):
id: int
name: str
age: int
try:
user = User(id='abc', name='Alice', age='twenty-five')
except ValidationError as e:
print(e)
In this example, a
ValidationError
will be raised due to incorrect data types forid
andage
.
ValidationError
includes detailed information about the errors, including a list of issues that occurred during validation. This makes it easy for developers to identify and correct data problems.
The output may show something like:
2 validation errors for User
id
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str]
For further information visit https://errors.pydantic.dev/2.7/v/int_parsing
age
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='twenty-five', input_type=str]
For further information visit https://errors.pydantic.dev/2.7/v/int_parsing
You can handle validation errors using standard Python exception handling constructs. This allows you to log errors, return informative messages to users, or perform other actions as needed.
import logging
from pydantic import BaseModel, ValidationError
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class User(BaseModel):
id: int
name: str
age: int
try:
user = User(id='abc', name='Alice', age='twenty-five')
except ValidationError as e:
logger.error("Validation error: %s", e)
# Additional error handling logic
In addition to using ValidationError
, you can create custom exceptions for more specialized error handling. This can be useful if you need custom handling logic for different types of validation errors.
from pydantic import BaseModel, field_validator
class CustomValidationError(Exception):
def __init__(self, message: str):
super().__init__(message)
class User(BaseModel):
id: int
name: str
age: int
@field_validator('age')
def check_age(cls, value):
if value < 0:
raise CustomValidationError('Age must be a positive integer')
return value
try:
user = User(id=1, name='Alice', age=-5)
except CustomValidationError as e:
print(e)
# Output: Age must be a positive integer
Error handling in Pydantic is a critical aspect of application development that ensures data correctness and improves reliability. By leveraging ValidationError
, custom validators, and logging, developers can effectively manage errors and maintain high data quality.
Advanced Features
Working with Nested Models
Pydantic enables developers to work with nested models, allowing you to create and validate complex data structures. Nested models are particularly useful when dealing with hierarchical data or data that contains multiple layers of nesting. By leveraging nested models, you improve both the readability and maintainability of your code while making data handling more straightforward.
You define nested models in Pydantic just like normal models, but use them as field types within other models. This approach lets you build rich, multi-layered data structures where each nested model can maintain its own validation rules and attributes.
from pydantic import BaseModel
class Address(BaseModel):
street: str
city: str
zip_code: str
class User(BaseModel):
id: int
name: str
address: Address
When creating model instances that include nested models, Pydantic automatically instantiates and validates these nested models. This eliminates the need to manually validate each level of the data structure.
address_data = {
"street": "123 Main St",
"city": "New York",
"zip_code": "10001"
}
user_data = {
"id": 1,
"name": "Alice",
"address": address_data
}
user = User(**user_data)
print(user)
Pydantic validates data at every level of nesting. If a nested model fails validation, these errors are included in a single ValidationError
structure for easy debugging.
from pydantic import BaseModel, ValidationError
class Address(BaseModel):
street: str
city: str
zip_code: str
class User(BaseModel):
id: int
name: str
address: Address
invalid_user_data = {
"id": 1,
"name": "Alice",
"address": {
"street": "123 Main St",
"city": "New York",
"zip_code": 10001 # Should be a string, not int
}
}
try:
user = User(**invalid_user_data)
except ValidationError as e:
print(e.json())
Benefits of Nested Models:
- Improved Readability and Maintainability: Nested models break down complex data structures into logical components, making the code easier to read and maintain.
- Automatic Validation at All Levels: Pydantic automatically validates data at every level of nesting, reducing the risk of errors and ensuring data integrity.
- Model Reusability: Nested models can be reused in different parts of the application, cutting down on code duplication and simplifying updates.
Examples of Nested Models:
Example 1: An Order Model with Nested Models for Items and Shipping Address
from pydantic import BaseModel
from typing import List
class Item(BaseModel):
name: str
price: float
class Address(BaseModel):
street: str
city: str
zip_code: str
class Order(BaseModel):
order_id: int
items: List[Item]
shipping_address: Address
order_data = {
"order_id": 123,
"items": [
{"name": "Laptop", "price": 999.99},
{"name": "Mouse", "price": 25.75}
],
"shipping_address": {
"street": "456 Elm St",
"city": "Los Angeles",
"zip_code": "90001"
}
}
order = Order(**order_data)
print(order)
Example 2: A Company Model with Nested Models for Employees and Office Address
from pydantic import BaseModel
from typing import List
class Address(BaseModel):
street: str
city: str
zip_code: str
class Employee(BaseModel):
employee_id: int
name: str
position: str
class Company(BaseModel):
name: str
address: Address
employees: List[Employee]
company_data = {
"name": "Tech Corp",
"address": {
"street": "789 Maple Ave",
"city": "San Francisco",
"zip_code": "94107"
},
"employees": [
{"employee_id": 1, "name": "John Doe", "position": "CEO"},
{"employee_id": 2, "name": "Jane Smith", "position": "CTO"}
]
}
company = Company(**company_data)
print(company)
Dynamic Model Creation
Sometimes, you need to dynamically create data models based on parameters or configurations known only at runtime. Pydantic’s create_model
function lets you define models on the fly, giving you flexibility in scenarios where the data structure isn’t known in advance or can change depending on application logic.
create_model
allows you to dynamically create new Pydantic models by defining their fields and attributes at runtime. This is especially useful when working with evolving data schemas, loading configurations from external sources, or generating test models.
from pydantic import BaseModel, create_model
# Dynamically create a model
DynamicModel = create_model(
'DynamicModel',
name=(str, ...), # A required string field
age=(int, 30) # An int field with a default value of 30
)
# Creating an instance of the dynamic model
dynamic_instance = DynamicModel(name='Alice')
print(dynamic_instance)
Features of create_model
:
- Field Definition: Fields are defined as keyword arguments to
create_model
where the key is the field name and the value is a tuple containing the field type and its default value. If no default is specified, the field is required. - Validators and Configuration: You can add validators using
@field_validator
and include custom configuration via a derived class. - Inheritance from Existing Models:
create_model
can base new models on existing ones, inheriting fields, validators, and configurations. This makes it easy to extend models dynamically without duplicating code.
from pydantic import BaseModel, create_model, ValidationError, field_validator
# Base model
class BaseUser(BaseModel):
id: int
# Dynamic model creation
DynamicUser = create_model(
'DynamicUser',
__base__=BaseUser,
name=(str, ...),
age=(int, ...)
)
# Add a validator
class DynamicUserWithValidator(DynamicUser):
@field_validator('age')
def check_age(cls, v):
if v < 18:
raise ValueError('Age must be at least 18')
return v
# Add configuration via inheritance
class ConfiguredUser(DynamicUserWithValidator):
class Config:
str_min_length = 1
# Creating an instance of the configured dynamic model
try:
dynamic_user = ConfiguredUser(id=1, name='Bob', age=17)
except ValidationError as e:
print(e)
Dynamic model creation is particularly handy when structures are derived from external configurations like JSON schemas, databases, or user input.
import json
from pydantic import create_model
# Example JSON schema
json_schema = '''
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"email": {"type": "string"}
},
"required": ["name", "email"]
}
'''
# Parse the JSON schema
schema = json.loads(json_schema)
# Create a model based on the JSON schema
fields = {
key: (str if value['type'] == 'string' else int, ...)
for key, value in schema['properties'].items()
}
DynamicModelFromJSON = create_model('DynamicModelFromJSON', **fields)
# Instantiate the dynamically created model
instance = DynamicModelFromJSON(name='Alice', email='alice@example.com', age=25)
print(instance)
Benefits of create_model
:
- Flexibility: Dynamic model creation lets you adapt to changes in data structures and configurations without defining every possible structure upfront.
- Convenience: Perfect for temporary or ad-hoc data structures, making it ideal for prototyping, testing, or one-off scenarios.
- Integration: Dynamically created models can inherit from existing models, reuse their validators, and integrate seamlessly into your existing codebase.
Validating Complex Data Types
Validating Lists:
Lists are a common data structure, and Pydantic makes list validation straightforward. You define a list by using Python type annotations, such as List[str]
.
class User(BaseModel):
id: int
name: str
tags: list[str]
user = User(id=1, name='Alice', tags=['admin', 'user'])
print(user)
If the data doesn’t match the expected list type, Pydantic raises a validation error:
try:
user = User(id=1, name='Alice', tags='not-a-list')
except ValidationError as e:
print(e)
Validating Dictionaries:
Dictionaries store key-value pairs, and Pydantic supports dictionary validation using type annotations like dict[str, str]
.
class Config(BaseModel):
settings: dict[str, str]
config = Config(settings={"theme": "dark", "language": "en"})
print(config)
If the data is not a valid dictionary matching the specified structure, Pydantic raises an error:
try:
config = Config(settings="not-a-dict")
except ValidationError as e:
print(e)
Validating Custom Types:
You can also define custom types and constraints. For instance, you might use a conint
(constrained integer) to ensure a value is positive.
from pydantic import BaseModel, conint
class PositiveInt(BaseModel):
value: conint(gt=0)
positive_value = PositiveInt(value=10)
print(positive_value)
try:
negative_value = PositiveInt(value=-10)
except ValueError as e:
print(e)
Complex Nested Types:
Pydantic supports composing complex data structures from lists, dictionaries, and custom types. This is particularly useful for validating and handling deeply nested and layered data.
from pydantic import BaseModel
from typing import List, Dict
class Address(BaseModel):
street: str
city: str
class User(BaseModel):
id: int
name: str
addresses: List[Address]
preferences: Dict[str, str]
user_data = {
"id": 1,
"name": "Alice",
"addresses": [
{"street": "123 Main St", "city": "New York"},
{"street": "456 Elm St", "city": "Los Angeles"}
],
"preferences": {"theme": "dark", "language": "en"}
}
user = User(**user_data)
print(user)
Integration with Other Libraries
FastAPI
FastAPI is a modern Python web framework for building APIs, notable for its speed, ease of use, and support for asynchronous operations. One of FastAPI’s standout features is its tight integration with Pydantic, which enables automatic validation and serialization of incoming API data. This greatly simplifies the development of reliable and secure web applications.
FastAPI uses Pydantic models to define request and response schemas, automating data validation and improving code quality through typed models.
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: EmailStr
@app.post("/users/")
async def create_user(user: User):
return user
In this example, the API receives JSON user data, validates it with a Pydantic model, and returns the data. FastAPI automatically validates request data against Pydantic models and returns clear error messages if validation fails.
{
"id": "not-an-integer",
"name": "Alice",
"email": "alice@example.com"
}
FastAPI will automatically return a detailed error message:
{
"detail": [
{
"type": "int_parsing",
"loc": [
"body",
"id"
],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "not-an-integer"
}
]
}
FastAPI and Pydantic also support nested models and complex data structures, enabling you to build APIs that handle intricate data relationships.
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from typing import List
app = FastAPI()
class Address(BaseModel):
street: str
city: str
class User(BaseModel):
id: int
name: str
email: EmailStr
addresses: List[Address]
@app.post("/users/")
async def create_user(user: User):
return user
FastAPI automatically generates interactive API documentation, including request and response schemas derived from Pydantic models, making your API self-documenting and easier to integrate with other systems.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
users = {}
@app.post("/users/", response_model=User)
async def create_user(user: User):
if user.id in users:
raise HTTPException(status_code=400, detail="User already exists")
users[user.id] = user
return user
@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
if user_id not in users:
raise HTTPException(status_code=404, detail="User not found")
return users[user_id]
Because FastAPI supports asynchronous operations, you can build high-performance, scalable APIs. Integration with Pydantic remains simple and effective, even when using async code.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post("/items/")
async def create_item(item: Item):
# Perform async data processing
await some_async_function(item)
return item
FastAPI and Pydantic are used in various real-world projects to create reliable, scalable web services—such as RESTful APIs for e-commerce platforms, user management systems, and integration gateways for microservices.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Item(BaseModel):
id: int
name: str
price: float
items = {}
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
if item.id in items:
raise HTTPException(status_code=400, detail="Item already exists")
items[item.id] = item
return item
@app.get("/items/", response_model=List[Item])
async def list_items():
return list(items.values())
@app.get("/items/{item_id}", response_model=Item)
async def get_item(item_id: int):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return items[item_id]
SQLAlchemy
Integrating Pydantic with SQLAlchemy involves using Pydantic models to validate data stored in or retrieved from the database. This approach combines the strengths of both libraries: SQLAlchemy for data management and Pydantic for strict data validation.
First, define your SQLAlchemy models that represent database tables. For example, a User
model:
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
email = Column(String, unique=True, index=True)
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
Next, create corresponding Pydantic models for validating incoming and outgoing data:
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
name: str
email: EmailStr
class UserRead(BaseModel):
id: int
name: str
email: EmailStr
class Config:
from_attributes = True
UserCreate
is used to validate incoming data when creating a new user, and UserRead
is used to serialize data retrieved from the database.
Here’s how to use Pydantic and SQLAlchemy together in a FastAPI endpoint:
from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy.orm import Session
app = FastAPI()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/users/", response_model=UserRead)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = db.query(User).filter(User.email == user.email).first()
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
new_user = User(name=user.name, email=user.email)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
@app.get("/users/{user_id}", response_model=UserRead)
def read_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
Update and delete operations can also be validated with Pydantic:
class UserUpdate(BaseModel):
name: str
email: EmailStr
@app.put("/users/{user_id}", response_model=UserRead)
def update_user(user_id: int, user_update: UserUpdate, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
user.name = user_update.name
user.email = user_update.email
db.commit()
db.refresh(user)
return user
@app.delete("/users/{user_id}", response_model=UserRead)
def delete_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
db.delete(user)
db.commit()
return user
Benefits of Integrating Pydantic and SQLAlchemy:
- Strict Data Validation: Pydantic ensures that data is validated before being saved to the database, improving data integrity.
- Easy Data Handling: Pydantic models make it simple to serialize and deserialize data, facilitating easy JSON handling.
- Improved Readability and Maintainability: Separating Pydantic validation models and SQLAlchemy ORM models keeps the codebase clean, well-structured, and easier to maintain.
Django
Integrating Pydantic with Django involves using Pydantic models for data validation and serialization, enhancing code reliability and readability. The main steps are to define Pydantic models and use them to validate data in Django views and other components.
First, create Pydantic models for validation and serialization. For example:
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
class UserRead(BaseModel):
id: int
username: str
email: EmailStr
class Config:
from_attributes = True
You can use these Pydantic models in Django views to validate incoming request data:
from django.http import JsonResponse
from django.views import View
from pydantic import ValidationError
from .models import User as DjangoUser
from .pydantic_models import UserCreate, UserRead
class CreateUserView(View):
def post(self, request, *args, **kwargs):
try:
data = UserCreate.model_validate_json(request.body)
except ValidationError as e:
return JsonResponse(e.errors(), status=400, safe=False)
user = DjangoUser.objects.create(
username=data.username,
email=data.email,
password=data.password # Remember to hash passwords
)
user_data = UserRead.model_validate(user)
return JsonResponse(user_data.model_dump())
While Django offers its own forms for validation, Pydantic can be used for extra checks and transformations:
from django import forms
from pydantic import BaseModel, EmailStr, ValidationError
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
class UserForm(forms.Form):
username = forms.CharField(max_length=100)
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
def clean(self):
cleaned_data = super().clean()
try:
UserCreate(**cleaned_data)
except ValidationError as e:
raise forms.ValidationError(e.errors())
Pydantic can also be used for serialization, for example when returning JSON responses from Django views:
from django.http import JsonResponse
from django.views import View
from .models import User as DjangoUser
from .pydantic_models import UserRead
class ListUsersView(View):
def get(self, request, *args, **kwargs):
users = DjangoUser.objects.all()
users_data = [UserRead.model_validate(user) for user in users]
return JsonResponse([user.model_dump() for user in users_data], safe=False)
Advantages of Using Pydantic with Django:
- Strict Data Validation: Pydantic provides a high level of control over incoming data, reducing errors and increasing application reliability.
- Convenience and Clarity: Pydantic models simplify defining and using data structures, improving code readability and maintainability.
- Reusability: You can reuse Pydantic models across different parts of your application (views, forms, serializers), reducing duplication and simplifying updates.