Python

How to Master Pydantic Validators

Becoming a pro at Pydantic Validators is like becoming good at using a unique tool that checks if our data is correct. Pydantic is like a helper in Python that is famous for ensuring that the data looks right and is accurate. When we learn about Pydantic Validators, we know how to use this helper tool really well. This means that we understand how to set the rules for what data should look like and catch mistakes. To get really good at this, we must know about the different types of data and how to make our own rules for checking it. Once we are masters at Pydantic Validators, we can make our programs more trustworthy and solid.

Method 1: Mastering Pydantic Validators for the Real World Applications

Imagine that we are building a web application for a restaurant where the customers can place orders online. To ensure the orders’ accuracy and prevent mishaps, we decide to use the Pydantic Validators to validate the incoming data.

Whenever we start with the example that requires the Pydantic functions, we need to install its packages first to bring the library with all its packages in the Python dependencies. To bring the necessary dependencies into the project, we use the following commands:

!pip install pydantic

from typing import List

from pydantic import BaseModel, conlist, field_validator

Now that we already installed the required packages, let’s do the first step to implement the example which is to define a data model using Pydantic’s BaseModel. Since we are working on the online restaurant order application, in our case, the model has the attribute as per the requirement of the delivery app. Each order has the customer’s name, the items they want to order, and any special notes. We start by creating a Python class that inherits from Pydantic’s BaseModel and specify the fields we need:

In this model, we now define the data types for each of these attributes where the customer_name is a required string, the items is a list of strings (at least one item required), and the special_notes is an optional string.

class Order(BaseModel):

  customer_name: str

  items: List[str]

  special_notes: str = None

Conventionally, after defining the model, we create the instance of that model for its validation and then access its object attributes. Still, sometimes, we need to apply some conditions on the data model attributes before creating its object instance. This type of check is known as custom validation. For custom validation, suppose we want to make sure that the customer’s name is at least three characters long and that each item in the order is not empty. We can use Pydantic’s validators for this.

@field_validator('customer_name')

def validate_customer_name(cls, value):

  if len(value) < 3:

    raise ValueError("Customer name must be at least 3 characters long")

  return value

These validators ensure that the customer’s name is long enough and that each item has a meaningful name.

After applying the custom validations, using the validators with the data model and validators in place, we can now use them to validate the incoming orders in our web application.

order_data = {

  "customer_name": "John",

  "items": ["Burger", "Fries"],

  "special_notes": "No onions"

}

try:

  order = Order(**order_data)

  print("Order is valid:", order)

except ValueError as e:

  print("Invalid order:", str(e))

When we create an “Order” instance using the provided data, Pydantic automatically runs the validators that we defined. If any of the validators raise a ValueError, the data is invalid according to our rules.

Here’s the code for the example that we mentioned previously with its output:

!pip install pydantic

from typing import List

from pydantic import BaseModel, conlist, field_validator

class Order(BaseModel):

  customer_name: str

  items: List[str]

  special_notes: str = None

  @field_validator('customer_name')

  def validate_customer_name(cls, value):

    if len(value) < 3:

        raise ValueError("Customer name must be at least 3 characters long")

    return value

  @field_validator('items')

  def validate_item(cls, value):

    if not value:

        raise ValueError("Item names cannot be empty")

    return value

  order_data = {

    "customer_name": "John",

    "items": ["Burger", "Fries"],

    "special_notes": "No onions"

}

try:

    order = Order(**order_data)

    print("Order is valid:", order)

except ValueError as e:

    print("Invalid order:", str(e))

By mastering the Pydantic Validators in this example, we ensured that the orders placed through our restaurant’s online platform are accurate and gained valuable skills in data validation that can be applied in various contexts. Whether we are working on web applications, APIs, or any other software that deals with data, Pydantic Validators can be a powerful tool in our developer toolkit.

Method 2: Advanced Validation Options in Pydantic

As we become more comfortable with Pydantic Validators, we can explore the advanced features like pre and post-processing functions, working with complex data structures, and utilizing Pydantic’s built-in validators.

For instance, we could add a validator that checks if the order total does not exceed a certain amount or create nested models to handle more complex structures like shipping addresses.

