Repository: pydantic/pydantic (Python)

Issue:
2.12.0: Follow up to issue #11748
### Initial Checks

- [x] I confirm that I'm using Pydantic V2

### Description

Follow up to https://github.com/pydantic/pydantic/issues/11748

Thanks so much for rolling out a fix so quickly for the original bug 🙏 

Unfortunately it seems that the bug is still there as soon as we change the original MRE to make the `Other` model class inherit from another model 🥲 

### Example Code

```Python
from typing import Generic, Optional, TypeVar
from pydantic import BaseModel


class ConfiguredBaseModel(BaseModel):
    """Base model to share configuration settings."""
    # example model_config commented out since the bug seems present with or
    # without the model_config set

    # model_config = ConfigDict(
    #     extra="ignore",
    #     revalidate_instances="always",
    #     validate_assignment=True,
    # )


T = TypeVar("T")


class Base(ConfiguredBaseModel, Generic[T]):
    t: Optional[T]


class Other(ConfiguredBaseModel): # if we change this to inherit from BaseModel then the model validates as expected
    children: "Base[Other]"


a = Other.model_validate({"children": {"t": {"children": {"t": None}}}})

print(a)
# Expected Output: children=Base[Other](t=Other(children=Base[Other](t=None)))
# Actual Output: children=Base[Other](t=Other())
```

### Python, Pydantic & OS Version

```Text
pydantic version: 2.12.1
        pydantic-core version: 2.41.3
          pydantic-core build: profile=release pgo=false
               python version: 3.13.7 (main, Sep  2 2025, 14:05:52) [Clang 20.1.4 ]
                     platform: macOS-15.7.1-arm64-arm-64bit-Mach-O
             related packages: typing_extensions-4.15.0
                       commit: unknown
```


Discussion:
Thanks for the report, I have a fix that I will backport in the next patch release.



Patch:
diff --git a/pydantic/_internal/_generate_schema.py b/pydantic/_internal/_generate_schema.py
index ad0bba5213..839764cefb 100644
--- a/pydantic/_internal/_generate_schema.py
+++ b/pydantic/_internal/_generate_schema.py
@@ -764,7 +764,7 @@ def _model_schema(self, cls: type[BaseModel]) -> core_schema.CoreSchema:
                 if cls.__pydantic_fields_complete__ or cls is BaseModel_:
                     fields = getattr(cls, '__pydantic_fields__', {})
                 else:
-                    if not hasattr(cls, '__pydantic_fields__'):
+                    if '__pydantic_fields__' not in cls.__dict__:
                         # This happens when we have a loop in the schema generation:
                         # class Base[T](BaseModel):
                         #     t: T
diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py
index f18b5e82b3..4fe223c97c 100644
--- a/pydantic/_internal/_model_construction.py
+++ b/pydantic/_internal/_model_construction.py
@@ -340,7 +340,7 @@ def __pydantic_fields_complete__(self) -> bool:
 
         This is a private attribute, not meant to be used outside Pydantic.
         """
-        if not hasattr(self, '__pydantic_fields__'):
+        if '__pydantic_fields__' not in self.__dict__:
             return False
 
         field_infos = cast('dict[str, FieldInfo]', self.__pydantic_fields__)  # pyright: ignore[reportAttributeAccessIssue]


Test patch:
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 192a7d6c06..e7398be33c 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -1724,6 +1724,31 @@ class Other(BaseModel):
     Base[Other].model_validate({'t': {'child': {'t': {'child': None}}}})
 
 
+def test_generic_recursive_models_parametrized_with_model_subclass() -> None:
+    """https://github.com/pydantic/pydantic/issues/12396.
+
+    Follow up on `test_generic_recursive_models_parametrized_with_model()`.
+    """
+
+    # The code to check if `__pydantic_fields__` was set was wrongly
+    # checking for parent classes as well (and not in the class' `__dict__`):
+    class MyBaseModel(BaseModel):
+        pass
+
+    T = TypeVar('T')
+
+    class Base(MyBaseModel, Generic[T]):
+        t: T
+
+    class Other(MyBaseModel):
+        child: 'Optional[Base[Other]]'
+
+    with pytest.raises(ValidationError):
+        Base[Other].model_validate({'t': {}})
+
+    Base[Other].model_validate({'t': {'child': {'t': {'child': None}}}})
+
+
 @pytest.mark.xfail(reason='Core schema generation is missing the M1 definition')
 def test_generic_recursive_models_inheritance() -> None:
     """https://github.com/pydantic/pydantic/issues/9969"""