Mypy – why does TypeVar not work without bound specified

Question:

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.

Asked By: Whistleroosh

||

Answers:

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 TypeVars 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 TypeVars

Bound TypeVars 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 TypeVars 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.

Answered By: Alex Waygood

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.

Answered By: BoHuang
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.