Contents
overloading.py
Function overloading for Python 3
overloading
is a module that provides function and method dispatching
based on the types and number of runtime arguments.
When an overloaded function is invoked, the dispatcher compares the supplied arguments to available signatures and calls the implementation providing the most accurate match:
@overload
def biggest(items: Iterable[int]):
return max(items)
@overload
def biggest(items: Iterable[str]):
return max(items, key=len)
>>> biggest([2, 0, 15, 8, 7])
15
>>> biggest(['a', 'abc', 'bc'])
'abc'
Features
- Function validation during registration and comprehensive resolution rules guarantee a well-defined outcome at invocation time.
- Supports the typing module introduced in Python 3.5.
- Supports optional parameters.
- Supports variadic signatures (
*args
and**kwargs
). - Supports class-/staticmethods.
- Evaluates both positional and keyword arguments.
- No dependencies beyond the standard library
Current release
overloading.py v0.5 was released on 16 April 2016.
Notable changes since v0.4
Breaking change notice: The treatment of classmethod
and staticmethod
has been harmonized with that of other decorators. They must now appear after the overloading directive.
- Added support for extended type hints as specified in PEP 484.
- Remaining restrictions on the use of abstract base classes have been lifted.
- The number of exact type matches is no longer a separate factor in function ranking.
- Optional parameters now accept an explicit
None
. - Multiple implementations may now declare
*args
, so the concept of a default implementation no longer exists. - Better decorator support
Installation
pip3 install overloading
To use extended type hints on Python versions prior to 3.5, install the typing module from PyPI:
pip3 install typing
Compatibility
The library is primarily targeted at Python versions 3.3 and above, but Python 3.2 is still supported for PyPy compatibility.
The Travis CI test suite covers CPython 3.3/3.4/3.5 and PyPy3.
Motivation
Why use overloading instead of *args / **kwargs?
- Reduces the need for mechanical argument validation.
- Promotes explicit call signatures, making for cleaner, more self-documenting code.
- Enables the use of full type declarations and type checking tools.
Consider:
class DB:
def get(self, *args):
if len(args) == 1 and isinstance(args[0], Query):
return self.get_by_query(args[0])
elif len(args) == 2:
return self.get_by_id(*args)
else:
raise TypeError(...)
def get_by_query(self, query):
...
def get_by_id(self, id, model):
...
The same thing with overloading:
class DB:
@overload
def get(self, query: Query):
...
@overload
def get(self, id, model):
...
Why use overloading instead of functions with distinct names?
Function names may not always be chosen freely; just consider
__init__()
or any other externally defined interface.Sometimes you just want to expose a single function, particularly when different names don’t add semantic value:
def feed(creature: Human): ... def feed(creature: Dog): ...
vs:
def feed_human(human): ... def feed_dog(dog): ...
Usage
Use the overload
decorator to register multiple implementations of a function. All variants must differ by parameter type or count.
Type declarations are specified as function annotations; see PEP 484.
from overloading import *
@overload
def div(r: Number, s: Number):
return r / s
@overload
def div(r: int, s: int):
return r // s
>>> div(3.0, 2)
1.5
>>> div(3, s=2)
1
>>> div(3, 'a')
TypeError: Invalid type or number of arguments when calling 'div'.
This simple example already demonstrates several key points:
- Subclass instances satisfy type requirements as one would expect.
- Abstract base classes are valid as type declarations.
- When a call matches multiple implementations, the most specific variant is chosen. Both of the above functions are applicable to
div(3, 2)
, but the latter is more specific. - Keyword arguments can be used as normal.
from overloading import *
imports three decorators: overload
, overloaded
, and overloads
. The last two are discussed in Syntax alternatives.
Here’s another example:
@overload
def f(x, *args):
return 1
@overload
def f(x, y, z):
return 2
@overload
def f(x, y, z=0):
return 3
>>> f(1, 2, 3)
2
>>> f(1, 2)
3
>>> f(1)
1
As shown here, type declarations are entirely optional. If a parameter has no expected type, then any value is accepted.
A detailed explanation of the resolution rules can be found in Function matching, but the system is intuitive enough that you can probably just start using it.
Complex types
Starting with v0.5.0, overloading.py has preliminary support for the extended typing elements implemented in the typing module, including parameterized generic collections:
@overload
def f(arg: Iterable[int]):
return 'an iterable of integers'
@overload
def f(arg: Tuple[Any, Any, Any]):
return 'a three-tuple'
>>> f((1, 2, 3, 4))
'an iterable of integers'
>>> f((1, 2, 3))
'a three-tuple'
Type hints can be arbitrarily complex, but the overloading mechanism ignores nested parameters. That is, Sequence[Tuple[int, int]]
will be simplified to Sequence[tuple]
internally. Union
does not count as a type in its own right, so parameterized containers inside a Union
are okay.
At invocation time, if the expected type is a fixed-length Tuple
, every element in the supplied tuple is type-checked. By contrast, type-constrained collections of arbitrary length are supposed to be homogeneous, so only one element in the supplied value is inspected (the first one if it’s a sequence).
Optional parameters
None
is not automatically considered an acceptable value for a parameter with a declared type. To extend a type constraint to include None
, the parameter can be designated as optional in one of two ways:
arg: X = None
arg: Optional[X]
(if using the typing module)
The difference is that in the latter case an explicit argument must still be provided. Optional
simply allows it to be None
as well as an instance of X
.
Syntax alternatives
The overload
syntax is really a shorthand for two more specialized decorators:
overloaded
declares a new overloaded function and registers the first implementationoverloads(f)
registers a subsequent implementation onf
.
The full syntax must be used when an implementation’s qualified name is not enough to identify the existing function it overloads. On Python 3.2, only the full syntax is available.
Classes
Everything works the same when overloading methods on classes. Classmethods and staticmethods are directly supported.
class C:
@overload
def __init__(self):
...
@overload
def __init__(self, length: int, default: Any):
...
@overload
@classmethod
def from_iterable(cls, things: Sequence):
...
@overload
@classmethod
def from_iterable(cls, things: Iterable, key: Callable):
...
When a subclass definition adds implementations to an overloaded function declared in a superclass, it is necessary to use the explicit overloads(...)
syntax to refer to the correct function:
class C:
@overload
def f(self, foo, bar):
...
class D(C):
@overloads(C.f) # Note `C.f` here.
def f(self, foo, bar, baz):
...
It is not yet possible to override an already registered signature in a subclass.
Decorators
Overloading directives may be combined with other decorators.
The rule is to always apply the other decorator before the resulting function is passed to the overloading apparatus. This means placing the custom decorator after the overloading directive:
@overload
@decorates_int_operation
def f(x: int):
...
@overload
@decorates_float_operation
def f(x: float):
...
Note
When writing decorators, remember to use functools.wraps()
so that the original function can be discovered.
Errors
OverloadingError
is raised if something goes wrong when registering a function.
TypeError
is raised if an overloaded function is called with arguments that don’t match any of the registered signatures.
Function matching
Although real-life use cases are typically quite straightforward, the function resolution algorithm is equipped to deal with any combination of simultaneously matching functions and rank them by match quality. A core goal of this library is to eliminate situations where a call to an overloaded function fails due to multiple implementations providing an equally good fit to a set of arguments.
In compiled languages, such ambiguous cases can often be identified and rejected at compile time. This possibility doesn’t exist in Python, leaving us with two options: either prepare to raise an ambiguous call exception on invocation, or devise a set of rules that guarantee the existence of a preferred match every time. overloading.py takes the latter approach.
Validation
Function signatures are validated on registration to ensure that a truly ambiguous situation cannot arise at invocation time. Specifically, the required regular parameters must form a unique signature. In addition, a catch-all parameter for positional arguments is considered a further identifying feature, allowing f(x)
and f(x, *args)
to coexist.
For example, attempting the following definitions will immediately raise an error:
@overload
def f(a: str, b: int, c: int = 100):
...
@overload
def f(a: str, b: int, c: str = None):
...
The reason should be obvious: were f
called with only the first two arguments, inferring the correct function would be impossible.
Resolution
The closest match between a set of arguments and a group of implementations is selected as follows:
- Identify candidate functions based on parameter count and names.
- Of those, identify applicable functions by comparing argument types to expected types.
If there are multiple matches, the rest of the rules are applied until a single function remains:
- Choose the function that accepts the most arguments to fill its regular parameter slots.
- Choose the function whose signature matches the most arguments due to specific type declarations (as opposed to arguments that match because any type is allowed).
- Choose the function that, in terms of parameter order, is the first to produce a unique most specific match.
- Choose the function that accepts the greatest number of required parameters.
- Choose the function that is of fixed arity (does not declare
*args
).
Note
Even though optional parameters are ignored when assessing signature uniqueness, they do matter at invocation time when the actual argument matching is carried out.
Note
The matching algorithm only examines regular parameters (those before *args
). While variable-argument parameters (*
and **
) and keyword-only parameters are allowed, arguments consumed by them don’t count towards match quality.
There are two kinds of situations where the algorithm may not produce an unequivocal winner:
The declared types for a particular parameter might include a group of abstract base classes with a type hierarchy that is either inconsistent or divided into multiple disjoint parts. This is mostly a theoretical concern.
A more practically relevant case arises when an empty container is passed to a function that is overloaded on the type parameter of a generic collection:
@overload
def f(x: Iterable[int]): ...
@overload
def f(x: Iterable[str]): ...
>>> f([])
Clearly, it is impossible to infer anything about the type of elements that aren’t there. However, since applying overloading to generics is potentially useful, such a setup is allowed.
The current solution is to fall back on the function definition order as a last resort: an earlier declaration takes precedence. In the future, a specialized type hint could be used to explicitly designate a function as a preferred handler for empty collections.
Apart from these special cases, function definition order is of no consequence to the dispatch logic.
Example: Argument subtyping
Consider a case like this:
@overload
def f(x: Iterable, y: Sequence):
...
@overload
def f(x: Sequence, y: Iterable):
...
>>> f([0, 1], [2, 3])
The call would seem to result in an equally explicit match with regard to both functions, and indeed many languages, such as Java, would reject such a call as ambiguous. The resolution rules address this by dictating that the first parameter to produce a ranking will determine the winner. Therefore, in this case, the second function would be chosen, as it provides a more specific match at the initial position. Intuitively, a list is closer to Sequence
than to Iterable
in the type hierarchy, even though it doesn’t really inherit from either.