Using Computed Fields with Generic Models in Pydantic
Learn how to combine Pydantic v2's computed_field with generic models: type parameter handling, serialization behavior, inheritance, custom serializers, and safe patterns.
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 computed_field and generic models work together in Pydantic v2, where they can 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 Any, 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) -> Any: # Assumes T supports multiplication with int. return self.item * self.quantity
This works only when the type parameter T actually supports the operations used inside the computed method. Pydantic does not inspect the method body or validate the result against T; the method is evaluated when the model is serialized. 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.
How Pydantic Handles Computed Fields During Serialization
When you call model_dump() or model_dump_json(), Pydantic's serializer includes regular fields and then evaluates every computed_field method. In the dictionary produced by model_dump(), the result is merged with the regular fields. 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}
In this example, total appears after the regular fields. The exact ordering is controlled by Pydantic's serializer; avoid relying on it in external code.
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, annotations are stored as strings. Pydantic v2 resolves model field annotations when it builds the model. Computed-field return annotations are not used to validate values, so this usually does not cause additional problems. Still, define the type variable before the class body if you use it in a return annotation.
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 collects computed fields on the subclass, so the override replaces the parent method during serialization.
Computed Fields and Model Serializers
A custom model_serializer changes how the model is serialized. With a plain serializer that returns a dict, computed fields are not added automatically, so include them manually if you want them in the response.
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
With the default serializer, 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)
A Protocol bound is mainly useful for type checkers; Pydantic's runtime handling of protocol bounds can vary by version. It should not be treated as a runtime constraint. If you need to fail during model validation, add a field validator or an explicit runtime check.
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 approach is more verbose and less integrated with the model schema.
When migrating from v1, remember that in v1 a plain @property is not included in dict() or JSON output unless you add it manually. In v2, a computed_field property is included automatically, which changes the serialized output. Review API responses after upgrading to ensure no computed values are unexpectedly missing or added.