Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing a python generic to parent class?

I have a parent class which is declared as a generic, an abstract subclass, and a concrete implementation of that subclass that declares the generic type:

MyType = TypeVar('MyType')

class A(Generic[MyType]):
   a: MyType

class B(Generic[MyType], A[MyType]):
   pass

class C(B[int]):
   pass

But this doesn't forward the generic declaration from C to A, so the type of a is not int. Is there a correct way to do this? Tried searching both Stack Overflow and the Python docs but could not find anything.

like image 923
Noam Avatar asked Mar 22 '26 04:03

Noam


2 Answers

On A you have a class variable, so it is shared amongst all instances of the class. If you try and type hint this you have a conflict anytime you create a new sub-class of A.

For instance what type does a have here:

class A(Generic[MyType]):
   a: MyType

class A1(A[str]):
   pass

class A2(A[int]):
   pass

If you want to represent a member variable on A then you can do it like this:

class A(Generic[MyType]):
    def __init__(self, a: MyType):
        self.val = a


class B(Generic[MyType], A[MyType]):
    def __init__(self, b: MyType):
        A.__init__(self, b)


class C(B[int]):
    def __init__(self, c: int):
        B.__init__(self, c)


class D(B[str]):
    def __init__(self, d: str):
        B.__init__(self, d)

Here we have two classes C and D, which have different generics int and str, and the type hinting works because we are creating sub-classes which have different generics on them.

Hope after 6 months this might help :)

like image 158
Henry B Avatar answered Mar 23 '26 17:03

Henry B


The accepted answer from @henry-b is wrong on a few points.

  1. a in the example is not a class variable, because it is not assigned to anything. It is only added to A.__annotations__.
  2. Because of (1), A1.a and A2.a (and similarly C.a and D.a in the second example) already have different types. There is nothing to worry about.
  3. Although the abstract subclass B in the original example doesn't "forward" the generic declaration, this is already true even for a direct concrete subclass:
class A(Generic[T]):
    a: T

class C(A[int]):
   pass

inspect.get_annotations(A)  # {'a': ~T}
inspect.get_annotations(B)  # {}

Breaking down the solution in the accepted answer, it does two things:

  1. Create an instance variable val (I'll call it a for consistency with the original question)
  2. Make the subclasses' __init__ functions "inspect"-able with the implementation type.

We can write that in a cleaner way:

# This is a pure abstract class, so it shouldn't have __init__
class A(Generic[T]):
    a: T

class B(Generic[T], A[T]):
    def __init__(self, a: T):
        self.a = a

class C(B[int]):
    def __init__(self, a: int):
        super().__init__(a)

class D(B[str]):
    pass

# As mentioned above, if we inspect `C.__init__` we get the impl type,
# while for `D.__init__` it's still the generic type:
inspect.get_annotations(C.__init__)  # {'a': <class 'int'>}
inspect.get_annotations(D.__init__)  # {'a': ~T}

What the accepted answer does not do is what it claims to do: "forward" the type annotation from the concrete class to the abstract ancestor class (i.e., answer the question). The instance variable a is still the generic type:

C(1).__annotations__      # {'a': ~T}
D('foo').__annotations__  # {'a': ~T}

And it doesn't matter anyway; PyType can catch invalid types either way:

c = C(1)      # fine
d = D('foo')  # fine

c = C('foo')  # pytype throws 'wrong-arg-types'
d = D(1)      # pytype throws 'wrong-arg-types'

If you do want the concrete class to have inspect-able types for the instance attribute, you have to annotate it again:

class E(B[bool]):
    a: bool
    def __init__(self, a: bool):
        super().__init__(a)
like image 24
Ben Withbroe Avatar answered Mar 23 '26 16:03

Ben Withbroe



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!