When passing around functions, I normally type hint them with typing.Callable
.
The docs for collections.abc.Callable
state that it has four dunder methods:
class collections.abc.Callable
ABCs for classes that provide respectively the methods
__contains__()
,__hash__()
,__len__()
, and__call__()
.
At one point, I want to check if there is a __wrapped__
attribute on a function. This works fine at runtime via a check with hasattr(func, "__wrapped__")
.
When static type checking with mypy
, it reports: error: "Callable[..., Any]" has no attribute "__wrapped__" [attr-defined]
. This makes sense to me, as Callable
isn’t supposed to have a __wrapped__
attribute.
How can I properly type hint a Callable
with a __wrapped__
attribute? Is there some other type hint or workaround I can do?
Code Sample
I am using mypy==0.782
and Python==3.8.2
:
from functools import wraps
from typing import Callable
def print_int_arg(arg: int) -> None:
"""Print the integer argument."""
print(arg)
@wraps(print_int_arg)
def wrap_print_int_arg(arg: int) -> None:
print_int_arg(arg)
# do other stuff
def print_is_wrapped(func: Callable) -> None:
"""Print if a function is wrapped."""
if hasattr(func, "__wrapped__"):
# error: "Callable[..., Any]" has no attribute "__wrapped__" [attr-defined]
print(f"func named {func.__name__} wraps {func.__wrapped__.__name__}.")
print_is_wrapped(wrap_print_int_arg)
Advertisement
Answer
Obviously the easy answer is to add a # type: ignore
comment. However, this isn’t actually solving the problem, IMO.
I decided to make a type stub for a callable with a __wrapped__
attribute. Based on this answer, here is my current solution:
from typing import Callable, cast
class WrapsCallable:
"""Stub for a Callable with a __wrapped__ attribute."""
__wrapped__: Callable
__name__: str
def __call__(self, *args, **kwargs):
def print_is_wrapped(func: Callable) -> None:
"""Print if a function is wrapped."""
if hasattr(func, "__wrapped__"):
func = cast(WrapsCallable, func)
print(f"func named {func.__name__} wraps {func.__wrapped__.__name__}.")
And mypy
now reports Success: no issues found in 1 source file
.
I feel as if this is a lot of boiler-plate code, and would love a more streamlined answer.