Hello again! Welcome back to our exploration of Pydantic.
In our last lesson, we established that Pydantic's BaseModel is the direct counterpart to DRF's Serializer, used for defining the shape of our data. We learned how FastAPI automatically validates incoming request bodies against these models. However, our validation was limited to basic type checking (e.g., ensuring a field is a str or an int).
This lesson takes the next logical step. We'll learn how to enforce more specific, fine-grained rules, much like you would use arguments like max_length or validators in a DRF Serializer field.
Learning Outcome: Apply fine-grained validation using Pydantic field types, constraints, and the Field() function.
1. Adding Constraints with Field() and Annotated
In DRF, you'd add constraints directly as arguments to your serializer fields, like this:name = serializers.CharField(max_length=50, min_length=3)
Pydantic has a very similar mechanism using the Field() function. In Pydantic v2, the modern and recommended way to apply these constraints is by using typing.Annotated. This approach keeps your type hints clean while "annotating" them with extra metadata that Pydantic uses for validation.
Let's see how to apply constraints for string length, numeric ranges, and even regular expression patterns.
Python Pydantic Tutorial: Complete Data Validation Course (Used by FastAPI)
The following segment from Corey Schafer's Pydantic tutorial provides an excellent demonstration of the modern way to add constraints. Pay close attention to the Annotated[<type>, Field(...)] syntax.
Watch from 34:00 to 40:43. Focus on how Field is used to define: String constraints (min_length, max_length). Numeric constraints (gt, ge, lt, le). A regular expression pattern for a URL slug.
As you saw, the pattern is straightforward. You wrap your original type with Annotated and pass a Field function with your desired constraints as the second argument.
Let's solidify this with a direct comparison to the DRF concepts you already know.
Here is a Pydantic model with several constraints:
from typing import Annotated
from pydantic import BaseModel, Field
class ItemCreate(BaseModel):
name: Annotated[str, Field(min_length=3, max_length=50)]
price: Annotated[float, Field(gt=0, description="The price must be a positive value.")]
stock: Annotated[int, Field(ge=0)] # Must be greater than or equal to 0
sku: Annotated[str, Field(pattern=r'^[A-Z]{3}-\d{4}$')] # e.g., 'ABC-1234'
And here's how those Pydantic constraints map to their DRF equivalents:
Pydantic Constraint (Field(...)) |
Django Rest Framework Equivalent | Analogy |
|---|---|---|
max_length=50 |
max_length=50 in CharField |
Direct equivalent for maximum string length. |
min_length=3 |
min_length=3 in CharField |
Direct equivalent for minimum string length. |
gt=0 (greater than) |
validators=[MinValueValidator(epsilon)] |
For numbers, gt and lt check for exclusivity. |
ge=0 (greater/equal) |
validators=[MinValueValidator(0)] |
For numbers, ge and le check for inclusivity. |
pattern=r'...' |
validators=[RegexValidator(r'...')] |
Both use regular expressions for complex string validation. |
This powerful combination of Annotated and Field allows you to define most of your business logic for data validation declaratively, right in the schema.
2. Specialized Built-in Types
While Field() is flexible, Pydantic also provides convenient, pre-packaged types for very common validation scenarios. Using these can make your code even more readable and expressive. Think of these as shortcuts that bundle common constraints.
The most useful ones include:
EmailStr: Validates that a string is a properly formatted email address.HttpUrl: Validates that a string is a valid HTTP or HTTPS URL.SecretStr: Used for sensitive data like passwords. It automatically redacts the value when the model is printed or logged, preventing accidental exposure.
Let's see them in practice.
Python Pydantic Tutorial: Complete Data Validation Course (Used by FastAPI)
Let's continue with the same tutorial, as it flows directly into covering these specialized types. Note how EmailStr replaces a complex regex and how SecretStr works.
Watch from 40:43 to 49:04. Pay attention to: How to import and use types like EmailStr, HttpUrl, and SecretStr. The mention that some types, like EmailStr, require an extra dependency (pip install pydantic[email]). The behavior of SecretStr when printing the model vs. accessing the value with .get_secret_value().
These specialized types are another area where you can draw a direct line from your DRF experience.
- Pydantic's
EmailStris the direct equivalent of DRF'sserializers.EmailField. - Pydantic's
HttpUrlis the direct equivalent of DRF'sserializers.URLField. - Pydantic's
SecretStrprovides similar protection to usingwrite_only=Trueon a DRF serializer field, but with the added benefit of still being able to access the value in your application logic when needed.
Let's update our user model to use these more expressive types.
from pydantic import BaseModel, EmailStr, HttpUrl, SecretStr
from typing import Optional
class UserProfileUpdate(BaseModel):
email: Optional[EmailStr] = None
website: Optional[HttpUrl] = None
password: Optional[SecretStr] = None # Will be hidden in logs
3. Putting It All Together
By combining standard types, the Annotated and Field pattern, and specialized Pydantic types, you can build very robust and descriptive validation schemas.
Here's a comprehensive example for creating a new user:
from pydantic import BaseModel, Field, EmailStr
from typing import Annotated
class UserCreate(BaseModel):
username: Annotated[
str,
Field(
min_length=3,
max_length=20,
pattern=r"^[a-zA-Z0-9_]+$",
description="Username can only contain letters, numbers, and underscores."
)
]
email: EmailStr
age: Annotated[int, Field(ge=18, le=120, description="User must be between 18 and 120 years old.")]
password: Annotated[str, Field(min_length=8)]
This single class clearly and concisely defines a complex set of rules that FastAPI will automatically enforce on any incoming request to an endpoint that uses it. If any rule is violated, FastAPI will return a detailed 422 Unprocessable Entity error pointing to the exact problem.
For a final reference, the official Pydantic documentation provides a concise summary of these concepts.
The official documentation is an excellent resource for a quick, text-based review of what we've covered. It formally defines the Annotated pattern and Field constraints.
Read the sections 'The annotated pattern' and 'Field constraints'. This will reinforce the syntax and concepts from the video.
Conclusion
You've now moved beyond basic type validation and can enforce precise business logic within your Pydantic models.
Key Takeaways:
Field()is the primary tool for adding constraints like length, numeric ranges, and patterns to your model's fields.Annotatedis the modern Pydantic v2 syntax for applyingField()constraints, keeping type hints clean and readable.- The constraints provided by
Field()have direct analogies in DRF, such asmax_length,MinValueValidator, andRegexValidator. - Pydantic offers specialized types like
EmailStrandHttpUrlas convenient, readable shortcuts for common validation patterns, similar toEmailFieldandURLFieldin DRF.
So far, we've dealt with flat data structures. But real-world APIs often involve nested objects—for example, a blog post that contains a user object for the author.
In our next lesson, we will tackle this by learning how to create nested Pydantic models to represent complex, hierarchical JSON structures, a concept analogous to nested serializers in DRF.