#     For further information visit https://errors.pydantic.dev/2.8/v/greater_than
```

In this example, we made the following changes:

1. We defined a `DuckDescription` Pydantic model to represent the expected structure and types of our duck data.
2. We used the `@validate_call` decorator on our `describe_duck` function. This decorator automatically validates the function's arguments based on the type annotations.
3. The function now expects a `DuckDescription` object instead of separate parameters. This ensures that all the data is validated as a unit before the function is called.
4. We simplified the function body since we can now be confident that the data is valid and of the correct type.

By using Pydantic's `@validate_call` decorator, we made our function safer and easier to read.

## Comparing Python Typing Methods

The table below summarizes the key differences between the Python typing methods we discussed. Keep in mind that some points may have exceptions or nuances depending on your specific use case. The table is meant to provide a general overview only.

<Table
  title="Comparing Python Typing Methods"
  columns={[
    { key: "feature", header: "Feature" },
    { key: "type_annotations", header: "Type Annotations" },
    { key: "data_classes", header: "Data Classes" },
    { key: "typed_dict", header: "TypedDict" },
    { key: "pydantic", header: "Pydantic" }
  ]}
  data={[
    { feature: "Static type checking", type_annotations: "✅", data_classes: "✅", typed_dict: "✅", pydantic: "✅" },
    { feature: "Runtime type checking", type_annotations: "❌", data_classes: "❌", typed_dict: "❌", pydantic: "✅" },
    { feature: "Automatic data validation", type_annotations: "❌", data_classes: "❌", typed_dict: "❌", pydantic: "✅" },
    { feature: "JSON serialization", type_annotations: "❌", data_classes: "❌", typed_dict: "❌", pydantic: "✅" },
    { feature: "Nested object support", type_annotations: "✅", data_classes: "✅", typed_dict: "✅", pydantic: "✅" },
    { feature: "Custom validation rules", type_annotations: "❌", data_classes: "❌", typed_dict: "❌", pydantic: "✅" },
    { feature: "IDE autocomplete support", type_annotations: "✅", data_classes: "✅", typed_dict: "✅", pydantic: "✅" },
    { feature: "Performance overhead", type_annotations: "None", data_classes: "Minimal", typed_dict: "None", pydantic: "Minimal" },
    { feature: "Compatibility with dicts", type_annotations: "❌", data_classes: "❌", typed_dict: "✅", pydantic: "✅" },
    { feature: "Standard Library", type_annotations: "✅", data_classes: "✅", typed_dict: "✅", pydantic: "❌" }
  ]}
  rowKeyField="feature"
/>

## Why Speakeasy Chose Pydantic

At Speakeasy, we chose Pydantic as the primary tool for data validation and serialization in the Python SDKs we create.

After our initial Python release, support for Pydantic was one of the most requested features from our users. Pydantic provides a great balance between flexibility and type safety. And because Pydantic uses Rust under the hood, it has a negligible performance overhead compared to other third-party data validation libraries.

SDKs are an ideal use case for Pydantic, providing automatic data validation and serialization for the data structures that API users interact with.

By working with the Pydantic team, we've contributed to the development of features that make Pydantic even better suited for SDK development.

## The Value of Runtime Type Safety

To illustrate the value of runtime type safety, consider a scenario where we are building an API that receives JSON data from a client to represent an order from a shop. Let's use a `TypedDict` to define the shape of the order data:

```python
from typing import TypedDict


class Order(TypedDict):
    customer_name: str
    quantity: int
    unit_price: float


def calculate_order_total(order: Order) -> float:
    return order["quantity"] * order["unit_price"]


print(
    calculate_order_total(
        {
            "customer_name": "Alex",
            "quantity": 10,
            "unit_price": 5,
        }
    )
)  # Output: 50
```

In this example, we define an `Order` `TypedDict` with three keys: `customer_name`, `quantity`, and `unit_price`. We then create an `order_data` dictionary with values for these keys and pass it to the `calculate_order_total` function.

The `calculate_order_total` function multiplies the `quantity` and `unit_price` values from the `order` dictionary to calculate the total order amount. It works fine when the `order_data` dictionary has the correct types of values, but what if the client sends us invalid data?

```python
print(
    calculate_order_total(
        {
            "customer_name": "Sam",
            "quantity": 10,
            "unit_price": "5",
        }
    )
)  # Output: 5555555555
```

In this case, the client sends us a string value for the `unit_price` key instead of a float. Since Python is a duck-typed language, the code will still run without errors, but the result will be incorrect. This is a common source of bugs in Python code, especially when working with JSON data from external sources.

Now, let's see how we can use Pydantic to define a data model for the order data and enforce type safety at runtime:

```python
from pydantic import BaseModel, computed_field


class Order(BaseModel):
    customer_name: str
    quantity: int
    unit_price: float

    @computed_field
    def calculate_total(self) -> float:
        return self.quantity * self.unit_price


order = Order(
    customer_name="Sam",
    quantity=10,
    unit_price="5",
)
print(order.calculate_total)  # Output: 50.0
```

In this case, Pydantic converts the string `"5"` to a float value of `5.0` for the `unit_price` field. The automatic type coercion prevents errors and ensures the data is in the correct format.

Pydantic enforces type safety at runtime, but don't we lose the simplicity of passing dictionaries around?

But we don't have to give up on dictionaries.

## Using Typed Dictionaries With Pydantic Models

In some cases, you may want to accept both `TypedDict` and Pydantic models as input to your functions. You can achieve this by using a union type in your function signature:

```python
from typing import TypedDict

from pydantic import BaseModel


class OrderTypedDict(TypedDict):
    customer_name: str
    quantity: int
    unit_price: float


class Order(BaseModel):
    customer_name: str
    quantity: int
    unit_price: float


def calculate_order_total(order: Order | OrderTypedDict) -> float:
    if not isinstance(order, BaseModel):
        order = Order(**order)
    return order.quantity * order.unit_price


print(
    calculate_order_total(
        {
            "customer_name": "Sam",
            "quantity": 10,
            "unit_price": "5",
        }
    )
)  # Output: 50.0
```

In this example, we define an `OrderTypedDict` `TypedDict` and an `Order` Pydantic model for the order data. We then define a `calculate_order_total` function to accept a union type of `Order` and `OrderTypedDict`.

If the input is a `TypedDict`, it'll be converted to a Pydantic model before performing the calculation. Now our function can accept both `TypedDict` and Pydantic models as input, providing us flexibility while still enforcing type safety at runtime.

Speakeasy SDKs employ this pattern so users can pass in either dictionaries or Pydantic models to the SDK functions, reducing the friction of using the SDK while maintaining type safety.

## Conclusion

To learn more about how we use Pydantic in our SDKs, see our post about [Python Generation with Async & Pydantic Support](/post/release-python-v2-alpha).