Many real APIs (open, dict.get, parsers) return different types depending on arguments.
Some functions return different types depending on their arguments. open(path, "rb") returns a binary file and open(path, "r") a text file. A union return type would force every caller to narrow. @overload describes each case precisely:
@overload
def load(raw: str) -> dict[str, Any]: ...
@overload
def load(raw: bytes, *, binary: Literal[True]) -> bytes: ...
def load(raw, *, binary=False):
... # the real implementationRules:
- Write two or more
@overload-decorated signatures with...bodies, followed by exactly one implementation without@overload. - The checker uses the overloads for callers and checks the implementation against all of them loosely; the implementation signature itself is not visible to callers.
- Overloads are tried in order; the first match wins, so put specific cases first.
- Combining with
Literalmakes return types depend on flag values.
At runtime, the overload definitions are replaced by the final implementation; typing.get_overloads(func) (3.11+) retrieves them for introspection.
Don't use overloads when a generic type variable expresses the relationship; def f[T](x: T) -> T is simpler than an overload per type.
Example
from typing import Literal, overload, get_overloads
@overload
def fetch(key: str, *, many: Literal[False] = False) -> str: ...
@overload
def fetch(key: str, *, many: Literal[True]) -> list[str]: ...
def fetch(key: str, *, many: bool = False) -> str | list[str]:
values = [f"{key}-1", f"{key}-2"]
return values if many else values[0]
one = fetch("user") # checker: str
many = fetch("user", many=True) # checker: list[str]
print(one.upper(), len(many)) # USER-1 2
print(len(get_overloads(fetch))) # 2Remember
- @overload lists the precise signatures callers see
- Exactly one non-decorated implementation must follow
- Literal flags let return types depend on argument values
Watch out for
- Putting a general overload before a specific one, so the specific case is never selected.