Explain Codes LogoExplain Codes Logo

How can I specify the function type in my type hints?

python
type-hints
callable
protocol
Alex KataevbyAlex Kataev·Aug 7, 2024
TLDR

To type hint your Python functions, use the typing.Callable class:

from typing import Callable def execute(callback: Callable[[int, str], float]) -> None: result = callback(42, "example") # result is unforeseeable... Just like my ex. It's a float

The Callable[[Arg1Type, Arg2Type], ReturnType] class defines the callback function to accept an int and a str, and return a float.

Upgrade your Callable game

The basic usage of Callable is intuitive: each argument type in the square brackets, and return type, right? But sometimes, you might need something fancier.

For instance, Python 3.8 introduced Protocol from typing to allow specification of more sophisticated function arguments:

from typing import Protocol, runtime_checkable @runtime_checkable class SupportsClose(Protocol): def close(self, softly: bool = True) -> None: ... def close_resource(resource: SupportsClose) -> None: # Function expects a 'close' method. Handle with care! resource.close()

Above, any object passed to close_resource should have a close method. Much like all of us need a close method. Hers was just too abrupt...

Space-saving with variadic generics

Python 3.9+ further ups the game introducing variadic generics. This way, you can specify callbacks with any number of arguments. All using the well-named ParamSpec object:

from typing import Callable, ParamSpec P = ParamSpec('P') # Introducing the ParamSpec def log_and_call(func: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: # ParamSpec in action. Don't tell it's secret identity! print(f"Calling {func.__name__}") return func(*args, **kwargs)

The joys and woes of Type Hints

In the wild world of Python, functions come in different shapes and sizes. Here's how you can apply type hints in different situations:

Anonymous but useful: Lambdas

For lambdas or functions sans clear signature, Callable[..., ReturnType] is your catch-all:

def run_lambda(func: Callable[..., int]) -> int: # Anonymous, but carries a gallon of power... just like Batman. return func()

Your functions need band-aids? Go partial!

When leveraging functools.partial, remember to tailor your Callable to reflect the remaining arguments:

from functools import partial def multiply(x: int, y: int) -> int: return x * y # Type hint takes only ONE integer. Because two's a crowd. partially_applied: Callable[[int], int] = partial(multiply, 2)

For the minimalists: Callable without imports

Avoid Callable to keep it simple. Granted, it's like shooting in the dark, but at least you didn't have to import anything:

def run(func: callable) -> None: # Just nod and smile! All we know is that 'func' is callable. func()

For the curious minds: Function introspection

Determine the function's type via introspection using type(), and you can specify type hints without guessing:

def is_function(obj: object) -> bool: # If I remove a function's docstring, is it still a function? return type(obj) is type(is_function)