Function overloading
Sometimes the arguments and types in a function depend on each otherin ways that can’t be captured with a Union
. For example, supposewe want to write a function that can accept x-y coordinates. If we passin just a single x-y coordinate, we return a ClickEvent
object. However,if we pass in two x-y coordinates, we return a DragEvent
object.
Our first attempt at writing this function might look like this:
- from typing import Union, Optional
- def mouse_event(x1: int,
- y1: int,
- x2: Optional[int] = None,
- y2: Optional[int] = None) -> Union[ClickEvent, DragEvent]:
- if x2 is None and y2 is None:
- return ClickEvent(x1, y1)
- elif x2 is not None and y2 is not None:
- return DragEvent(x1, y1, x2, y2)
- else:
- raise TypeError("Bad arguments")
While this function signature works, it’s too loose: it implies mouse_event
could return either object regardless of the number of argumentswe pass in. It also does not prohibit a caller from passing in the wrongnumber of ints: mypy would treat calls like mouse_event(1, 2, 20)
as beingvalid, for example.
We can do better by using overloadingwhich lets us give the same function multiple type annotations (signatures)to more accurately describe the function’s behavior:
- from typing import Union, overload
- # Overload *variants* for 'mouse_event'.
- # These variants give extra information to the type checker.
- # They are ignored at runtime.
- @overload
- def mouse_event(x1: int, y1: int) -> ClickEvent: ...
- @overload
- def mouse_event(x1: int, y1: int, x2: int, y2: int) -> DragEvent: ...
- # The actual *implementation* of 'mouse_event'.
- # The implementation contains the actual runtime logic.
- #
- # It may or may not have type hints. If it does, mypy
- # will check the body of the implementation against the
- # type hints.
- #
- # Mypy will also check and make sure the signature is
- # consistent with the provided variants.
- def mouse_event(x1: int,
- y1: int,
- x2: Optional[int] = None,
- y2: Optional[int] = None) -> Union[ClickEvent, DragEvent]:
- if x2 is None and y2 is None:
- return ClickEvent(x1, y1)
- elif x2 is not None and y2 is not None:
- return DragEvent(x1, y1, x2, y2)
- else:
- raise TypeError("Bad arguments")
This allows mypy to understand calls to mouse_event
much more precisely.For example, mypy will understand that mouse_event(5, 25)
willalways have a return type of ClickEvent
and will report errors forcalls like mouse_event(5, 25, 2)
.
As another example, suppose we want to write a custom container class thatimplements the getitem
method ([]
bracket indexing). If thismethod receives an integer we return a single item. If it receives aslice
, we return a Sequence
of items.
We can precisely encode this relationship between the argument and thereturn type by using overloads like so:
- from typing import Sequence, TypeVar, Union, overload
- T = TypeVar('T')
- class MyList(Sequence[T]):
- @overload
- def __getitem__(self, index: int) -> T: ...
- @overload
- def __getitem__(self, index: slice) -> Sequence[T]: ...
- def __getitem__(self, index: Union[int, slice]) -> Union[T, Sequence[T]]:
- if isinstance(index, int):
- # Return a T here
- elif isinstance(index, slice):
- # Return a sequence of Ts here
- else:
- raise TypeError(...)
Note
If you just need to constrain a type variable to certain types orsubtypes, you can use a value restriction.
Runtime behavior
An overloaded function must consist of two or more overload variants_followed by an _implementation. The variants and the implementationsmust be adjacent in the code: think of them as one indivisible unit.
The variant bodies must all be empty; only the implementation is allowedto contain code. This is because at runtime, the variants are completelyignored: they’re overridden by the final implementation function.
This means that an overloaded function is still an ordinary Pythonfunction! There is no automatic dispatch handling and you must manuallyhandle the different types in the implementation (e.g. by usingif
statements and isinstance
checks).
If you are adding an overload within a stub file, the implementationfunction should be omitted: stubs do not contain runtime logic.
Note
While we can leave the variant body empty using the pass
keyword,the more common convention is to instead use the ellipsis (…
) literal.
Type checking calls to overloads
When you call an overloaded function, mypy will infer the correct returntype by picking the best matching variant, after taking into considerationboth the argument types and arity. However, a call is never typechecked against the implementation. This is why mypy will report callslike mouse_event(5, 25, 3)
as being invalid even though it matches theimplementation signature.
If there are multiple equally good matching variants, mypy will selectthe variant that was defined first. For example, consider the followingprogram:
- from typing import List, overload
- @overload
- def summarize(data: List[int]) -> float: ...
- @overload
- def summarize(data: List[str]) -> str: ...
- def summarize(data):
- if not data:
- return 0.0
- elif isinstance(data[0], int):
- # Do int specific code
- else:
- # Do str-specific code
- # What is the type of 'output'? float or str?
- output = summarize([])
The summarize([])
call matches both variants: an empty list couldbe either a List[int]
or a List[str]
. In this case, mypywill break the tie by picking the first matching variant: output
will have an inferred type of float
. The implementor is responsiblefor making sure summarize
breaks ties in the same way at runtime.
However, there are two exceptions to the “pick the first match” rule.First, if multiple variants match due to an argument being of typeAny
, mypy will make the inferred type also be Any
:
- dynamic_var: Any = some_dynamic_function()
- # output2 is of type 'Any'
- output2 = summarize(dynamic_var)
Second, if multiple variants match due to one or more of the argumentsbeing a union, mypy will make the inferred type be the union of thematching variant returns:
- some_list: Union[List[int], List[str]]
- # output3 is of type 'Union[float, str]'
- output3 = summarize(some_list)
Note
Due to the “pick the first match” rule, changing the order of youroverload variants can change how mypy type checks your program.
To minimize potential issues, we recommend that you:
- Make sure your overload variants are listed in the same order asthe runtime checks (e.g.
isinstance
checks) in your implementation. - Order your variants and runtime checks from most to least specific.(See the following section for an example).
Type checking the variants
Mypy will perform several checks on your overload variant definitionsto ensure they behave as expected. First, mypy will check and make surethat no overload variant is shadowing a subsequent one. For example,consider the following function which adds together two Expression
objects, and contains a special-case to handle receiving two Literal
types:
- from typing import overload, Union
- class Expression:
- # ...snip...
- class Literal(Expression):
- # ...snip...
- # Warning -- the first overload variant shadows the second!
- @overload
- def add(left: Expression, right: Expression) -> Expression: ...
- @overload
- def add(left: Literal, right: Literal) -> Literal: ...
- def add(left: Expression, right: Expression) -> Expression:
- # ...snip...
While this code snippet is technically type-safe, it does contain ananti-pattern: the second variant will never be selected! If we try callingadd(Literal(3), Literal(4))
, mypy will always pick the first variantand evaluate the function call to be of type Expression
, not Literal
.This is because Literal
is a subtype of Expression
, which meansthe “pick the first match” rule will always halt after considering thefirst overload.
Because having an overload variant that can never be matched is almostcertainly a mistake, mypy will report an error. To fix the error, we caneither 1) delete the second overload or 2) swap the order of the overloads:
- # Everything is ok now -- the variants are correctly ordered
- # from most to least specific.
- @overload
- def add(left: Literal, right: Literal) -> Literal: ...
- @overload
- def add(left: Expression, right: Expression) -> Expression: ...
- def add(left: Expression, right: Expression) -> Expression:
- # ...snip...
Mypy will also type check the different variants and flag any overloadsthat have inherently unsafely overlapping variants. For example, considerthe following unsafe overload definition:
- from typing import overload, Union
- @overload
- def unsafe_func(x: int) -> int: ...
- @overload
- def unsafe_func(x: object) -> str: ...
- def unsafe_func(x: object) -> Union[int, str]:
- if isinstance(x, int):
- return 42
- else:
- return "some string"
On the surface, this function definition appears to be fine. However, it willresult in a discrepancy between the inferred type and the actual runtime typewhen we try using it like so:
- some_obj: object = 42
- unsafe_func(some_obj) + " danger danger" # Type checks, yet crashes at runtime!
Since some_obj
is of type object
, mypy will decide that unsafe_func
must return something of type str
and concludes the above will type check.But in reality, unsafe_func
will return an int, causing the code to crashat runtime!
To prevent these kinds of issues, mypy will detect and prohibit inherently unsafelyoverlapping overloads on a best-effort basis. Two variants are considered unsafelyoverlapping when both of the following are true:
- All of the arguments of the first variant are compatible with the second.
- The return type of the first variant is not compatible with (e.g. is not asubtype of) the second.
So in this example, the
int
argument in the first variant is a subtype oftheobject
argument in the second, yet theint
return type is not a subtype ofstr
. Both conditions are true, so mypy will correctly flagunsafe_func
asbeing unsafe.
However, mypy will not detect all unsafe uses of overloads. For example,suppose we modify the above snippet so it calls summarize
instead ofunsafe_func
:
- some_list: List[str] = []
- summarize(some_list) + "danger danger" # Type safe, yet crashes at runtime!
We run into a similar issue here. This program type checks if we look just at theannotations on the overloads. But since summarize(…)
is designed to be biasedtowards returning a float when it receives an empty list, this program will actuallycrash during runtime.
The reason mypy does not flag definitions like summarize
as being potentiallyunsafe is because if it did, it would be extremely difficult to write a safeoverload. For example, suppose we define an overload with two variants that accepttypes A
and B
respectively. Even if those two types were completely unrelated,the user could still potentially trigger a runtime error similar to the ones above bypassing in a value of some third type C
that inherits from both A
and B
.
Thankfully, these types of situations are relatively rare. What this does mean,however, is that you should exercise caution when designing or using an overloadedfunction that can potentially receive values that are an instance of two seeminglyunrelated types.
Type checking the implementation
The body of an implementation is type-checked against thetype hints provided on the implementation. For example, in theMyList
example up above, the code in the body is checked withargument list index: Union[int, slice]
and a return type ofUnion[T, Sequence[T]]
. If there are no annotations on theimplementation, then the body is not type checked. If you want toforce mypy to check the body anyways, use the —check-untyped-defs
flag (more details here).
The variants must also also be compatible with the implementationtype hints. In the MyList
example, mypy will check that theparameter type int
and the return type T
are compatible withUnion[int, slice]
and Union[T, Sequence]
for thefirst variant. For the second variant it verifies the parametertype slice
and the return type Sequence[T]
are compatiblewith Union[int, slice]
and Union[T, Sequence]
.
Note
The overload semantics documented above are new as of mypy 0.620.
Previously, mypy used to perform type erasure on all overload variants. Forexample, the summarize
example from the previous section used to beillegal because List[str]
and List[int]
both erased to just List[Any]
.This restriction was removed in mypy 0.620.
Mypy also previously used to select the best matching variant using a differentalgorithm. If this algorithm failed to find a match, it would default to returningAny
. The new algorithm uses the “pick the first match” rule and will fall backto returning Any
only if the input arguments also contain Any
.