Search code examples
pythongenericstyping

Python Generic that creates or isinstance checks their type parameter


I want to create an inheritable class that can be parameterized with a type. Here is a working example without type annotations:

class Simple:
    pass

class Bla:
    obj_class = Simple

    def do(self):
        return self.obj_class()

    def check(self, x):
        return isinstance(x, self.obj_class)

Users of this code would inherit from Bla, and can set a different obj_class, like so:

class Advanced(Simple):
    pass

class Foo(Bla):
    obj_class = Advanced

The problem starts when I want to correctly type annotate this. I thought of making Bla inherit from Generic[T], where T is defined as TypeVar('T', bound=Simple), but then the constructor T() and isinstance won't work, and also manually assigning a different class to obj_class also doesn't work.

Here is one non-working example, as T can't be used in non-typing contexts:

class Simple:
    pass

T = TypeVar('T', bound=Simple)

class Bla(Generic[T]):
    def do(self) -> T:
        return T()

    def check(self, x: Any) -> bool:
        return isinstance(x, T)

Here is another non-working example, where I can't assign Simple to obj_class because of incompatible types.

class Simple:
    pass

T = TypeVar('T', bound=Simple)

class Bla(Generic[T]):
    obj_class: Type[T] = Simple

    def do(self) -> T:
        return self.obj_class()

    def check(self, x: Any) -> bool:
        return isinstance(x, self.obj_class)

class Advanced(Simple):
    pass

class Foo(Bla):
    obj_class = Advanced

Is there a way to solve this?


Solution

  • You don't need Type[T] = Simple.

    mypy states:

    Incompatible types in assignment (expression has type "Type[Simple]", variable has type "Type[T]").

    You are trying to assign a concrete type to a generic type variable.

    Instead, do something like:

    class Simple:
        pass
    
    
    class Advanced(Simple):
        pass
    
    
    class Other:
        pass
    
    
    T = TypeVar('T', bound=Simple)
    
    
    class Bla(Generic[T]):
        obj_class: Type[T]
    
        def do(self) -> T:
            return self.obj_class()
    
        def check(self, x: Any) -> bool:
            return isinstance(x, self.obj_class)
    
    
    class SimpleFoo(Bla[Simple]):
        obj_class = Simple
    
    
    class AdvancedFoo(Bla[Advanced]):
        obj_class = Advanced
    
    
    class OtherFoo(Bla[Other]):
        obj_class = Other
    

    Now, mypy correctly states:

    error: Type argument "tmp.Other" of "Bla" must be a subtype of "tmp.Simple"

    Note

    OtherFoo has to subclass Bla with a specific type so mypy will correctly warn you.

    The following produces no errors:

    class OtherFoo(Bla):
        obj_class = Other