Search code examples
pythongenericslambdamypytype-alias

Binding a mypy type variable for a type alias in the type annotation of an assignment


from typing import Callable, TypeVar

T = TypeVar('T')

IndicatorFunction = Callable[[T], bool]

# mypy accepts this annotation.
s1: Callable[[T], bool] = lambda x: False

# mypy rejects this annotation, but it's just an alias of the first one.
s2: IndicatorFunction[T] = lambda x: False

I am trying to understand how to make mypy consider T to be bound in the type annotation for s2 in the above code snippet. I get the following error:

main.py:9: error: Type variable "__main__.T" is unbound
main.py:9: note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class)
main.py:9: note: (Hint: Use "T" in function signature to bind "T" inside a function)
Found 1 error in 1 file (checked 1 source file)

The hints helpfully tell me how to bind "T" inside a class and in a function signature, but not how to do so for the type annotation of a lambda expression.

Subquestions:

  • How do I bind T in the type annotation of s2?
  • Why do I not get the same error in the type annotation of s1? Since IndicatorFunction is an alias for s1's type, I expected both s1 and s2 to be accepted by mypy.

Solution

  • After reading a lot of PEPs and mypy's issue tracker, I believe I understand what's happening here.

    How do I bind T in the type annotation of s2?

    You can't. There is no syntax defined for it in PEP 526, which introduced variable annotations.

    As far as I can tell, and as implied by mypy's error message, there are only two ways to quantify (bind) a type variable T in Python as it stands today:

    1. In a class definition, by including either Generic[T] or Protocol[T] in the inheritance list (or another class that inherits from these two). In other words, Generic and Protocol are "magical" implicit type variable binders.
    2. In a function definition's parameter list, by annotating a parameter with a generic type that contains T as a type parameter.

    There is no way to introduce a type variable binding in a PEP 526 variable annotation. PEP 526 makes almost no mention of type parameters and generics.

    Will Python add a way to bind type variables in variable annotations in the future? It doesn't look like it. PEP 695, if accepted, will greatly improve the clarity of type variable binding syntax, but, again, it only treats the above two cases: introducing bindings in either class or function definitions, not in variable annotations.

    The next question implied by all this is why does mypy treat s1 as valid and implicitly binds T? You can see that mypy does implicitly bind T if you try to give it this code:

    T = TypeVar('T')
    
    s3: Callable[[T], bool] = lambda x: x + False
    

    mypy gives these errors:

    main.py:5: error: Incompatible types in assignment (expression has type "Callable[[T], int]", variable has type "Callable[[T], bool]")
    main.py:5: error: Unsupported operand types for + ("T" and "bool")
    main.py:5: error: Incompatible return value type (got "int", expected "bool")
    

    The second one, Unsupported operand types for + ("T" and "bool"), shows that mypy did bind T and infer that x is of type T.

    This appears to be a mypy bug. In fact, one of the first posted comments on that bug is a question about Callable and aliases of Callable being treated inconsistently by mypy.

    This answers the second question:

    Why do I not get the same error in the type annotation of s1? Since IndicatorFunction is an alias for s1's type, I expected both s1 and s2 to be accepted by mypy.

    It's the above mypy bug.

    In contrast, Pyright correctly rejects s1, complaining that Type variable "T" has no meaning in this context -- in other words, T is unbound.