Search code examples
pythonoverloadingpython-typing

Type-hinting an overloaded function when parameter default is determined by module level variable


I have a Python function with an input parameter, the value of which controls the type of the return value. The parameter may be omitted (default value None), in which case a module level variable is used. This way, the default behavior can be changed by changing the module level variable. A minimal example:

from typing import overload, Union, Literal, Optional

option_default: Literal["str", "int"] = "int"


@overload
def test(option: Literal["str"]) -> str:
    ...


@overload
def test(option: Literal["int"]) -> int:
    ...


@overload
def test(option: Literal[None] = None) -> Union[str, int]:
    ...


def test(option: Optional[Literal["str", "int"]] = None) -> Union[str, int]:

    if option is None:
        option = option_default

    if option == "str":
        return "foo"
    else:
        return 1


foo: int = test() # Incompatible types in assignment (expression has type "Union[str, int]", variable has type "int")

option_default = "str"
baz: str = test() # Incompatible types in assignment (expression has type "Union[str, int]", variable has type "str")

Normally one would use typing.overload to deal with this sort of situation. However, for test() without arguments that doesn't work--hence the mypy errors. Is there a better way to provide type info here?


Solution

  • That's not possible. There is a proposal on GitHub to add typeof from TypeScript, but that's a different thing - it doesn't track mutable variables like you've shown.

    I don't think such a feature can be implemented, even in theory. It brings a lot of new complexity into type checking. I can think of some problematic scenarios:

    1. Passing a callback
    def foo(callback: Callable[[], int]):
        some_module.definitely_returns_an_int = callback
    
    option_default = "int"
    foo(test)  # ok?
    option_default = "str"
    

    Now foo has saved a function that returns a string.

    1. Tracking mutations that happen inside functions
    def ints_are_the_bomb():
        global option_default
        option_default = "int"
    
    option_default = "str"
    some_other_module.baz()
    
    value = test()
    

    Can you be sure about the type of value? There's no guarantee that some_other_module.baz() didn't call your_module.ints_are_the_bomb(). So now you need to track all changes that can ever happen to your_module.option_default, potentially across modules. And if client code (if this is a library) can change the flag, then it's just impossible.

    To generalize, the type of a value (including functions) can't change when you mutate something. That could break distant code that also happens to have a reference to this object.