Problem
The recommended adopter pattern — extend a library type and override a nested field with a more-specific element type — requires # type: ignore[assignment] to satisfy mypy under strict = true. We carry roughly 50 of these in our codebase across the schema package.
The mypy Pydantic plugin treats field annotations as invariant under inheritance, even when the override is logically covariant (every AffectedPackage IS-A LibraryAffectedPackage).
Reproducer
from adcp.types import UpdateMediaBuyResponse as LibraryUpdateMediaBuyResponse
from adcp.types import AffectedPackage as LibraryAffectedPackage
from pydantic import Field
class AffectedPackage(LibraryAffectedPackage):
# Internal-only fields excluded on the wire
changes_applied: dict | None = Field(default=None, exclude=True)
buyer_package_ref: str | None = Field(default=None, exclude=True)
class UpdateMediaBuyResponse(LibraryUpdateMediaBuyResponse):
# Override to use the extended element type — same shape on the wire,
# extra internal fields excluded via exclude=True. Pydantic accepts it
# at runtime; mypy rejects it.
affected_packages: list[AffectedPackage] | None = None # type: ignore[assignment]
mypy --strict complains about affected_packages being incompatible with the parent's list[LibraryAffectedPackage]. There's nothing wrong at runtime — every AffectedPackage is a LibraryAffectedPackage. The issue is that mypy sees list[X] as invariant in X.
Where this hits us
We carry ~50 instances of # type: ignore[assignment] in src/core/schemas/ for exactly this pattern. Sample:
This is the recommended pattern (Critical Pattern #1 in our CLAUDE.md). It's not a code smell on our side — we're following the docs. The strictness ergonomics for adopters are the issue.
Proposed SDK shape
A few possible directions, in increasing order of impact:
A) Use Sequence[X] instead of list[X] in library response types where adopters are expected to override the element. Sequence is covariant in its element type, so Sequence[AffectedPackage] is a valid subtype of Sequence[LibraryAffectedPackage].
Drawback: callers who want .append() need a mutable type. May not work for in-place mutation paths.
B) Generic library types parameterised on extension points. The library generates UpdateMediaBuyResponse[TAffectedPackage = AffectedPackage], and adopters subclass with class UpdateMediaBuyResponse(LibraryUpdateMediaBuyResponse[AffectedPackage]): .... mypy is happy; no type: ignore needed.
Drawback: more codegen complexity; new public API.
C) Codegen extension hooks. datamodel-codegen emits # extension: list-element-type comments on field annotations that adopters typically override; a separate codegen step generates an Extended<Type> skeleton that adopters can subclass with the field overridden cleanly.
Drawback: most invasive.
Why this matters beyond us
Any Python adopter who extends library types under strict mypy hits this. We absorb 50 type: ignore lines because we're the most advanced adopter; subsequent adopters either (a) carry the same lines, (b) silence the errors with bare type: ignore, or (c) abandon the inheritance pattern in favour of duplication — which the docs explicitly warn against.
This is a strictness-ergonomics gap, not a runtime bug. Companion to #615 (nested model_dump() overrides), which is the same family of friction.
Files
Problem
The recommended adopter pattern — extend a library type and override a nested field with a more-specific element type — requires
# type: ignore[assignment]to satisfy mypy understrict = true. We carry roughly 50 of these in our codebase across the schema package.The mypy Pydantic plugin treats field annotations as invariant under inheritance, even when the override is logically covariant (every
AffectedPackageIS-ALibraryAffectedPackage).Reproducer
mypy --strictcomplains aboutaffected_packagesbeing incompatible with the parent'slist[LibraryAffectedPackage]. There's nothing wrong at runtime — everyAffectedPackageis aLibraryAffectedPackage. The issue is that mypy seeslist[X]as invariant inX.Where this hits us
We carry ~50 instances of
# type: ignore[assignment]in src/core/schemas/ for exactly this pattern. Sample:affected_packages: list[AffectedPackage] | NoneThis is the recommended pattern (Critical Pattern #1 in our CLAUDE.md). It's not a code smell on our side — we're following the docs. The strictness ergonomics for adopters are the issue.
Proposed SDK shape
A few possible directions, in increasing order of impact:
A) Use
Sequence[X]instead oflist[X]in library response types where adopters are expected to override the element.Sequenceis covariant in its element type, soSequence[AffectedPackage]is a valid subtype ofSequence[LibraryAffectedPackage].Drawback: callers who want
.append()need a mutable type. May not work for in-place mutation paths.B) Generic library types parameterised on extension points. The library generates
UpdateMediaBuyResponse[TAffectedPackage = AffectedPackage], and adopters subclass withclass UpdateMediaBuyResponse(LibraryUpdateMediaBuyResponse[AffectedPackage]): .... mypy is happy; notype: ignoreneeded.Drawback: more codegen complexity; new public API.
C) Codegen extension hooks. datamodel-codegen emits
# extension: list-element-typecomments on field annotations that adopters typically override; a separate codegen step generates anExtended<Type>skeleton that adopters can subclass with the field overridden cleanly.Drawback: most invasive.
Why this matters beyond us
Any Python adopter who extends library types under strict mypy hits this. We absorb 50
type: ignorelines because we're the most advanced adopter; subsequent adopters either (a) carry the same lines, (b) silence the errors with baretype: ignore, or (c) abandon the inheritance pattern in favour of duplication — which the docs explicitly warn against.This is a strictness-ergonomics gap, not a runtime bug. Companion to #615 (nested
model_dump()overrides), which is the same family of friction.Files