Search code examples
pythongenericspython-typingpydantic

Generic of Class ignored by Generic Pydantic constructor


I have a generic class with a function that returns a Pydantic model where one of the fields is the generic type. What follows is a code snippet that defines two classes the generic GetValue and GetInt. I don't understand why the behavior of GetValue[int] is not the same as GetInt.

from typing import Any, Generic, TypeVar

from pydantic import BaseModel

X = TypeVar("X")


class TestModel(BaseModel, Generic[X]):
    a: X


T = TypeVar("T")


class GetValue(Generic[T]):

    @classmethod
    def get_value(cls, x: Any) -> TestModel[T]:
        return TestModel[T](a=x)


class GetInt:
    @classmethod
    def get_value(cls, x: Any) -> TestModel[int]:
        return TestModel[int](a=x)


res = GetValue[int].get_value("1")
assert type(res.a) == str


res_2 = GetInt.get_value("1")

assert type(res_2.a) == int

assert res.a != res_2.a # I want these two to be equal, not sure why they are different.

x: Any = "1"
res_3 = TestModel[int](a=x)
assert type(res_3.a) == int

Inspecting things in my debugger I see that in this function call GetValue[int].get_value("1") T is just a TypeVar. This causes Pydantic to fall back on an Any annotation for a so it stays as a str. res_3 shows that when the type is supplied to the Pydantic class it will convert the string to an int. I'm not sure why this construction isn't working how I'd expect. Still new to python's typing system, was hoping someone could shed light on this.

Python version: Python 3.12.5

pydantic version: 2.10.0


Solution

  • Well, Python doesn't really support a simple way to determine the type of a TypeVar at runtime.

    pydantic models are a bit special because they override the standard behaviour of generic classes such that they can more or less easily determine a generic type.

    However, I just realized that in your code you're not trying around with a pydantic generic model (which I thought, when I wrote the comment yesterday). Instead, you want that for any "standard" generic class, right?

    You may want to inform yourself about all this stuff with __args__ and _GenericAlias in the typing module. But I actually created a package (python-generics) some time ago to solve this problem. With that, the solution would look like:

    from typing import Any, Generic, TypeVar
    
    from generics import get_filled_type
    from pydantic import BaseModel
    
    X = TypeVar("X")
    
    
    class TestModel(BaseModel, Generic[X]):
        a: X
    
    
    T = TypeVar("T")
    
    
    class GetValue(Generic[T]):
    
        @classmethod
        def get_value(cls, x: Any) -> TestModel[T]:
            return TestModel[get_filled_type(cls, GetValue, T)](a=x)
    

    One last note: It is generally possible for type checkers like mypy to narrow the type for generic classes through e.g. constructors elements. Meaning that the generic type doesn't necessarily have to be explicitly defined in the code to pass the type check. But if you want the solution to work you must always define the type explicitly.