Best Practices for Pydantic Model Naming, Management, and Usage in Python Applications

The Primadonna
5 min readMar 4, 2023

--

Photo by Alexandra Muñoz on Unsplash

Introduction

Pydantic is a popular library for data validation and settings management in Python, widely used in the FastAPI framework. When using Pydantic models for CRUD operations or internal data management, it’s important to follow best practices for naming, managing, and using them. This can improve code readability and maintainability, as well as ensure the reliability and security of your application.

Best Practices Naming Pydantic Models for CRUD Operations

When defining Pydantic models for CRUD operations, it’s essential to choose names that accurately reflect the model’s role and responsibilities. Here are some common naming conventions that you can follow:

  • User: A Pydantic model representing general user data.
  • UserCreate: A Pydantic model used to create new user data.
  • UserUpdate: A Pydantic model used to update existing user data.
  • UserInDB: A Pydantic model representing user data retrieved from a database.

By following these naming conventions, you can help other developers understand the role and responsibilities of each Pydantic model in your codebase. This, in turn, can make your code more readable and maintainable.

Naming Pydantic Models for Internal Data Management

When defining Pydantic models for internal data management, it’s also essential to choose names that accurately reflect the model’s role and responsibilities. Here are some common naming conventions that you can follow:

  • Settings: A Pydantic model representing application settings data.
  • Credentials: A Pydantic model representing user authentication credentials.
  • TokenData: A Pydantic model representing user authentication token data.
  • RequestData: A Pydantic model representing data received in an HTTP request.
  • ResponseData: A Pydantic model representing data returned in an HTTP response.

By following these naming conventions, you can help other developers understand the role and responsibilities of each Pydantic model in your codebase, even when they’re not directly related to CRUD operations.

Managing Pydantic Models

To manage Pydantic models, it’s a good practice to define each model in a separate module within a package. This allows you to easily find and organize your models and validation code.

For example, you could have the following project structure:

my_project/
├── main.py
├── models/
│ ├── __init__.py
│ ├── user.py
│ └── item.py
└── ...

In this structure, the models directory is a module package that contains all the Pydantic model classes. Each model is defined in a separate module file.

# models/user.py
from pydantic import BaseModel

class User(BaseModel):
username: str
password: str
email: str
full_name: str
phone_number: str

# models/item.py
from pydantic import BaseModel

class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = 0.0

and you can import all the necessary models in the __init__.py file.

# models/__init__.py
from .user import User
from .item import Item

Using Validators in Pydantic Models

Pydantic models also allow you to define validators, which are functions that validate and transform field values. You can use validators to add additional checks and transformations to your models, even when they’re not related to CRUD operations.

For example, you can define a validator for the email field in a User model to ensure that the email address contains the "@" symbol:

from pydantic import BaseModel, validator
from typing import List

class User(BaseModel):
username: str
password: str
email: str
full_name: str
phone_number: str

@validator('email')
def email_must_contain_at(cls, v):
if '@' not in v:
raise ValueError('must contain "@"')
return v

@validator('phone_number')
def phone_number_must_contain_dash(cls, v):
if '-' not in v:
raise ValueError('must contain "-"')
return v

def create_user(user: User) -> dict:
user_dict = user.dict()
return user_dict

In this example, we define a User model and use validator functions to validate and, if necessary, transform the values of the email and phone_number fields.

For example, suppose we have the following user data:

user_data = {
"username": "john_doe",
"password": "password123",
"email": "john_doeexample.com",
"full_name": "John Doe",
"phone_number": "5555555555"
}

The values of the email and phone_number fields are invalid. When we try to create a User model using this data, Pydantic will raise a ValidationError.

To avoid this, we call the create_user function, which uses Pydantic's built-in validation to validate the data and transform the field values, if necessary.

user = User(**user_data)
created_user = create_user(user)

Now created_user contains a dictionary with only valid user data. This is an example of how to use validator functions to improve data validation and avoid unnecessary transformation.

Using Pydantic Models in Functions

Pydantic models can be used not only in CRUD operations but also in other functions that require input validation or output serialization. For example, you can define a function that takes a User model as input and returns a JSON-serializable dictionary:

from typing import Dict
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
username: str
password: str
email: str
full_name: str
phone_number: str

@app.post('/users/')
async def create_user(user: User) -> Dict:
return {'user': user.dict()}

In this example, the create_user function takes a User model as input and returns a dictionary containing the user data serialized as JSON. By using Pydantic models in your functions, you can ensure that your input and output data are always valid and consistent.

Pydantic models can also be used in regular functions. This is because Pydantic automatically handles data validation and type conversion, making the code more secure and concise.

For example, consider the following function:

from typing import List

def create_user(username: str, password: str, email: str, full_name: str, phone_number: str) -> dict:
user = {
"username": username,
"password": password,
"email": email,
"full_name": full_name,
"phone_number": phone_number
}
return user

This function takes user data and converts it to a dictionary. However, because the function doesn’t perform data validation, it can cause unexpected behavior if a user enters incorrect data.

To solve this problem, Pydantic models can be used. For example, the following model can be defined:

from pydantic import BaseModel

class User(BaseModel):
username: str
password: str
email: str
full_name: str
phone_number: str

The function can now be updated to use the model:

def create_user(user: User) -> dict:
user_dict = user.dict()
return user_dict

Because this function uses a Pydantic model to validate the user data, it is now safer. To call this function, the following code can be used:

user_data = {
"username": "john_doe",
"password": "password123",
"email": "john_doe@example.com",
"full_name": "John Doe",
"phone_number": "555-555-5555"
}

user = User(**user_data)
created_user = create_user(user)

Therefore, it’s recommended to use Pydantic models in regular functions as well. This is because it makes the code more secure and concise.

Performance Considerations

Using Pydantic models and validators can have a small impact on performance, but this is usually negligible in most applications. Pydantic uses a fast parsing and validation algorithm based on Pydantic’s own custom implementation of the Abstract Syntax Tree (AST) and relies on the built-in Python json module for serialization.

However, if you’re dealing with extremely large volumes of data or have strict performance requirements, you may need to optimize your code or use a different approach. For example, you can use more efficient serialization libraries like ujson or orjson, or use specialized data validation libraries like cerberus or voluptuous for faster validation.

Conclusion

Choosing the right names for Pydantic models is essential for code readability and maintainability. By following the naming conventions we’ve discussed and organizing your models within a module package, you can make your code more understandable to other developers and easier to maintain over time.

By using Pydantic models in your functions, you can ensure that your input and output data are always valid and consistent, improving the reliability and security of your application. However, you should also consider the potential performance impact of using Pydantic models and validators, especially in high-performance applications.

--

--

The Primadonna
The Primadonna

Written by The Primadonna

Experienced software engineer in Golang and Python. Sharing knowledge on software development in this blog.

Responses (1)