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.
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 :)
The accepted answer from @henry-b is wrong on a few points.
a in the example is not a class variable, because it is not assigned to anything. It is only added to A.__annotations__.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.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:
val (I'll call it a for consistency with the original question)__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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With