pythonpython-typing

Python type hinting for value that could also be a callable


I was recently trying to design a function that takes an optional default argument -- either a value or a callable that returns a value.

I wrote this method to do so:

T = TypeVar("T")


def default_example(default: T | Callable[[], T] | None) -> T:
    # stuff
    if default is not None:
        return default() if callable(default) else default
    # more stuff

However, my type hinter (pylance) is understandably upset because the type T itself could also be callable and its return type is not bound. Is there a way to modify this function to achieve what I want (value or callable or none) without using overloads or bounding the type variable?


Solution

  • I think the standard way to achieve this is to do like dataclasses.field does:

    The parameters to field() are:

    • default: If provided, this will be the default value for this field. This is needed because the field() call itself replaces the normal position of the default value.

    • default_factory: If provided, it must be a zero-argument callable that will be called when a default value is needed for this field. Among other purposes, this can be used to specify fields with mutable default values, as discussed below. It is an error to specify both default and default_factory.

    So your function should look something like this:

    T = TypeVar("T")
    
    
    def default_example(
            default: T | None = None,
            *,
            default_factory: Callable[[], T] | None = None
            ) -> T:
        # stuff
        if default is not None and default_factory is None:
            return default
        elif default is None and default_factory is not None:
            return default_factory()
        else:
            raise("must provide exactly one of `default`, `default_factory`")
    
    default_example(3)  # 3
    default_example(int)  # the `int` built-in function
    default_example(default_factory=int)  # 0, as it is the output of `int()`
    

    Be careful, if you want your default to be able to have the value None, you'll have to work a bit harder, once again you can look what they did in dataclasses (they created a custom MISSING type/value).

    Also note that the T = TypeVar("T") is no longer needed in Python 3.12 thanks to the type-parameter syntax, although not all type checkers accept this yet.

    def default_example[T](
            *,
            default: T | None = None,
            default_factory: Callable[[], T] | None = None
            ) -> T:
        # your stuff
        ...