Search code examples
pythonmypypython-typing

What determines the order of type variables when narrowing a generic type?


Note: this question refers to Python 3.12+

Suppose I have:

from typing import Any, TypeVar

import numpy as np


T = TypeVar("T")
U = TypeVar("U")
ListLike = T | list[T] | tuple[T, ...] | np.ndarray[Any, U]
ListLikeStr = ListLike[str, np.object_]
# ListLikeStr should be: str | list[str] | tuple[str, ...] | np.ndarray[Any, np.object_]

This works, but it was lucky. I could have instead written: ListLike[np.object_, str], and then I'd get ListLikeStr being np.object_ | list[np.object_] | tuple[np.object_, ...] | np.ndarray[Any, str], which is not what I'd like.

Ideally I could have done something like: ListLike[T=str, U=np.object_], but that does not work. So what determines the order when I am instantiating the type variables in ListLike? How does ListLike "know" that T corresponds with str and U with np.object_, when I write ListLike[str, np.object_]?


Solution

  • In a "traditional" type alias, whichever is referenced first goes first.

    PEP 695 type statements were created to fix this:

    (playground: Pyright)

    type A[U: str, T] = T | list[T] | tuple[T, U]
    
    a: A[int, str]  # error: "int" is not a subtype of "str"
    b: A[str, int]  # fine
    
    reveal_type(b)  # str | list[str] | tuple[int, str]
    

    Mypy have yet to add support for it, but you can import TypeAliasType from typing_extensions to achieve the same result (all Python versions):

    (playgrounds: Mypy, Pyright)

    from typing import TypeVar
    from typing_extensions import TypeAliasType
    
    T = TypeVar('T')
    U = TypeVar('U', bound = str)
    A = TypeAliasType('A', T | list[T] | tuple[T, U], type_params = (U, T))
    
    a: A[int, str]  # error: "int" is not a subtype of "str"
    b: A[str, int]  # fine
    
    reveal_type(b)  # int | list[int] | tuple[int, str]