Overload Decorators: Multiple Function Signatures
Python does not support true function overloading like Java or C++, but the @overload decorator lets you define multiple type signatures for a single function. This enables mypy to infer precise return types based on input types, providing better IDE autocompletion and type checking. This article teaches you to use @overload to create context-aware function signatures.
Why Overload Matters
Without overload, a function that accepts multiple input types must have a generic return type:
from typing import Union
def process(value: Union[int, str]) -> Union[int, str]:
"""Process an int or str and return the same type."""
if isinstance(value, int):
return value * 2
return value.upper()
# mypy doesn't know the return type matches the input type
result = process(42) # mypy: result is Union[int, str], not int
if isinstance(result, int):
print(result + 10) # Have to narrow the type manually
With @overload, you can specify that the return type matches the input type:
from typing import overload, Union
@overload
def process(value: int) -> int: ...
@overload
def process(value: str) -> str: ...
def process(value: Union[int, str]) -> Union[int, str]:
"""Process an int or str and return the same type."""
if isinstance(value, int):
return value * 2
return value.upper()
# mypy now knows the exact return type
result_int = process(42) # mypy: int
result_str = process("hello") # mypy: str
# No manual type narrowing needed
print(result_int + 10) # Safe: mypy knows it's int
print(result_str.upper()) # Safe: mypy knows it's str
mypy uses the overloads to determine the return type, making the code more type-safe and IDE-friendly.
Syntax: Defining Overloads
The pattern is:
- Define one or more
@overloadsignatures (stubs with...bodies) - Define the actual implementation (without
@overload)
from typing import overload
@overload
def convert(value: int) -> str: ...
@overload
def convert(value: str) -> int: ...
def convert(value): # Implementation uses Union type
"""Convert between int and str."""
if isinstance(value, int):
return str(value)
return int(value)
# Usage
s = convert(42) # mypy: str
i = convert("100") # mypy: int
Key rules:
- All
@overloadsignatures must have a docstring or...in the body (no actual code) - The implementation function (no
@overload) does the real work - mypy uses the overloads to determine types; it ignores the implementation signature
- At runtime, only the implementation exists;
@overloaddefinitions are erased
Overloads with Multiple Parameters
Overloads can specify different combinations of parameter types:
from typing import overload, Union
@overload
def parse(value: int, to_type: None) -> int: ...
@overload
def parse(value: str, to_type: None) -> str: ...
@overload
def parse(value: int, to_type: str) -> str: ...
@overload
def parse(value: str, to_type: int) -> int: ...
def parse(value: Union[int, str], to_type: Union[None, str, int] = None) -> Union[int, str]:
"""Parse and optionally convert a value."""
if to_type is None:
return value
if to_type == str:
return str(value)
if to_type == int:
return int(value)
return value
# Each overload specifies the return type based on inputs
result1: int = parse(42) # No conversion
result2: str = parse("hello") # No conversion
result3: str = parse(42, str) # Convert int to str
result4: int = parse("100", int) # Convert str to int
This is powerful when functions have different behavior depending on multiple inputs.
Overloads with Default Parameters and Optional Values
Overloads can express different signatures based on optional parameters:
from typing import overload, Optional
@overload
def fetch(url: str) -> str: ...
@overload
def fetch(url: str, timeout: int) -> Optional[str]: ...
@overload
def fetch(url: str, timeout: int, retries: int) -> Optional[str]: ...
def fetch(url: str, timeout: Optional[int] = None, retries: int = 0) -> Optional[str]:
"""Fetch a URL with optional timeout and retries."""
# Implementation omitted
return None
# mypy infers return types based on arguments
result1: str = fetch("http://example.com") # No timeout, returns str
result2: Optional[str] = fetch("http://example.com", 5) # With timeout, Optional[str]
result3: Optional[str] = fetch("http://example.com", 5, 3) # With retries, Optional[str]
Overloads with Literal Types
Combine @overload with Literal for even more precision:
from typing import overload, Literal, Union
@overload
def format_value(value: int, fmt: Literal["decimal"]) -> str: ...
@overload
def format_value(value: int, fmt: Literal["binary"]) -> str: ...
@overload
def format_value(value: int, fmt: Literal["hex"]) -> str: ...
def format_value(value: int, fmt: str) -> str:
"""Format an integer as decimal, binary, or hex."""
if fmt == "decimal":
return str(value)
elif fmt == "binary":
return bin(value)
elif fmt == "hex":
return hex(value)
return str(value)
# mypy confirms the format is one of the three Literal values
dec: str = format_value(42, "decimal") # OK
binary: str = format_value(42, "binary") # OK
result: str = format_value(42, "random") # mypy: error: Literal["random"] not supported
Practical Example: Flexible Data Converter
Here's a real-world example—a data converter with precise overload signatures:
from typing import overload, Union, Type, TypeVar, Any, cast
T = TypeVar("T")
@overload
def convert(data: str, target_type: Type[int]) -> int: ...
@overload
def convert(data: str, target_type: Type[float]) -> float: ...
@overload
def convert(data: str, target_type: Type[bool]) -> bool: ...
@overload
def convert(data: str, target_type: Type[list]) -> list: ...
@overload
def convert(data: Any, target_type: Type[T]) -> T: ...
def convert(data: Any, target_type: Type[T]) -> Union[int, float, bool, list, T]:
"""Convert data to a target type."""
if target_type is int:
return int(data)
elif target_type is float:
return float(data)
elif target_type is bool:
return bool(data)
elif target_type is list:
return list(data) if hasattr(data, "__iter__") else [data]
return cast(T, data)
# mypy infers exact return types
number: int = convert("42", int)
decimal: float = convert("3.14", float)
flag: bool = convert("true", bool)
items: list = convert("abc", list) # ["a", "b", "c"]
# Even with complex types
from typing import List
ids: List[int] = convert([1, 2, 3], List[int]) # Works with TypeVar overload
Common Pitfalls with Overloads
Pitfall 1: Missing or incorrect implementation
from typing import overload
@overload
def func(x: int) -> str: ...
@overload
def func(x: str) -> int: ...
# ERROR: no implementation!
# You MUST provide the actual function after overloads
Fix by adding the implementation:
@overload
def func(x: int) -> str: ...
@overload
def func(x: str) -> int: ...
def func(x): # Implementation (no @overload)
return str(x) if isinstance(x, int) else int(x)
Pitfall 2: Implementation signature too specific
@overload
def func(x: int) -> str: ...
@overload
def func(x: str) -> int: ...
def func(x: int) -> str: # ERROR: implementation must accept Union[int, str]
return str(x)
Fix by using Union in the implementation:
from typing import Union
def func(x: Union[int, str]) -> Union[str, int]:
if isinstance(x, int):
return str(x)
return int(x)
Pitfall 3: Overlapping overloads
@overload
def func(x: int) -> str: ...
@overload
def func(x: int) -> int: ... # ERROR: duplicate signature
mypy requires each overload to be distinct. Remove duplicates or add a distinguishing parameter.
Key Takeaways
@overloaddefines multiple type signatures for a single function- Each
@overloadis a stub with...in the body; the actual implementation follows - mypy uses overloads to infer precise return types based on input types
- Overloads with
Literaltypes provide maximum precision for string/enum-based behavior - The implementation function signature must be general (Union) enough to accept all overload cases
- At runtime, only the implementation exists;
@overloaddefinitions are erased by Python - Use overloads to improve IDE autocompletion and catch type errors at the call site
Frequently Asked Questions
Do I have to use Union in the implementation signature?
Yes. The implementation must accept all types from all overloads. Use Union[type1, type2] or modern union syntax type1 | type2 to make it general enough.
Can I use TypeVar with overload?
Yes, but carefully. Overloads are best for specific type mappings; TypeVar is better for generic preserving constraints. Use both when needed: @overload def func(x: List[T]) -> T: ....
Does @overload have a runtime cost?
No. The @overload decorator is a typing-only construct. At runtime, the decorator definitions are removed and only the implementation exists.
How many overloads should I define?
Keep overloads to a reasonable number (typically 2-5). If you need many, consider whether the function is doing too much or if a generic approach would be clearer.
Can I use overload with class methods?
Yes: methods, static methods, and class methods all work with @overload. Place @overload before the method definition, and place the implementation as the last method without @overload.
Further Reading
- Python
typing.overloadDocumentation — Official reference and examples - PEP 484: Function Overloading — Full specification for overload
- mypy Overloads Guide — Detailed mypy behavior and edge cases
- Real Python: Function Overloading — Practical examples and patterns