I’m trying to perfectly type annotate the following Python function:
from typing import Callable, Any
import inspect
def foo(func: Callable[..., Any]) -> None:
if inspect.isclass(func):
raise ValueError
# Do something...
class Bad:
pass
def good() -> Bad:
return Bad()
foo(good) # OK
foo(Bad) # NOK
I would like to narrow down the Callable[..., Any] type so that it rejects Bad passed as argument.
See Mypy playground here.
I can’t find anything in the typing module nor the Mypy documentation to distinguish between a simple function and a class. Is this even possible?
It’s not clear why type(Bad) and type(good) are very distinct at run-time, but I can’t find the equivalent for type annotation.
>Solution :
As you may know, Callable is a kind of structural type, and so anything with the __call__ instance method (which includes all classes, because a class is an instance of type, and type.__call__ allows you to use the call syntax Bad()) will fulfil Callable.
Statically typing something to behave differently for a subset of a type is not fully supported in the Python typing system; you’ll face the same problems if you try, for example, to type something as "an integer but not 0". The closest you can get is making a @typing.overload with typing.Never, which says that foo will error at runtime, which is your implementation of foo anyway. Here, mypy will throw an error on the first line after the call site expected to fail at runtime (example given at mypy-playground, copied below):
from typing_extensions import Callable, Any, overload, Never
import inspect
@overload
def foo(func: type) -> Never: ...
@overload
def foo(func: Callable[..., Any]) -> None: ...
def foo(func: Callable[..., Any]) -> None:
if inspect.isclass(func):
raise ValueError
# Do something...
class Bad:
pass
def good() -> Bad:
return Bad()
foo(good) # OK
foo(Bad) # NOK
a = 3 # mypy: Statement is unreachable [unreachable]
This requires specifying the --warn-unreachable CLI option or warn_unreachable = True configuration file option to work.