How to Implement Function Overloading with singledispatch decorator in Python
Python does not natively support function overloading: defining multiple versions of a function with different parameter types, as you might do in Java or C++. However, Python's functools module provides the @singledispatch decorator, which enables single-dispatch generic functions. This lets a function behave differently based on the type of its first argument, giving you a clean, Pythonic way to implement overloading.
This guide covers how to use @singledispatch, register implementations for different types, stack registrations, and inspect the dispatch mechanism.
What Is Single Dispatch?
Single dispatch means the function implementation is chosen based on the type of a single argument, specifically the first argument. The decorated function serves as the default (fallback) implementation, and you register specialized versions for specific types.
from functools import singledispatch
@singledispatch
def process(value):
"""Default implementation for unregistered types."""
print(f"Default: {value}")
print(process({"key": "value"}))
Output:
Default: {'key': 'value'}
None
Since no specific implementation is registered for dict, the default function is called.
Registering Type-Specific Implementations
Use the @function.register(type) decorator to add specialized behavior for specific types:
from functools import singledispatch
@singledispatch
def process(value):
"""Default implementation."""
print(f"Default: {value}")
@process.register(int)
def _process_int(value):
print(f"Integer: {value * 2}")
@process.register(str)
def _process_str(value):
print(f"String: '{value.upper()}'")
@process.register(list)
def _process_list(value):
for i, item in enumerate(value):
print(f" [{i}] {item}")
# Each call dispatches to the appropriate implementation
process(42)
process("hello")
process(["a", "b", "c"])
process(3.14) # No float registered; falls back to default
Output:
Integer: 84
String: 'HELLO'
[0] a
[1] b
[2] c
Default: 3.14
When you call process(x), Python checks the type of x against registered implementations:
- If an exact type match exists, it uses that implementation.
- If not, Python walks the type's Method Resolution Order (MRO) to find the closest registered parent type.
- If no match is found in the MRO, the default (
@singledispatch-decorated) function is used.
Registering Multiple Types for One Implementation
You can stack @register decorators to handle multiple types with the same implementation:
from functools import singledispatch
from decimal import Decimal
@singledispatch
def format_number(value):
print(f"Unsupported type: {type(value).__name__}")
@format_number.register(float)
@format_number.register(Decimal)
def _format_decimal(value):
print(f"Rounded: {round(value, 2)}")
format_number(3.14159)
format_number(Decimal("4.897"))
format_number(42) # Falls back to default
Output:
Rounded: 3.14
Rounded: 4.90
Unsupported type: int
Both float and Decimal types are handled by the same _format_decimal function.
Using Type Annotations (Python 3.7+)
Starting with Python 3.7, you can use type annotations instead of passing the type to register(), making the code cleaner:
from functools import singledispatch
@singledispatch
def describe(value):
print(f"Something: {value}")
@describe.register
def _describe_int(value: int):
print(f"An integer: {value}")
@describe.register
def _describe_str(value: str):
print(f"A string of length {len(value)}: '{value}'")
@describe.register
def _describe_list(value: list):
print(f"A list with {len(value)} items")
describe(42)
describe("hello")
describe([1, 2, 3])
describe(3.14)
Output:
An integer: 42
A string of length 5: 'hello'
A list with 3 items
Something: 3.14
The annotation-based syntax is recommended for Python 3.7+ as it is more readable and keeps the type information close to the parameter.
Inspecting the Dispatch Mechanism
singledispatch provides tools to inspect how dispatch works at runtime.
Using .dispatch() to Check Which Implementation Is Selected
The .dispatch(type) method returns the function that would be called for a given type:
from functools import singledispatch
@singledispatch
def process(value):
print(f"Default: {value}")
@process.register(int)
def _process_int(value):
print(f"Integer: {value}")
@process.register(list)
def _process_list(value):
print(f"List: {value}")
# Check which function handles each type
print(process.dispatch(int)) # → _process_int
print(process.dispatch(list)) # → _process_list
print(process.dispatch(dict)) # → process (default, no dict registered)
Output:
<function _process_int at 0x7cda41a7c680>
<function _process_list at 0x7cda41a7c720>
<function process at 0x7cda41a34220>
Using .registry to View All Registered Types
The .registry attribute is a read-only dictionary mapping types to their implementations:
print(process.registry.keys())
print(process.registry[int])
print(process.registry[object]) # The default implementation
Output:
dict_keys([<class 'object'>, <class 'int'>, <class 'list'>])
<function _process_int at 0x7eec955a0680>
<function process at 0x7eec95558220>
The default implementation is registered under object, since all Python types inherit from object.
Practical Example: Serializing Different Data Types
A realistic use case for singledispatch is building a custom serializer that converts different Python types to strings:
from functools import singledispatch
from datetime import datetime, date
@singledispatch
def serialize(value):
"""Default: convert to string."""
return str(value)
@serialize.register
def _serialize_str(value: str):
return f'"{value}"'
@serialize.register
def _serialize_bool(value: bool):
return "true" if value else "false"
@serialize.register
def _serialize_int(value: int):
return str(value)
@serialize.register
def _serialize_list(value: list):
items = ", ".join(serialize(item) for item in value)
return f"[{items}]"
@serialize.register
def _serialize_dict(value: dict):
pairs = ", ".join(f"{serialize(k)}: {serialize(v)}" for k, v in value.items())
return f"{{{pairs}}}"
@serialize.register
def _serialize_datetime(value: datetime):
return f'"{value.isoformat()}"'
# Test with various types
print(serialize("hello"))
print(serialize(42))
print(serialize(True))
print(serialize([1, "two", 3]))
print(serialize({"name": "Alice", "age": 30}))
print(serialize(datetime.now()))
Output:
"hello"
42
true
[1, "two", 3]
{"name": "Alice", "age": 30}
"2026-02-15T09:50:39.374720"
bool and int orderingIn Python, bool is a subclass of int. If you register handlers for both, the bool handler must be registered to ensure booleans are not caught by the int handler:
# Without a bool handler, True dispatches to the int handler
print(serialize(True)) # Would print "1" instead of "true"
singledispatch correctly handles this through MRO; it matches bool before int because bool is more specific. However, the bool handler must be registered for this to work.
singledispatchmethod for Class Methods (Python 3.8+)
Starting with Python 3.8, functools.singledispatchmethod brings single dispatch to class methods:
from functools import singledispatchmethod
class Formatter:
@singledispatchmethod
def format(self, value):
return f"Unknown: {value}"
@format.register
def _format_int(self, value: int):
return f"Integer: {value:,}"
@format.register
def _format_str(self, value: str):
return f"String: '{value}'"
fmt = Formatter()
print(fmt.format(1000000))
print(fmt.format("hello"))
print(fmt.format(3.14))
Output:
Integer: 1,000,000
String: 'hello'
Unknown: 3.14
Quick Reference
| Feature | Syntax | Description |
|---|---|---|
| Define default | @singledispatch | Base implementation for unregistered types |
| Register by type | @func.register(int) | Explicit type registration |
| Register by annotation | @func.register + type hint | Cleaner syntax (Python 3.7+) |
| Stack registrations | Multiple @func.register | One function handles multiple types |
| Check dispatch | func.dispatch(type) | See which implementation handles a type |
| View all registered | func.registry | Dictionary of all type → function mappings |
| Class methods | @singledispatchmethod | Single dispatch in classes (Python 3.8+) |
Conclusion
The @singledispatch decorator provides an elegant, Pythonic way to implement function overloading based on the type of the first argument.
- It encourages clean code architecture by separating type-specific logic into focused, registered functions while maintaining a sensible default fallback.
- For class-based designs,
@singledispatchmethodextends the same pattern to instance methods.
Whether you're building serializers, formatters, or type-specific processors, singledispatch keeps your code organized, extensible, and easy to maintain.