The Fix
Better document `BaseModel.__pydantic_generic_metadata__` to clarify its purpose and usage.
Based on closed pydantic/pydantic issue #12720 · PR/commit linked
@@ -132,8 +132,10 @@ class BaseModel(metaclass=_model_construction.ModelMetaclass):
__pydantic_decorators__: Metadata containing the decorators defined on the model.
This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
- __pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to
- __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
+ __pydantic_generic_metadata__: A dictionary containing metadata about generic Pydantic models.
import abc
import enum
from types import GenericAlias
from typing import Any, ClassVar, Generic, TypeVar, cast
import pydantic
from pydantic._internal._generics import get_args, get_origin
# The return type is really typing._GenericAlias, but that's private.
def _get_specialization(cls: type, base: type) -> Any:
"""
Inspect a class for its specialization of a generic base class.
A class may specialize a generic base class in various ways: for
example, it might supply specific values for some of the relevant type
variables, or it might itself be generic and rely on being specialized
by its own subclasses, or both. To help us introspect such classes,
this returns the specialization of `base` by `cls` in the form of a
:py:class:`typing._GenericAlias`.
:raises AssertionError: if `cls` specializes `base` in different ways by
means of multiple inheritance.
"""
specializations: set[GenericAlias] = set()
if hasattr(cls, "__pydantic_generic_metadata__"):
orig_bases = [get_origin(cls)[get_args(cls)]]
else:
orig_bases = getattr(cls, "__orig_bases__", [])
for cls_base in orig_bases:
origin = get_origin(cls_base)
if origin == base:
specializations.add(cls_base)
elif origin is not None and issubclass(origin, base):
specializations.add(
_get_specialization(origin, base)[get_args(cls_base)]
)
if len(specializations) != 1:
raise AssertionError(
f"{cls.__qualname__} must specialize {base.__qualname__} with "
f"exactly one consistent list of type arguments "
f"(got {specializations})"
)
[specialization] = specializations
return specialization
def extract_generic_type_arguments(
cls: type, expected_origin: type
) -> tuple[type, ...]:
"""
Extract type arguments from a generic class.
This is expected to be called from __init_subclass__ in a generic class
(i.e. one that has Generic[...] as a base class), and allows extracting
the specializing type arguments so that they can be used as factories.
"""
args = get_args(_get_specialization(cls, expected_origin))
assert isinstance(args, tuple)
return args
class ArtifactCategory(enum.StrEnum):
SOURCE_PACKAGE = "debian:source-package"
class ArtifactData(pydantic.BaseModel):
model_config = pydantic.ConfigDict(validate_assignment=True, extra="forbid")
class DebianSourcePackage(ArtifactData):
name: str
version: str
dsc_fields: dict[str, Any]
AD = TypeVar("AD", bound=ArtifactData)
class LocalArtifact(pydantic.BaseModel, Generic[AD], abc.ABC):
"""Represent an artifact locally."""
model_config = pydantic.ConfigDict(validate_assignment=True, extra="forbid")
category: ClassVar[ArtifactCategory]
data: AD
_data_type: type[AD]
_local_artifacts_category_to_class: ClassVar[
dict[str, type["LocalArtifact['Any']"]]
] = {}
def __init_subclass__(cls, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
if get_origin(cls) is not None:
# The task data type, computed by introspecting the type argument
# used to specialize this generic class.
[cls._data_type] = extract_generic_type_arguments(
cls, LocalArtifact
)
LocalArtifact._local_artifacts_category_to_class[cls.category] = cls
@classmethod
def create_data(cls, data_dict: dict[str, Any]) -> AD:
"""Instantiate a data model from a dict.
... (truncated) ...
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Option A — Apply the official fix\nBetter document `BaseModel.__pydantic_generic_metadata__` to clarify its purpose and usage.\nWhen NOT to use: This fix should not be used if the documentation needs to remain private or unchanged.\n\n
Why This Fix Works in Production
- Trigger: :raises AssertionError: if `cls` specializes `base` in different ways by
- Mechanism: The documentation for __pydantic_generic_metadata__ was unclear regarding its purpose and usage
Why This Breaks in Prod
- The documentation for __pydantic_generic_metadata__ was unclear regarding its purpose and usage
- Production symptom (often without a traceback): :raises AssertionError: if `cls` specializes `base` in different ways by
Proof / Evidence
- GitHub issue: #12720
- Fix PR: https://github.com/pydantic/pydantic/pull/12729
- Reproduced locally: No (not executed)
- Last verified: 2026-02-09
- Confidence: 0.80
- Did this fix it?: Yes (upstream fix exists)
- Own content ratio: 0.35
Discussion
High-signal excerpts from the issue thread (symptoms, repros, edge-cases).
“Similar solution: https://github.com/pydantic/pydantic/issues/11000#issuecomment-2535932284”
Failure Signature (Search String)
- :raises AssertionError: if `cls` specializes `base` in different ways by
- raise AssertionError(
Copy-friendly signature
Failure Signature
-----------------
:raises AssertionError: if `cls` specializes `base` in different ways by
raise AssertionError(
Error Message
Signature-only (no traceback captured)
Error Message
-------------
:raises AssertionError: if `cls` specializes `base` in different ways by
raise AssertionError(
Minimal Reproduction
import abc
import enum
from types import GenericAlias
from typing import Any, ClassVar, Generic, TypeVar, cast
import pydantic
from pydantic._internal._generics import get_args, get_origin
# The return type is really typing._GenericAlias, but that's private.
def _get_specialization(cls: type, base: type) -> Any:
"""
Inspect a class for its specialization of a generic base class.
A class may specialize a generic base class in various ways: for
example, it might supply specific values for some of the relevant type
variables, or it might itself be generic and rely on being specialized
by its own subclasses, or both. To help us introspect such classes,
this returns the specialization of `base` by `cls` in the form of a
:py:class:`typing._GenericAlias`.
:raises AssertionError: if `cls` specializes `base` in different ways by
means of multiple inheritance.
"""
specializations: set[GenericAlias] = set()
if hasattr(cls, "__pydantic_generic_metadata__"):
orig_bases = [get_origin(cls)[get_args(cls)]]
else:
orig_bases = getattr(cls, "__orig_bases__", [])
for cls_base in orig_bases:
origin = get_origin(cls_base)
if origin == base:
specializations.add(cls_base)
elif origin is not None and issubclass(origin, base):
specializations.add(
_get_specialization(origin, base)[get_args(cls_base)]
)
if len(specializations) != 1:
raise AssertionError(
f"{cls.__qualname__} must specialize {base.__qualname__} with "
f"exactly one consistent list of type arguments "
f"(got {specializations})"
)
[specialization] = specializations
return specialization
def extract_generic_type_arguments(
cls: type, expected_origin: type
) -> tuple[type, ...]:
"""
Extract type arguments from a generic class.
This is expected to be called from __init_subclass__ in a generic class
(i.e. one that has Generic[...] as a base class), and allows extracting
the specializing type arguments so that they can be used as factories.
"""
args = get_args(_get_specialization(cls, expected_origin))
assert isinstance(args, tuple)
return args
class ArtifactCategory(enum.StrEnum):
SOURCE_PACKAGE = "debian:source-package"
class ArtifactData(pydantic.BaseModel):
model_config = pydantic.ConfigDict(validate_assignment=True, extra="forbid")
class DebianSourcePackage(ArtifactData):
name: str
version: str
dsc_fields: dict[str, Any]
AD = TypeVar("AD", bound=ArtifactData)
class LocalArtifact(pydantic.BaseModel, Generic[AD], abc.ABC):
"""Represent an artifact locally."""
model_config = pydantic.ConfigDict(validate_assignment=True, extra="forbid")
category: ClassVar[ArtifactCategory]
data: AD
_data_type: type[AD]
_local_artifacts_category_to_class: ClassVar[
dict[str, type["LocalArtifact['Any']"]]
] = {}
def __init_subclass__(cls, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
if get_origin(cls) is not None:
# The task data type, computed by introspecting the type argument
# used to specialize this generic class.
[cls._data_type] = extract_generic_type_arguments(
cls, LocalArtifact
)
LocalArtifact._local_artifacts_category_to_class[cls.category] = cls
@classmethod
def create_data(cls, data_dict: dict[str, Any]) -> AD:
"""Instantiate a data model from a dict.
... (truncated) ...
Environment
- Pydantic: 1
What Broke
Developers faced confusion and inefficiencies when using generic models due to poor documentation.
Why It Broke
The documentation for __pydantic_generic_metadata__ was unclear regarding its purpose and usage
Fix Options (Details)
Option A — Apply the official fix
Better document `BaseModel.__pydantic_generic_metadata__` to clarify its purpose and usage.
Fix reference: https://github.com/pydantic/pydantic/pull/12729
Last verified: 2026-02-09. Validate in your environment.
When NOT to Use This Fix
- This fix should not be used if the documentation needs to remain private or unchanged.
Verify Fix
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Did This Fix Work in Your Case?
Quick signal helps us prioritize which fixes to verify and improve.
Prevention
- Add a CI check that diffs key outputs after upgrades (OpenAPI schema snapshots, JSON payload shapes, CLI output).
- Upgrade behind a canary and run integration tests against the canary before 100% rollout.
Related Issues
No related fixes found.
Sources
We don’t republish the full GitHub discussion text. Use the links above for context.