How to type-overload functions with multiple optional args?

Viewed 260

I have a function with multiple kwargs with defaults. One of them (in the middle somewhere) is a boolean toggle that controls the return type.

I would like to create two overloads for this method with Literal[True/False] but keeping the default value.

My idea was the following:

from typing import overload, Literal

@overload
def x(a: int = 5, t: Literal[True] = True, b: int = 5) -> int: ...

@overload
def x(a: int = 5, t: Literal[False] = False, b: int = 5) -> str: ...

def x(a: int = 5, t: bool = True, b: int = 5) -> int | str:
    if t:
        return 5
    return "asd"

But mypy raises:

error: Overloaded function signatures 1 and 2 overlap with incompatible return types

I assume that is because x() will conflict.

But I cannot remove the default = False value in the second overload since it is preceded by arg a with a default.

How can I overload this properly such that

  • x() → int
  • x(t=True) → int
  • x(t=False) → str
1 Answers

It is an old problem. The reason is that you specify default value in both branches, so x() is possible in both and return type is undefined.

I have the following pattern for such cases:

from typing import overload, Literal

@overload
def x(a: int = 5, t: Literal[True] = True, b: int = 5) -> int: ...

@overload
def x(a: int = 5, *, t: Literal[False], b: int = 5) -> str: ...

@overload
def x(a: int, t: Literal[False], b: int = 5) -> str: ...

def x(a: int = 5, t: bool = True, b: int = 5) -> int | str:
    if t:
        return 5
    return "asd"

Why and how? You have to think about ways to call your function. First, you can provide a, then t can be given as kwarg (#2) or arg (#3). You can also leave a default, then t is always a kwarg (#2 again). This is needed to prevent putting arg after kwarg, which is SyntaxError. Overloading on more than one parameter is more difficult, but possible this way too:

@overload
def f(a: int = 1, b: Literal[True] = True, c: Literal[True] = True) -> int: ...

@overload
def f(a: int = 1, *, b: Literal[False], c: Literal[True] = True) -> Literal['True']: ...

@overload
def f(a: int = 1, *, b: Literal[False], c: Literal[False]) -> Literal['False']: ...

@overload
def f(a: int, b: Literal[False], c: Literal[True] = True) -> Literal['True']: ...

@overload
def f(a: int, b: Literal[False], c: Literal[False]) -> Literal['False']: ...

def f(a: int = 1, b: bool = True, c: bool = True) -> int | Literal['True', 'False']:
    return a if b else ('True' if c else 'False')  # mypy doesn't like str(c)

You can play with overloading here.

Related