Skip to content

feat: make list field type hints mypy-compatible with subclasses #108

@bokelley

Description

@bokelley

Summary

The runtime coercion from #106 works great, but mypy still complains about list invariance because the type hints use list[Product] instead of something covariant.

Problem

from adcp import GetProductsResponse, Product

class OurProduct(Product):
    internal_config: dict | None = Field(default=None, exclude=True)

products: list[OurProduct] = [OurProduct(id="1", name="Test")]

# Runtime: works (BeforeValidator coercion)
# Mypy: error - list[OurProduct] not assignable to list[Product]
response = GetProductsResponse(products=products)

We currently work around this with cast():

response = GetProductsResponse(products=cast(list[Product], products))

Potential Solutions

Option 1: Use Union in type hints

from typing import Union

class GetProductsResponse(BaseModel):
    # Accept list of Product or any subclass
    products: list[Product] | list[Any]

This is ugly and loses type safety.

Option 2: Use Sequence (covariant) in public API

from typing import Sequence

class GetProductsResponse(BaseModel):
    # Sequence is covariant, so Sequence[SubClass] is assignable
    products: Sequence[Product]

Issue: Pydantic may not handle Sequence the same as list.

Option 3: Type stub overrides

Provide .pyi stub files that declare the fields as accepting subclass lists, while the actual implementation uses list.

Option 4: Custom generic types

from typing import TypeVar, Generic

T = TypeVar("T", bound=Product)

class GetProductsResponse(BaseModel, Generic[T]):
    products: list[T]

Allows GetProductsResponse[OurProduct] but changes the API.

Option 5: Accept current limitation

Document that cast() is needed for mypy when using subclass lists, since this is a fundamental Python typing limitation (list invariance).

Recommendation

I'd lean toward Option 5 (document the limitation) or Option 2 if Pydantic handles Sequence properly. The BeforeValidator handles runtime coercion, and cast() is a reasonable workaround for static typing since it has no runtime cost.

Would love to hear thoughts on whether there's a cleaner solution I'm missing.

Related

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions