Jump to solution
Verify

The Fix

Better document `BaseModel.__pydantic_generic_metadata__` to clarify its purpose and usage.

Based on closed pydantic/pydantic issue #12720 · PR/commit linked

Jump to Verify Open PR/Commit
@@ -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.
repro.py
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) ...
verify
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
fix.md
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

Discussion

High-signal excerpts from the issue thread (symptoms, repros, edge-cases).

“Similar solution: https://github.com/pydantic/pydantic/issues/11000#issuecomment-2535932284”
@Viicos · 2026-01-21 · source

Failure Signature (Search String)

  • :raises AssertionError: if `cls` specializes `base` in different ways by
  • raise AssertionError(
Copy-friendly signature
signature.txt
Failure Signature ----------------- :raises AssertionError: if `cls` specializes `base` in different ways by raise AssertionError(

Error Message

Signature-only (no traceback captured)
error.txt
Error Message ------------- :raises AssertionError: if `cls` specializes `base` in different ways by raise AssertionError(

Minimal Reproduction

repro.py
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.

When NOT to use: This fix should not be used if the documentation needs to remain private or unchanged.

Fix reference: https://github.com/pydantic/pydantic/pull/12729

Last verified: 2026-02-09. Validate in your environment.

Get updates

We publish verified fixes weekly. No spam.

Subscribe

When NOT to Use This Fix

  • This fix should not be used if the documentation needs to remain private or unchanged.

Verify Fix

verify
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.