Back to Blog
Python

Using Computed Fields with Generic Models in Pydantic

python pydantic computed_field and generic models: Learn how to combine Pydantic's computed_field with generic models, handle type parameters, avoid common pitfalls, a...

Pydanticcomputed_fieldgeneric modelstype hintsserialization
A generic Pydantic model with a computed field, showing a type parameter T and a derived value appearing in serialized JSON output.

In Pydantic v2, computed_field lets you expose derived values as part of a model's serialized output without storing them as attributes. When you combine it with generic models, you can compute properties that depend on type parameters while keeping the serialized schema clean. This article explains how python pydantic computed_field and generic models work together, where they break, and how to use them safely.

What computed_field Does in Pydantic v2

computed_field is a decorator that marks a method as a computed property. The method's return value is included when the model is serialized via model_dump() or model_dump_json(), but it is not part of the model's fields and does not participate in validation. This is useful for values that can be derived from existing fields, such as a full name from first and last name, or a total price from item prices.

from pydantic import BaseModel, computed_field class Order(BaseModel): item_count: int unit_price: float @computed_field @property def total_price(self) -> float: return self.item_count * self.unit_price

When you serialize an Order instance, total_price appears in the output even though it was never passed to the constructor. The method is evaluated at serialization time, so it always reflects the current state of the model.

Generic Models and Type Parameterization

Generic models in Pydantic allow you to define a model that works with multiple types. You use TypeVar to declare a placeholder and then parameterize the model when you instantiate it.

from typing import Generic, TypeVar from pydantic import BaseModel T = TypeVar('T') class Wrapper(BaseModel, Generic[T]): value: T

Here, Wrapper[int] expects an integer, while Wrapper[str] expects a string. Pydantic validates the value field against the concrete type at runtime, and the model's schema reflects the specific type parameter.

Combining computed_field with Generic Models

You can place a computed_field method inside a generic model just like in a regular model. The method can reference the type parameter T in its return type or in its logic.

from typing import Generic, TypeVar from pydantic import BaseModel, computed_field T = TypeVar('T') class Container(BaseModel, Generic[T]): item: T quantity: int @computed_field @property def total(self) -> int: # Assume T supports multiplication with int return self.item * self.quantity

This works as long as the type parameter T actually supports the operations used inside the computed method. If you instantiate Container[str] and call model_dump(), Pydantic will attempt to multiply a string by an integer, which may or may not be what you want. The decorator does not enforce type constraints; it simply evaluates the method.

How Pydantic Handles Computed Fields During Serialization

When you call model_dump() or model_dump_json(), Pydantic collects all fields and then evaluates every computed_field method. The result is merged into the output dictionary. For generic models, the computed field's return type is not validated against the type parameter; it is used as-is in the serialized output.

c = Container[int](item=3, quantity=4) print(c.model_dump()) # {'item': 3, 'quantity': 4, 'total': 12}

The computed field appears after the regular fields. The order is deterministic and follows the order in which methods are defined in the class body.

Type Hints and Generic Parameters in Computed Fields

You can use the type variable T in the return type of a computed field. This is useful when the computed value should have the same type as the generic parameter.

from typing import Generic, TypeVar from pydantic import BaseModel, computed_field T = TypeVar('T') class Identity(BaseModel, Generic[T]): value: T @computed_field @property def echoed(self) -> T: return self.value

Pydantic does not validate the return type against T at runtime. The annotation is primarily for static type checkers. If you use a tool like mypy or Pyright, the annotation gives you useful information, but at runtime Pydantic simply includes whatever the method returns.

Be cautious when the computed field depends on the specific type parameter. For example, if you want to compute a hash of the value, you might need to call a method that only exists on certain types. In that case, you should either constrain T with a protocol or handle the operation conditionally inside the method.

Common Pitfalls and Edge Cases

Forward References and from __future__ import annotations

If you use from __future__ import annotations, all annotations become strings. Pydantic v2 resolves them lazily, but computed_field methods are not validated for type correctness, so this usually does not cause issues. However, if you use a type variable in the return type, make sure it is defined before the class body.

Inheritance and Overriding

If you inherit from a generic model that has a computed field, you can override the method. The subclass's version will be used during serialization. This is useful when you want to change the computed logic for a specific type parameter.

