Reputation: 99
I'm trying to understand type annotations and I have the following code:
from typing import TypeVar
T = TypeVar('T')
class MyClass():
x: int = 10
def foo(obj: T) -> None:
print(obj.x)
foo(MyClass())
When I run mypy, I get the following error:
main.py:9: error: "T" has no attribute "x"
Found 1 error in 1 file (checked 1 source file)
But when I add bound='MyClass'
to the TypeVar
, it shows no errors.
What is the reason for this behavior? I tried to read the documentation, but didn't find any answer on what exactly is happening when bound
is set to a default value.
Upvotes: 4
Views: 5664
Reputation: 139
I got the same problem.
MyNewType = TypeVar("NewType", type_1_with_no_attribute, type_2_with_super_attribute)
def super_attribute_consumer(new_type_instance: MyNewType):
print(new_type_instance.super_attribute)
MYPY throws error like
error: "type_1_with_no_attribute" has no attribute "super_attribute"
My solution is to adding assert
sentence
def super_attribute_consumer(new_type_instance: MyNewType):
assert isinstance(new_type_instance, type_2_with_super_attribute)
print(new_type_instance.super_attribute)
MYPY will throw the error.
Upvotes: -1
Reputation: 7569
This isn't what a TypeVar
is usually used for.
The following function is a good example of the kind of function that a TypeVar
is typically used for:
def baz(obj):
return obj
This function will work with an argument of any type, so one solution for annotating this function could be to use typing.Any
, like so:
from typing import Any
def baz(obj: Any) -> Any:
return obj
This isn't great, however. We generally should use Any
as a last resort only, as it doesn't give the type-checker any information about the variables in our code. Lots of potential bugs will fly under the radar if we use Any
too liberally, as the type-checker will essentially give up, and not check that portion of our code.
In this situation, there's a lot more information that we can feed to the type-checker. We don't know what the type of the input argument will be, and we don't know what the return type will be, but we do know that the input type and the return type will be the same, whatever they are. We can show this kind of relationship between types — a type-dependent relationship — by using a TypeVar
:
from typing import TypeVar
T = TypeVar('T')
def baz(obj: T) -> T:
return obj
We can also use TypeVar
s in similar, but more complex, situations. Consider this function, which will accept a sequence of any type, and construct a dictionary using that sequence:
def bar(some_sequence):
return {some_sequence.index(elem): elem for elem in some_sequence}
We can annotate this function like this:
from typing import TypeVar, Sequence
V = TypeVar('V')
def bar(some_sequence: Sequence[V]) -> dict[int, V]:
return {some_sequence.index(elem): elem for elem in some_sequence}
Whatever the inferred type is of some_sequence
's elements, we can guarantee the values of the dictionary that is returned will be of the same type.
Bound TypeVar
s
Bound TypeVar
s are useful for when we have a function where we have some kind of type dependency like the above, but we want to narrow the types involved a little more. For example, imagine the following code:
class BreakfastFood:
pass
class Spam(BreakfastFood):
pass
class Bacon(BreakfastFood):
pass
def breakfast_selection(food):
if not isinstance(food, BreakfastFood):
raise TypeError("NO.")
# do some more stuff here
return food
In this code, we've got a type-dependency like in the previous examples, but there's an extra complication: the function will throw a TypeError
if the argument passed to it isn't an instance of — or an instance of a subclass of — the BreakfastFood
class. In order for this function to pass a type-checker, we need to constrain the TypeVar
we use to BreakfastFood
and its subclasses. We can do this by using the bound
keyword-argument:
from typing import TypeVar
class BreakfastFood:
pass
B = TypeVar('B', bound=BreakfastFood)
class Spam(BreakfastFood):
pass
class Bacon(BreakfastFood):
pass
def breakfast_selection(food: B) -> B:
if not isinstance(food, BreakfastFood):
raise TypeError("NO.")
# do some more stuff here
return food
What's going on in your code
If you annotate the obj
argument in your foo
function with an unbound TypeVar
, you're telling the type-checker that obj
could be of any type. But the type-checker correctly raises an error here: you've told it that obj
could be of any type, yet your function assumes that obj
has an attribute x
, and not all objects in python have x
attributes. By binding the T
TypeVar to instances of — and instances of subclasses of — MyClass
, we're telling the type-checker that the obj
argument should be an instance of MyClass
, or an instance of a subclass of MyClass
. All instances of MyClass
and its subclasses have x
attributes, so the type-checker is happy. Hooray!
However, your current function shouldn't really be using TypeVar
s at all, in my opinion, as there's no kind of type-dependency involved in your function's annotation. If you know that the obj
argument should be an instance of — or an instance of a subclass of — MyClass
, and there is no type-dependency in your annotations, then you can simply annotate your function directly with MyClass
:
class MyClass:
x: int = 10
def foo(obj: MyClass) -> None:
print(obj.x)
foo(MyClass())
If, on the other hand, obj
doesn't need to be an instance of — or an instance of a subclass of — MyClass, and in fact any class with an x
attribute will do, then you can use typing.Protocol
to specify this:
from typing import Protocol
class SupportsXAttr(Protocol):
x: int
class MyClass:
x: int = 10
def foo(obj: SupportsXAttr) -> None:
print(obj.x)
foo(MyClass())
Explaining typing.Protocol
fully is beyond the scope of this already-long answer, but here's a great blog post on it.
Upvotes: 13