FastAPI Model Methods
Pydantic models in FastAPI aren't just simple data containers - they can include powerful methods that help you validate, transform, and manipulate your data. In this tutorial, we'll explore how to leverage model methods to enhance your FastAPI applications.
Introduction to Model Methods
Pydantic models, which serve as the backbone for data validation in FastAPI, can be extended with custom methods like any Python class. These methods allow you to add business logic directly to your data models, making your code more organized and maintainable.
Let's start by understanding what model methods are and why they're useful in FastAPI applications.
Basic Model Methods
At the most fundamental level, you can add regular instance methods to your Pydantic models:
from pydantic import BaseModel
from datetime import datetime
class User(BaseModel):
id: int
name: str
signup_date: datetime
def is_new_user(self) -> bool:
"""Check if the user signed up less than 30 days ago"""
time_since_signup = datetime.now() - self.signup_date
return time_since_signup.days < 30
You can use this method in your FastAPI route handlers:
from fastapi import FastAPI
from datetime import datetime, timedelta
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int):
# In a real app, you'd fetch this from a database
user = User(
id=user_id,
name="John Doe",
signup_date=datetime.now() - timedelta(days=15)
)
return {
"user": user,
"is_new": user.is_new_user()
}
When you call this endpoint with a user ID, it will return both the user details and whether they're considered a new user.
Property Methods
You can also use Python's @property
decorator to create computed properties:
from pydantic import BaseModel
from datetime import datetime
class Product(BaseModel):
name: str
price: float
discount_percentage: float = 0
@property
def discounted_price(self) -> float:
"""Calculate the price after discount"""
return self.price * (1 - self.discount_percentage / 100)
This creates a read-only property that calculates the discounted price:
from fastapi import FastAPI
app = FastAPI()
@app.get("/products/{product_id}")
def get_product(product_id: int):
# Mock product data
product = Product(
name="Laptop",
price=1000,
discount_percentage=10
)
return {
"product": product,
"discounted_price": product.discounted_price # Accessing as a property
}
The endpoint will return the product details with the calculated discounted price.
Validation Methods with Validators
One of the most powerful features of Pydantic is the ability to add custom validators. These are methods decorated with @validator
that allow you to perform complex validation or transformation on fields:
from pydantic import BaseModel, validator
class RegisterUser(BaseModel):
username: str
password: str
password_confirm: str
@validator('username')
def username_must_be_valid(cls, v):
if len(v) < 3:
raise ValueError('username must be at least 3 characters')
if not v.isalnum():
raise ValueError('username must contain only alphanumeric characters')
return v
@validator('password_confirm')
def passwords_match(cls, v, values, **kwargs):
if 'password' in values and v != values['password']:
raise ValueError('passwords do not match')
return v
Now when you use this model in a FastAPI route:
from fastapi import FastAPI
app = FastAPI()
@app.post("/register/")
def register_user(user: RegisterUser):
# If we get here, validation passed!
return {"message": "User registered successfully"}
If a client sends invalid data, they'll receive detailed error messages:
// Example request with invalid data
{
"username": "a!",
"password": "secret123",
"password_confirm": "different"
}
// Example response
{
"detail": [
{
"loc": ["body", "username"],
"msg": "username must be at least 3 characters",
"type": "value_error"
},
{
"loc": ["body", "password_confirm"],
"msg": "passwords do not match",
"type": "value_error"
}
]
}
Root Validators for Cross-Field Validation
When you need to validate multiple fields together, use the @root_validator
decorator:
from pydantic import BaseModel, root_validator
from datetime import date
class DateRange(BaseModel):
start_date: date
end_date: date
@root_validator
def check_dates_order(cls, values):
start = values.get('start_date')
end = values.get('end_date')
if start and end and start > end:
raise ValueError('end_date must be after start_date')
return values
This ensures that the end_date
is always after the start_date
:
from fastapi import FastAPI
from datetime import date
app = FastAPI()
@app.post("/bookings/")
def create_booking(date_range: DateRange):
# If we got here, validation passed
return {"message": "Booking created", "date_range": date_range}
Model Configuration Methods
Pydantic models also support special configuration methods. For instance, you can customize how a model is converted to a dictionary:
from pydantic import BaseModel
from datetime import datetime
from typing import Dict, Any
class AuditLog(BaseModel):
user_id: int
action: str
timestamp: datetime
details: Dict[str, Any]
def dict(self, *args, **kwargs):
# Get the default dict representation
data = super().dict(*args, **kwargs)
# Add a formatted timestamp
data['formatted_timestamp'] = self.timestamp.strftime('%Y-%m-%d %H:%M:%S')
return data
When you return this model from a FastAPI endpoint, the customized dictionary will be used:
from fastapi import FastAPI
from datetime import datetime
app = FastAPI()
@app.get("/audit-logs/{log_id}")
def get_audit_log(log_id: int):
# In a real app, you'd fetch this from a database
log = AuditLog(
user_id=123,
action="login",
timestamp=datetime.now(),
details={"ip": "192.168.1.1", "device": "mobile"}
)
return log # FastAPI will call dict() automatically
Practical Example: User Profile with Validation and Helper Methods
Let's put everything together in a more comprehensive example:
from pydantic import BaseModel, validator, root_validator
from fastapi import FastAPI
from typing import List, Optional
from datetime import date, datetime, timedelta
class UserProfile(BaseModel):
user_id: int
email: str
full_name: str
birth_date: date
interests: List[str] = []
premium_member: bool = False
last_login: Optional[datetime] = None
@validator('email')
def email_must_be_valid(cls, v):
# Simple validation for demonstration purposes
if '@' not in v:
raise ValueError('invalid email format')
return v.lower() # Normalize email to lowercase
@validator('birth_date')
def check_birth_date(cls, v):
today = date.today()
age = today.year - v.year - ((today.month, today.day) < (v.month, v.day))
if age < 13:
raise ValueError('user must be at least 13 years old')
return v
@root_validator
def check_premium_features(cls, values):
premium = values.get('premium_member')
interests = values.get('interests')
if premium and not interests:
raise ValueError('premium members should have at least one interest')
return values
@property
def age(self) -> int:
today = date.today()
return today.year - self.birth_date.year - ((today.month, today.day) <
(self.birth_date.month, self.birth_date.day))
def is_active(self) -> bool:
"""Check if user has logged in within the last 30 days"""
if not self.last_login:
return False
return (datetime.now() - self.last_login) < timedelta(days=30)
def add_interest(self, interest: str) -> None:
"""Add a new interest if it's not already in the list"""
if interest not in self.interests:
self.interests.append(interest)
# Set up FastAPI application
app = FastAPI()
# Sample database (in-memory for demonstration)
users_db = {
1: UserProfile(
user_id=1,
email="[email protected]",
full_name="John Doe",
birth_date=date(1990, 1, 15),
interests=["coding", "hiking"],
premium_member=True,
last_login=datetime.now() - timedelta(days=5)
)
}
@app.get("/users/{user_id}")
def get_user(user_id: int):
user = users_db.get(user_id)
if not user:
return {"error": "User not found"}
return {
"profile": user,
"age": user.age,
"is_active": user.is_active()
}
@app.post("/users/{user_id}/interests")
def add_interest(user_id: int, interest: str):
user = users_db.get(user_id)
if not user:
return {"error": "User not found"}
user.add_interest(interest)
return {"message": "Interest added", "profile": user}
This example demonstrates:
- Field validation with
@validator
for email and birth date - Cross-field validation with
@root_validator
for premium members - A computed property
age
that calculates the user's age - An instance method
is_active()
to check user activity - A method with side effects
add_interest()
that modifies the model
Summary
Pydantic model methods add powerful capabilities to your FastAPI applications:
- Instance Methods: Add custom behavior to your models
- Property Methods: Create computed properties based on model fields
- Validators: Add complex validation logic to individual fields
- Root Validators: Implement validation across multiple fields
- Configuration Methods: Customize how models are processed and serialized
By incorporating these methods into your Pydantic models, you can create more robust, maintainable, and feature-rich FastAPI applications, keeping your business logic close to your data definitions.
Additional Resources
Exercises
- Create a
Product
model with methods to calculate tax based on different regions. - Implement an
Order
model with a validator to ensure order items are unique. - Build a
BlogPost
model with methods to generate slugs and extract reading time. - Design a
PaymentCard
model with validations for card numbers and expiry dates.
By practicing these exercises, you'll gain confidence in implementing model methods in your own FastAPI applications.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)