I want to allow type hinting using Python 3 to accept sub classes of a certain class. E.g.:
class A:
pass
class B(A):
pass
class C(A):
pass
def process_any_subclass_type_of_A(cls: A):
if cls == B:
# do something
elif cls == C:
# do something else
Now when typing the following code:
process_any_subclass_type_of_A(B)
I get an PyCharm IDE hint 'Expected type A, got Type[B] instead.'
How can I change type hinting here to accept any subtypes of A?
According to PEP 484 ("Expressions whose type is a subtype of a specific argument type are also accepted for that argument."), I understand that my solution (cls: A) should work?
When you specify cls: A, you're saying that cls expects an instance of type A. The type hint to specify cls as a class object for the type A (or its subtypes) uses typing.Type.
from typing import Type
def process_any_subclass_type_of_A(cls: Type[A]):
pass
From The type of class objects
:
Sometimes you want to talk about class objects that inherit from a
given class. This can be spelled as Type[C] where C is a class. In
other words, when C is the name of a class, using C to annotate an
argument declares that the argument is an instance of C (or of a
subclass of C), but using Type[C] as an argument annotation declares
that the argument is a class object deriving from C (or C itself).
If we look at the Type description from the typing module, then we see these docs:
A special construct usable to annotate class objects.
For example, suppose we have the following classes::
class User: ... # Abstract base for User classes
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...
And a function that takes a class argument that's a subclass of
User and returns an instance of the corresponding class::
U = TypeVar('U', bound=User)
def new_user(user_class: Type[U]) -> U:
user = user_class()
# (Here we could write the user object to a database)
return user
joe = new_user(BasicUser)
At this point the type checker knows that joe has type BasicUser.
Based on this, I can imagine a synthetic example that reproduces the problem with type hinting errors in PyCharm.
from typing import Type, Tuple
class BaseClass: ...
class SubClass(BaseClass): ...
class SubSubClass(SubClass): ...
def process(model_instance: BaseClass, model_class: Type[BaseClass]) -> Tuple[BaseClass, BaseClass]:
""" Accepts all of the above classes """
return model_instance, model_class()
class ProcessorA:
#staticmethod
def proc() -> Tuple[SubClass, SubClass]:
""" PyCharm will show an error
`Expected type 'tuple[SubClass, SubClass]', got 'tuple[BaseClass, BaseClass]' instead` """
return process(SubClass(), SubClass)
class ProcessorB:
#staticmethod
def proc() -> Tuple[SubSubClass, SubSubClass]:
""" PyCharm will show an error
`Expected type 'tuple[SubSubClass, SubSubClass]', got 'tuple[BaseClass, BaseClass]' instead` """
return process(SubSubClass(), SubSubClass)
But we see in docs for Type that the situation can be corrected by using TypeVar with the bound argument. Then use it in places where BaseClass is declared as a type.
from typing import TypeVar, Type, Tuple
class BaseClass: ...
B = TypeVar('B', bound=BaseClass)
class SubClass(BaseClass): ...
class SubSubClass(SubClass): ...
def process(model_instance: B, model_class: Type[B]) -> Tuple[B, B]:
""" Accepts all of the above classes """
return model_instance, model_class()
class ProcessorA:
#staticmethod
def proc() -> Tuple[SubClass, SubClass]:
return process(SubClass(), SubClass)
class ProcessorB:
#staticmethod
def proc() -> Tuple[SubSubClass, SubSubClass]:
return process(SubSubClass(), SubSubClass)
Hope this will be helpful.
Type[A] accepts also the class itself, which is not always needed.
If you want your function to accept only subclasses, you should go with NewType, like
class A:
pass
B = NewType('B', A)
def foo(cls: Type[B]):
...
Related
What is the correct way to reuse the type of a class member to type hint other items in the class? As an example:
from typing import Type
class Model:
pass
class ChildModel:
childvar = "Child Model"
class Base:
var: Type[Model]
def fn(self) -> ??:
return self.var
class Child(Base):
var = ChildModel
def new_fn(self):
x = self.fn() # Type of x should be "ChildModel"
print(x.childvar)
Child().new_fn() # Prints "Child Model" successfully
I am looking for what would work to replace ?? such that the return type of fn() can be inferred for all child classes.
MyPy does not accept changing ?? to Type[Model] to match Base.var: Incompatible types in assignment (expression has type "Type[ChildModel]", base class "Base" defined the type as "Type[Model]" (though it is possible I made a mistake here). Even if this were allowed, this would allow Base.fn() to return any Model or Model subclass, not strictly the type of var (as defined in a child of Base)
Something like T = TypeVar("T", bound=Type[Model]) seems disallowed without generics, which don't seem quite applicable since the type can be inferred without generic-style specification. I think the solution would likely also work to type hint method arguments, method-local variables, and other class member variables.
What is the best way to do this (if possible)?
Edit: adding clarification, corrected issue with code
This can be accomplished with Generics.
from typing import Generic, TypeVar
T = TypeVar("T", bound="Model")
class Model:
pass
class ChildModel(Model):
childvar = "Child Model"
class Base(Generic[T]):
var: type[T]
def fn(self) -> type[T]:
return self.var
class Child(Base[ChildModel]):
var = ChildModel
def new_fn(self):
x = self.fn() # Type of x is type["ChildModel"]
print(x.childvar)
Child().new_fn()
Though this probably fails the "Explicit is better than Implicit" test, I suppose this will get you what you want while avoiding typing in two places. In this case, rather than defining var on the Child, the var is pulled from the annotation.
Tested on Python 3.10
import typing
from typing import Generic, TypeVar
T = TypeVar("T", bound="Model")
class Model:
pass
class ChildModel(Model):
childvar = "Child Model"
class Base(Generic[T]):
#classmethod
#property
def var(cls) -> type[T]:
for superclass in cls.__orig_bases__:
if getattr(superclass, "__origin__", None) == Base:
return typing.get_args(superclass)[0]
def fn(self) -> type[T]:
return self.var
class Child(Base[ChildModel]):
def new_fn(self):
x = self.fn() # Type of x is type["ChildModel"]
print(x.childvar)
I need to initialise an object of type TypeVar described in an generic class
class TDict(TypedDict):
a: int
T = TypeVar("TType", bound=TDict)
class TestClass(Generic[T]):
d: T
def __init__(self) -> None:
self.d = TDict(a=1)
This cause an error
Incompatible types in assignment (expression has type "TDict", variable has type "T")
At the same time, I cannot create an object with type T. How can I create an object with type T?
upd.
I need to inherit typed dict from TDict and use it in my program. So I heed TestClass to create a dict object of inherited class
Something like this
class ExpandedTDict(TDict):
b: int
t_class: TestClass[ExpandedTDict] = TestClass[ExpandedTDict]()
assert t_class.d[a] == 1
t_class.d[b] = 2
class OtherExpandedTDict(TDict):
c: int
other_t_class: TestClass[OtherExpandedTDict] = TestClass[OtherExpandedTDict]()
assert other_t_class.d[a] == 1
other_t_class.d[c] = 2
other_t_class.d[b] = 2 # -> error
In Python, type variables are "erased" at runtime -- you can't access T at runtime.
You can instantiate the class without providing a type to the class at all:
my_class: TestClass[SubclassOfTDict] = TestClass()
You don't necessarily have a concrete class as the parameter. For example, T could be assigned to Any or to a union of classes. What do you do in that case?
What you can do is pass the dictionary into __init__:
class TestClass(Generic[T]):
d: T
def __init__(self, d: T):
self.d = d
However, I think you're experiencing some kind of XY problem. I don't see how it's useful to instantiate a subclass of a TypedDict class, given that a subclass will have additional fields. Could you explain more about your probleM?
You can wrap the constructor call in a generic function. That lets you specify the type without the redundancy of also specifying it as a generic type to the class.
from typing import TypeVar, Type, Generic, TypedDict
class TDict(TypedDict):
a: int
_T = TypeVar("_T", bound="TDict")
class TestClass(Generic[_T]):
def __init__(self, type_: Type[_T]):
self.d: _T = type_(a=1)
# This is the trick you're looking for
def new_test_class(type_: Type[_T]):
return TestClass[_T](type_)
class ExpandedTDict(TDict):
b: int
class OtherExpandedTDict(TDict):
c: int
t_class = new_test_class(ExpandedTDict)
assert t_class.d['a'] == 1
t_class.d['b'] = 2
other_t_class = new_test_class(OtherExpandedTDict)
assert other_t_class.d['a'] == 1
other_t_class.d['c'] = 2
print(other_t_class.d['b']) # -> error. reading fails as desired. assignment as in your example won't fail no matter what.
This also works with regular classes and attributes.
Code:
import abc
class Interface(abc.ABC):
#abc.abstractmethod
#classmethod
def make(cls): ...
class AObject(Interface):
def __init__(self, a: int):
self.a = a
#classmethod
def make(cls):
return cls(a=3)
class BObject(Interface):
def __init__(self, b: int):
self.b = b
#classmethod
def make(cls):
return cls(b=3)
data: tuple[Interface, ...] = (AObject, BObject) # Incompatible types in assignment (expression has type "Tuple[Type[AObject], Type[BObject]]", variable has type "Tuple[Interface, ...]") [assignment]
There is an interface that implements classes and we need to specify that the classmethod make exists for the class. But if you specify the type tuple[Interface, ...], MyPy will return an error, because you can specify the type only for class instances, and not for the classes themselves
So, the question is — how to do it correctly?
I'm not sure I understand your problem, but if you want to specify that a variable stores a class of some sort you can use typing.Type:
import abc
from typing import Tuple, Type
...
data: Tuple[Type[Interface], Type[Interface]] = (AObject, BObject) # mypy is happy
Let's say i have to following classes.
class A:
#staticmethod
def foo():
pass
class B(A):
pass
And I have some kind of function that constructs an object based on it's type as well as calls a function.
def create(cls: Type[A]) -> A:
cls.foo()
return cls()
Now I can make the following calls to that function. And because B inherits from A it's all good.
instance_a: A = create(A)
instance_b: B = create(B)
Except the with the latter, type-checking will start complaining because create according to the annotations returns an instance of A.
This could be solved with TypeVar as follows.
from typing import Type, TypeVar
T = TypeVar('T')
def create(cls: Type[T]) -> T:
cls.foo()
return cls()
Except now the typing checking doesn't do it's original job of guarantying that cls has a method called foo. Is there a way to specify a generic to be of a certain type?
You can supply a bound:
T = TypeVar('T', bound=A)
I am trying to use a TypeVar to indicate an init parameter as a certain type.
But I am doing it wrong, or it might not even be possible.
from typing import TypeVar
T=TypeVar("T")
class TestClass:
def __init__(self,value:T):
self._value=value
a = TestClass(value=10)
b = TestClass(value="abc")
reveal_type(a._value)
reveal_type(b._value)
I was hoping the reveal type of a._value would have been int and b._value to have been string.
But they are both revealed as 'T`-1'
Any help or insight appreciated!
[EDIT]
A little more expanded example.
The BaseClass will be overridden and the actual type hint is provided by the overriding class.
from typing import TypeVar
T=TypeVar("T")
class BaseClass:
def __init__(self,value):
self._value = value
class Class1(BaseClass):
def __init__(self,value:str):
super().__init__(value)
class Class2(BaseClass):
def __init__(self,value:int):
super().__init__(value)
a = Class1("A value")
b = Class2(10)
reveal_type(a._value)
reveal_type(b._value)
By default, using a TypeVar restricts its scope only to the method/function in which it is used as an annotation. In order to scope a TypeVar to the instance and all methods/attributes, declare the class as Generic.
from typing import TypeVar, Generic
T=TypeVar("T")
class BaseClass(Generic[T]): # Scope of `T` is the class:
def __init__(self, value: T): # Providing some `T` on `__init__`
self._value = value # defines the class' `T`
This allows declaring subclasses either as generic or as concrete.
class Class1(BaseClass[str]): # "is a" BaseClass where `T = str`
pass # No need to repeat ``__init__``
class ClassT(BaseClass[T]): # "is a" BaseClass where `T = T'`
#property
def value(self) -> T:
return self._value
reveal_type(Class1("Hello World")._value) # Revealed type is 'builtins.str*'
reveal_type(Class1(b"Uh Oh!")._value) # error: Argument 1 to "Class1" has incompatible type "bytes"; expected "str"
reveal_type(ClassT(42).value) # Revealed type is 'builtins.int*'