Skip to main content

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:

  1. Define one or more @overload signatures (stubs with ... bodies)
  2. 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 @overload signatures 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; @overload definitions 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

  • @overload defines multiple type signatures for a single function
  • Each @overload is a stub with ... in the body; the actual implementation follows
  • mypy uses overloads to infer precise return types based on input types
  • Overloads with Literal types 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; @overload definitions 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