TypeVar in class __init__ type hinting - python

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*'

Related

Type Hinting: Use type of a class member as function return type (for inheritance)

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)

Type-hinting classes belonging to the same interface

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

Change return type of child class

I have a base class (BaseClass) that operates using objects of a base type (BaseType). I want a child class (ChildClass) that inherits from BaseClass but operates using objects of a child type (ChildType).
Is there a way to accomplish this without type hinting errors?
Consider the following code:
from dataclasses import dataclass
#dataclass
class BaseType:
name: str
#dataclass
class ChildType(BaseType):
favoriteColor: str
class BaseClass:
def __init__(self, myThing: BaseType):
self.myThing = myThing
def get_my_thing(self) -> BaseType:
return self.myThing
class ChildClass(BaseClass):
def __init__(self, myThing: ChildType):
self.myThing = myThing
self.do_something_super_complicated()
def do_something_super_complicated():
color = self.myThing.favoriteColor
# do complicated things with color
ChildClass.get_my_thing() now has a return type of ChildType. How can I mark that this is the case? For instance, I don't want to get a type hint error by doing this:
cc = ChildClass(ChildType(name="bob", favoriteColor="red"))
print(cc.get_my_thing().favoriteColor)
# This causes a type hint error because return type of get_my_thing is BaseType which does not have favoriteColor
If I do the following, I still get a type hint error:
class ChildClass(BaseClass):
def __init__(self, myThing: ChildType):
self.myThing = myThing
def get_my_thing(self) -> ChildType:
return super().get_my_thing()
#The return type of super().get_my_thing() is BaseType so this causes a type hint error
There is value in that type hint error - a value of BaseType isn't necessarily a value of ChildType. Concretely, if BaseType is Car and ChildType is Tesla, super().get_my_thing() in ChildClass returns some Car, but you are trying to treat it specifically like a Tesla.
From your example, it seems that you want to model some class that takes in a generic type (that must be a subclass of BaseType) and has a method get_my_thing that has the generic type as its return type. You can consider to define a single class instead, as follow:
from dataclasses import dataclass
from typing import Generic, TypeVar
#dataclass
class BaseType:
name: str
#dataclass
class ChildType(BaseType):
favoriteColor: str
T = TypeVar('T', bound=BaseType)
class MyClass(Generic[T]):
def __init__(self, myThing: T):
self.myThing = myThing
def get_my_thing(self) -> T:
return self.myThing
class BaseClass(MyClass[BaseType]):
pass
class ChildClass(MyClass[ChildType]):
pass
base_type = BaseType("Bob")
base_instance = BaseClass(base_type)
base_instance.get_my_thing().name # OK
child_type = ChildType("Bob", "blue")
child_instance = ChildClass(child_type)
child_instance.get_my_thing().name # OK
child_instance.get_my_thing().favoriteColor # OK
The type hinting works on my editor (VS Code, using the Pylance language server for checking type hints).

Dynamic typing of subclasses using generics in Python

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)

Subclass in type hinting

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]):
...

Categories

Resources