class SpecialContainer(Container[str]): @computed_field @property def total(self) -> str: return self.item * self.quantity

Here, SpecialContainer overrides the computed field to return a string. Pydantic will use the override because it resolves methods dynamically.

Computed Fields and Model Serializers

If you define a custom model_serializer, it takes precedence over individual computed fields. In that case, you must manually include the computed values in your serializer. The computed_field decorator does not automatically apply when a custom serializer is present.

from pydantic import BaseModel, computed_field, model_serializer class Custom(BaseModel): x: int @computed_field @property def double(self) -> int: return self.x * 2 @model_serializer def serialize(self) -> dict: return {'x': self.x, 'double': self.double}

In this example, the custom serializer explicitly includes double. If you omit it, the computed field will not appear in the output.

Performance and Maintainability Considerations

Every computed field is evaluated each time the model is serialized. If the computation is expensive, such as a database lookup or a complex calculation, serialization latency will increase. For read-heavy APIs, this can become a bottleneck. Consider caching the result if the underlying fields do not change frequently, but remember that Pydantic models are mutable by default, so cached values may become stale.

From a maintainability perspective, computed fields keep derived logic close to the data they depend on. This is often better than scattering the same calculation across multiple serializers or view functions. However, if the computation is only needed for a specific API endpoint, a plain method or a function may be more appropriate to avoid coupling the model to serialization concerns.

When to Use Computed Fields vs Regular Properties

A regular @property is available on the instance but is not included in model_dump() or JSON output unless you manually add it. A computed_field is specifically designed for serialization. Use computed_field when you want the derived value to appear in the API response or when you need to control the field's inclusion in the schema. Use a regular property when you only need the value in Python code and do not want it in the serialized representation.

For generic models, the choice is the same. If the derived value is part of the public API contract, use computed_field. If it is an internal convenience, a property is sufficient.

A Practical Example: Generic Pagination Metadata

Consider a generic paginated response that includes a computed field for the total number of pages based on a generic item type.

from typing import Generic, TypeVar from pydantic import BaseModel, computed_field T = TypeVar('T') class Page(BaseModel, Generic[T]): items: list[T] page_size: int total_items: int @computed_field @property def total_pages(self) -> int: if self.page_size == 0: return 0 return (self.total_items + self.page_size - 1) // self.page_size

Here, total_pages is computed from total_items and page_size. The generic type T does not affect the computation, but it allows the same Page model to be reused for different item types. When you serialize Page[User] or Page[Product], the total_pages field appears consistently without duplicating serialization logic.

This pattern is common in REST APIs where pagination metadata is part of the response envelope. Using a computed field keeps the calculation in one place and ensures every response includes the same derived value.

Handling Type-Dependent Computations Safely

If your computed field must perform operations that are only valid for certain types, you have two main options. First, constrain the type variable with a protocol or an upper bound. Second, check the runtime type inside the method and raise a clear error if the operation is not supported.

from typing import Generic, TypeVar, Protocol from pydantic import BaseModel, computed_field class SupportsLen(Protocol): def __len__(self) -> int: ... T = TypeVar('T', bound=SupportsLen) class SizedWrapper(BaseModel, Generic[T]): value: T @computed_field @property def length(self) -> int: return len(self.value)

Here, the type variable is bound to SupportsLen, so only types with a __len__ method can be used. This prevents accidental misuse at static analysis time, though Pydantic does not enforce the bound during instantiation. If you pass a type that does not satisfy the protocol, the computed field will raise an error at serialization time. To fail earlier, you could add a field validator, but that would change the model's validation behavior.

Compatibility with Pydantic v1

computed_field is a v2 feature. If you are still on Pydantic v1, you can achieve a similar effect by overriding dict() or using a custom serializer, but the syntax is different. The approach described here relies on v2-specific decorators and serialization behavior. If you need to support both versions, consider using a compatibility layer or upgrading to v2, as the v1 method is more verbose and less integrated with the model schema.

When migrating from v1, note that computed fields are not included in model_dump() by default in v1; you had to manually add them. In v2, the decorator handles this automatically, which simplifies the code but also changes the serialized output. Review your API responses after upgrading to ensure no computed values are unexpectedly missing or added.

python pydantic computed_field and generic models: Practical | RYUSLOG DEV