Reputation: 10872
Take this function from Django Rest Framework source:
def method_overridden(method_name, klass, instance):
"""
Determine if a method has been overridden.
"""
method = getattr(klass, method_name)
default_method = getattr(method, '__func__', method) # Python 3 compat
return default_method is not getattr(instance, method_name).__func__
The function is intended to determine if a method has been overridden by a subclass and accepts a type klass
and an object instance
. In context, instance
is meant to be a subclass of klass
, however, the function only really cares that they both have a method method_name
.
So that example has led me to try to solve a more general case, how can you express an interdependence between arguments of a function such that one argument should be an instance of another argument that is a type, without actually caring what that type is? I tried this:
from typing import TypeVar
T = TypeVar("T")
def fn(t: type[T], i: T) -> None:
pass
fn(dict, "str") # no mypy error
Using a class, you can bind the type to the class:
from typing import Generic, TypeVar
class K(Generic[T]):
def meth(self, t: type[T], i: T) -> None:
pass
K[dict]().meth(dict, "str") # Argument 2 to "meth" of "K" has incompatible type "str"; expected "Dict[Any, Any]"
Thanks!
Upvotes: 1
Views: 282
Reputation: 5458
The problem is that mypy
infers T
in such a way that both arguments can match. We can check that by redefining fn
slightly:
def fn(t: type[T], i: T) -> T:
return i
reveal_type(fn(dict, "str")) # -> Collection[Any]
reveal_type(fn(int, "str")) # -> object
To solve this you can either force T
to be a specific type (like you did) or separate the arguments and force mypy
to first infer T
for one of the arguments.
def fn2(t: type[T]) -> Callable[[T], None]:
def inner(i: T) -> None:
pass
return inner
fn2(dict)("str") # error: Argument 1 has incompatible type "str"; expected "Dict[Any, Any]"
fn2(dict)({}) # no error
fn2(int)(True) # also no error, because `type` is covariant
Upvotes: 1