I recently ended up with a function that looks a bit like this (most of the logic was removed for sake of example):
def build(record, **kwargs): # Pretend we're doing stuff with the **kwargs here.. return record(**kwargs)
Where record could be one of several classes. All we care about is, the function accepts a type as first argument, and returns an instance of that type.
At first, the most logical way to describe this to mypy was to use a TypeVar like this:
from typing import Any, TypeVar, Type T = TypeVar('T') def build(record: Type[T], **kwargs: Any) -> T: return record(**kwargs)
Unfortunately, mypy disagrees with us:
% mypy foo.py foo.py: note: In function "build": foo.py:7: error: Too many arguments for "object"
Apparently, if the typevar is used directly, it will straight typecheck its call against the object constructor, rather than waiting for an actual call and get the type.
Not sure if this is actually intentional, or just something missing from mypy (I would expect it to work), but this lead me to find an alternative approach.
If you think about it, another way to see the typing of that function is:
Accept a callable as first argument, return that callable's return value
In that case, type definition would be as follows:
from typing import Any, Callable, NamedTuple, TypeVar T = TypeVar('T') def build(record: Callable[..., T], **kwargs: Any) -> T: return record(**kwargs)
Testing if it worked
To check if it worked, and typechecking is able to catch incorrect usage, I wrote a small test script.
First, I declared a few callables to be passed in as record:
FooTuple = NamedTuple('FooTuple', [ ('foo', int), ('bar', int), ]) class FooClass: spam = None # type: str eggs = None # type: str def __init__(self, spam: str, eggs: str) -> None: self.spam = spam self.eggs = eggs def foofunc(a: int, b: int) -> int: return a + b
And a bunch of functions that expect a specific type:
def accept_tuple(arg: FooTuple) -> None: pass def accept_class(arg: FooClass) -> None: pass def accept_int(arg: int) -> None: pass def accept_str(arg: str) -> None: pass
This now will happily pass type checking:
accept_tuple(build(FooTuple, foo=123, bar=456)) accept_class(build(FooClass, spam='SPAM', eggs='EGGS')) accept_int(build(foofunc, a=1, b=2))
While this won't:
accept_class(build(FooTuple, foo=123, bar=456)) accept_tuple(build(FooClass, spam='SPAM', eggs='EGGS')) accept_str(build(foofunc, a=1, b=2))
foo.py: note: In function "main": foo.py:55: error: Argument 1 to "accept_class" has incompatible type "FooTuple"; expected "FooClass" foo.py:56: error: Argument 1 to "accept_tuple" has incompatible type "FooClass"; expected "FooTuple" foo.py:57: error: Argument 1 to "accept_str" has incompatible type "int"; expected "str"
Mypy still have problems figuring out the type of varargs / kwargs, and as such it's currently unable to spot the type errors here:
build(FooTuple, invalid='SOMETHING') build(FooClass, spam=123, eggs=456) build(foofunc, a='hello', invalid='foobar')
(no error was reported, as of mypy 0.4.6)
Even if you cannot have it on by default when checking your whole codebase (because of legacy unannotated code), it's usually helpful to add the --disallow-untyped-calls and --disallow-untyped-defs flags when calling mypy.
Especially I find it useful as apparently I keep forgetting that mypy will not typecheck a function without an explicit return type, even if that's just None.
For example, mypy won't catch the type error here:
def hello(a: int) -> int: return a def main(): hello('not an int')
But it definitely does if we remember to specify the function's return type explicitly:
def main() -> None: hello('not an int')
% mypy bar.py bar.py: note: In function "main": bar.py:6: error: Argument 1 to "hello" has incompatible type "str"; expected "int"
If we had run the first example with --disallow-untyped-calls --disallow-untyped-defs, we would have definitely spotted earlier that something was wrong:
% mypy --disallow-untyped-calls --disallow-untyped-defs bar.py bar.py: note: In function "main": bar.py:5: error: Function is missing a type annotation