For the Total Order Cost Validator:

In the previously mentioned example, we want to ensure that the total cost of the order doesn’t exceed a certain limit. To achieve this, we can use a validator to check the sum of the costs of each item.

class Order(BaseModel):

  customer_name: str

  items: List[str]

  special_notes: str = None

  @field_validator('items')

  def validate_order_cost(cls, value, values):

    item_prices = {"Burger": 5, "Fries": 2, "Soda": 1} # Sample item prices

    total_cost = sum(item_prices[item] for item in value)

    if total_cost > 20:

    raise ValueError("Order total cannot exceed $20")

  return value

This validator calculates the total cost of the order based on predefined item prices and raises an error if it exceeds the limit.

Regarding the previous example, the nested models for addresses, suppose we want to handle different delivery addresses; we can create a nested model for addresses and validate its components.

class Address(BaseModel):

  street: str

  city: str

  state: str

  zip_code: str

class Order(BaseModel):

  customer_name: str

  items: conlist(str, min_items=1)

  special_notes: str = None

  delivery_address: Address

By including the delivery_address as an instance of the “Address” model, we ensure that the address data is complete and follows the required structure.

For the advanced pre-processing, Pydantic allows us to perform the advanced pre-processing using the class methods. For instance, we can automatically capitalize the customer’s name and format the special notes.

class Order(BaseModel):

customer_name: str

  items: List[str]

  special_notes: str = None

  @classmethod

  def pre_process(cls, data):

    data['customer_name'] = data['customer_name'].capitalize()

    if 'special_notes' in data:

    data['special_notes'] = data['special_notes'].strip()

    return data

This pre_process method modifies the data before validation, ensuring consistency and improved user experience.

Here’s the full code with the snippet of the output for the observation. This code can be copied to any Python compiler to see the result:

!pip install pydantic

from typing import List

from pydantic import BaseModel, conlist, field_validator

# Nested model for delivery address

class Address(BaseModel):

  street: str

  city: str

  state: str

  zip_code: str

# Order model with advanced validation and preprocessing

class Order(BaseModel):

  customer_name: str

  items: list[str]

  special_notes: str = None

  delivery_address: Address

  @field_validator('items')

  def validate_order_cost(cls, value, values):

    item_prices = {"Burger": 5, "Fries": 2, "Soda": 1}

    total_cost = sum(item_prices[item] for item in value)

    if total_cost > 20:

        raise ValueError("Order total cannot exceed $20")

    return value

    @classmethod

    def pre_process(cls, data):

        data['customer_name'] = data['customer_name'].capitalize()

        if 'special_notes' in data:

            data['special_notes'] = data['special_notes'].strip()

        return data

  order_data = {

    "customer_name": "alien",

    "items": ["Burger", "Fries", "Soda"],

    "special_notes": "   No onions ",

    "delivery_address": {

    "street": "123 Main St",

    "city": "pwd",

    "state": "CA",

    "zip_code": "12345"

    }

  }

    # Process order

    try:

        order_data = Order.pre_process(order_data)

        order = Order(**order_data)

        print("Order is valid:", order)

      except ValueError as e:

        print("Invalid order:", str(e))

Mastering the Pydantic Validators beyond the basics helps us to confidently tackle the complex validation scenarios. The library can handle complex structures, integrate advanced preprocessing, and implement customized validators, allowing us to create powerful applications that maintain data integrity and user satisfaction. As we experimented with nested models, more detailed validation rules, and advanced data validation options, we improved our expertise in utilizing Pydantic to its fullest potential. These skills enhance our current project and make us work with a valuable skillset applicable to various development domains.

Conclusion

Becoming skilled with Pydantic Validators helps us to make sure our data is correct and trustworthy. Just like in the restaurant example, we can use Pydantic to set the rules for how the data should look and catch mistakes. We can create better programs by learning about models, custom rules, and advanced features. So, whether we are dealing with online orders or any data, Pydantic makes sure that things work right and the users are satisfied.

About the author

Omar Farooq

Hello Readers, I am Omar and I have been writing technical articles from last decade. You can check out my writing pieces.