I do not understand error like
Type variable "T" is unbound
Here a concrete example:
from typing import Dict, Type, Callable, TypeVar
class Sup: ...
class A(Sup): ...
class B(Sup): ...
def get_A(a: A) -> int:
return 1
def get_B(b: B) -> int:
return 2
T = TypeVar("T", bound=Sup)
f: Dict[Type[T], Callable[[T], int]] = {A: get_A, B: get_B}
mypy returns:
error: Type variable "foo.T" is unbound [valid-type]
note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class)
note: (Hint: Use "T" in function signature to bind "T" inside a function)
error: Dict entry 0 has incompatible type "type[A]": "Callable[[A], int]"; expected "type[T?]": "Callable[[T], int]" [dict-item]
error: Dict entry 1 has incompatible type "type[B]": "Callable[[B], int]"; expected "type[T?]": "Callable[[T], int]" [dict-item]
My understanding (which may be incorrect), is that mypy complains because it can not guarantee that later on, no tuple (key, value) not following the constrain on T will be added. For example:
# not a subclass of Sup !
class D: ...
def get_D(d: D)->int:
return 2
f[D] = get_D # D is not a subclass of Sup, so violation of the type of f
Question:
is my understanding of the error correct ? If not, could an explanation be provided ?
if my understanding is correct, why would the error occur here:
f: Dict[Type[T], Callable[[T], int]] = {A: get_A, B: get_B}
and not here ?
# This is the line not respecting the type of f !
f[D] = get_D
First of all, what does "Type variable "T" is unbound" mean? Let's clarify things, since it has nothing to do with its bound
argument.
When you use a type variable, you assign it some scope. This is not the runtime scope (global) where you define a T = TypeVar('T')
. In a generic class, the variable is bound by Generic[T]
(or Protocol[T]
, or some other class that is already generic) base class: when you use this form, you can use this type variable within the class body, so it's class-scoped. In a generic function, the variable is bound merely by inclusion into signature and is function-scoped. Here's another snippet that produces the same error (playground):
from typing import TypeVar
_T = TypeVar("_T")
def fn() -> int:
x: _T = 2
return 1
This error does not relate to such complicated matters as you consider: you are simply not allowed to use a type variable that is not assigned a scope (bound).
Only generic functions and classes bind type vars for their bodies, and also a type alias can use free type vars in its right-hand side (strictly speaking, that isn't "use in annotation" since that part is a plain runtime assignment).
This should be more clear with PEP695 generic syntax (I'm not a big fan of it, but can't argue it makes scoping more clear): you can declare a type var for function, for class and for a type alias. You cannot declare a type var for anything else.
def fn[T](x: T) -> T:
y: T = x
# You can use T here
class Foo[T]:
foo: T # You can use T here
def __init__(self, x: T) -> None:
y: T = x # And here (class-scoped, but also present in the func)
def foo(self) -> None:
y: T = self.foo # But also here - you already "allocated" T for the class
type Bar[T] = dict[T, T]
# All three T's above are different
# And you can't use T here
x: T # Typechecker error!
Note how the type variable is created for some scope (function/class/type alias). You couldn't rewrite your example using 3.12 syntax - and no, it isn't because new syntax isn't powerful enough. That just isn't supported by python's type system.
if my understanding is correct, why would the error occur here:
f: Dict[Type[T], Callable[[T], int]] = {A: get_A, B: get_B}
Because it's the place where you use a type variable not bound to current scope.
Because of scoping! Consider your idea (simplified - callable is irrelevant, right?):
T = TypeVar("T")
x: dict[T, T]
If I understand correctly, you want it to be interpreted like this (forgetting about all other methods, see typeshed for a wall of them):
_K = TypeVar("_K")
_V = TypeVar("_V")
class dict(Generic[_K, _V]):
def __getitem__(self, key: _K, /) -> _V: ...
def __delitem__(self, key: _K, /) -> None: ...
# dict[T, T] is equivalent to substituting T for both _K and _V
class YourDict(Generic[T]):
def __getitem__(self, key: T, /) -> T: ...
def __delitem__(self, key: T, /) -> None: ...
So YourDict
is something equivalent to dict[T, T]
using your syntax.
Let's unwrap this: T
in YourDict
is class scoped. It is the same for all places where it's mentioned inside the class body. At this point, type checkers can forget about the fact that __getitem__
could also be a generic function outside of class context, because it cannot "own" that T. Compare with __delitem__
where T
would make no sense at all shall we "forget" about class binding. So, there could be YourDict[int]
or YourDict[str]
- types that have __getitem__
accepting and returning int
or str
, resp., same as dict[int, int]
.
There is no way to express "substitute generic's typevar with another typevar, but only method-scoped" - mostly because this use case is pretty niche and semantics would be terribly difficult to define (what should happen to methods with only one typevar reference - __delitem__
above?).
You might be inspired by this example similar to one from PEP484 (references below):
T = TypeVar('T')
S = TypeVar('S')
class Foo(Generic[T]):
def __init__(self, x: T) -> None:
self._x = x
def method(self, x: T, y: S) -> S: ...
x = Foo(1) # Foo[int]
y = x.method(0, "abc") # inferred type of y is str