Python typing.overload Function Ordering Rules
In Python, the @typing.overload decorator allows
developers to declare multiple function signatures to describe how
return types depend on input argument types. Because static type
checkers like Mypy and Pyright evaluate overloaded signatures
sequentially from top to bottom, the order of declaration is critical.
This guide outlines the essential rules dictating the ordering of
@typing.overload definitions, detailing how type checkers
resolve signatures and how to prevent shadowed types.
1. Top-to-Bottom Sequential Evaluation
Static type checkers process @typing.overload signatures
in the exact order they are declared in the code. When a function call
is analyzed, the checker tests the arguments against each signature
starting from the first. The first signature that
matches the call arguments is selected, and any subsequent signatures
are ignored for that call.
Because of this first-match behavior, ordering determines which return type is inferred whenever two or more signatures overlap.
2. Specific Types Before General Types
The primary rule of overload ordering is to define narrower, more specific types before broader, more general types. If a general type is placed first, it will consume calls intended for the narrower type, effectively shadowing it.
- Subclasses before Base Classes: A signature
accepting a derived class must appear before one accepting its parent
class. For example, since
boolis a subclass ofintin Python, abooloverload must precede anintoverload. LiteralTypes before Primitive Types: Precise value matches (e.g.,Literal["json"],Literal[True]) must appear before generic primitive types (e.g.,str,bool).- Concrete Types before
Anyorobject: Any broad fallback type will match any argument, so it must be placed after specific types.
Example:
from typing import Literal, overload
# 1. Most specific: Literal match
@overload
def fetch_data(format: Literal["json"]) -> dict: ...
# 2. Less specific: general str
@overload
def fetch_data(format: str) -> str: ...
# Implementation
def fetch_data(format: str) -> dict | str:
if format == "json":
return {"status": "ok"}
return "raw data"If format: str were placed first, passing the literal
"json" would match the general str signature
first, returning str instead of dict.
3. Mutually Exclusive (Disjoint) Types
When argument types do not overlap, declaration order does not affect
type inference. For example, if one overload accepts int
and another accepts str, neither can match the other's
inputs.
@overload
def process(value: int) -> int: ...
@overload
def process(value: str) -> str: ...In this scenario, switching the order has no functional impact on static analysis. However, keeping related overloads grouped consistently or placing the most common use cases higher improves code readability.
4. Default and Optional Arguments
When functions use default parameter values or varying argument counts:
- Place overloads with fewer parameters or specific default signatures
where they will not be preempted by broader signatures accepting
*argsor optional values. - If an argument accepts
Noneas a sentinel to trigger a specific return type, place theNoneoverload before a broaderOptional[T]or generic parameter overload.
5. Implementation Must Be Last and Undecorated
The actual implementation of the function must appear immediately
after all @typing.overload definitions.
- The implementation function itself must not have
the
@overloaddecorator. - Its signature must be broad enough to accept all combinations
covered by the overloads, typically using unions
(
int | str) orAny. - Type checkers do not use the implementation signature when inferring
types for callers; they only use the preceding
@overloaddefinitions.