How Python Handles Method Overloading Without Traditional Overloading
Python has no Java-style overloading: the last definition wins. Use defaults, *args, singledispatch and classmethod constructors instead.
If you came to Python from Java, C++ or C#, you probably expect this to work:
class Calculator:
def add(self, a, b):
return a + b
def add(self, a, b, c):
return a + b + c
calc = Calculator()
calc.add(1, 2)TypeError: Calculator.add() missing 1 required positional argument: 'c'Surprise. Python didn't pick the two-argument version. It doesn't even know it existed.
The simple idea: one name, one box
In a class body, def add is just an assignment, like x = 5. Write x = 5 and then x = 7, and x is 7. The first value is gone.
Methods are the same. The second def add overwrites the first, so the class ends up with exactly one add, the three-argument version.
Think of a football squad sheet with one slot labelled "captain". You can write Messi's name in it, then Ronaldo's, but the sheet only keeps the last name you wrote. Python's class has one slot per name.
If someone asks "does Python support method overloading?", the strong answer is: "Not by signature, because the last definition wins. But Python solves the same problems with default arguments, *args, singledispatch and classmethod constructors." Then show one.
Fix 1: Default arguments (the 80% solution)
Most overloading in other languages exists to make some parameters optional. Python has that built in.
class Calculator:
def add(self, a, b, c=0):
return a + b + c
calc = Calculator()
print(calc.add(1, 2)) # 3
print(calc.add(1, 2, 3)) # 6One method, two ways to call it. If that's all you needed, stop here.
Fix 2: *args for "any number of things"
When the number of inputs truly varies, collect them:
def total(*numbers):
return sum(numbers)
print(total(1, 2)) # 3
print(total(1, 2, 3, 4)) # 10*numbers gathers every positional argument into a tuple. **kwargs does the same for keyword arguments, giving you a dictionary.
Fix 3: singledispatch, when behaviour depends on type
Sometimes you want different code for different types: an int should be doubled, a str measured, a list counted. You could write a chain of if isinstance(...), but it grows ugly fast. The standard library has a cleaner answer.
from functools import singledispatch
@singledispatch
def describe(value):
return f"Something else: {value!r}"
@describe.register
def _(value: int):
return f"An integer, doubled: {value * 2}"
@describe.register
def _(value: str):
return f"A string of length {len(value)}"
@describe.register
def _(value: list):
return f"A list with {len(value)} items"
print(describe(21)) # An integer, doubled: 42
print(describe("chai")) # A string of length 4
print(describe([1, 2, 3])) # A list with 3 items
print(describe(3.5)) # Something else: 3.5singledispatch looks at the type of the first argument and sends the call to the matching function. The type hints on each registered function tell it which is which. The undecorated base function is the fallback.
Here's a trap worth knowing:
print(describe(True)) # An integer, doubled: 2bool is a subclass of int in Python, so True goes to the integer version. If booleans need their own behaviour, register bool separately. The more specific type wins.
Fix 4: singledispatchmethod inside a class
singledispatch dispatches on the first argument, and in a method that's self. So for methods, use the sibling decorator:
from functools import singledispatchmethod
class Formatter:
@singledispatchmethod
def format(self, value):
return str(value)
@format.register
def _(self, value: float):
return f"{value:.2f}"
@format.register
def _(self, value: list):
return ", ".join(self.format(v) for v in value)
f = Formatter()
print(f.format(3.14159)) # 3.14
print(f.format([1.5, 2, "x"])) # 1.50, 2, xIt skips self and dispatches on value. Notice the list version calls self.format for each item, so a list of mixed types formats each one correctly.
Fix 5: typing.overload, for your editor only
You may see @overload in library code and think, "so Python does have overloading!" Not quite.
from typing import overload
@overload
def parse(value: str) -> int: ...
@overload
def parse(value: bytes) -> int: ...
def parse(value):
if isinstance(value, bytes):
value = value.decode()
return int(value)
print(parse("42"), parse(b"7")) # 42 7The @overload stubs are a menu for type checkers like mypy and Pyright, so your editor knows which input gives which output. At runtime they do nothing. The last, undecorated parse is the only one Python actually runs.
Fix 6: classmethod constructors for "many ways to build an object"
Java classes often have several constructors. Python's answer is named alternative constructors:
class Order:
def __init__(self, order_id, amount):
self.order_id = order_id
self.amount = amount
@classmethod
def from_csv_row(cls, row: str):
order_id, amount = row.split(",")
return cls(int(order_id), float(amount))
@classmethod
def from_dict(cls, data: dict):
return cls(data["id"], data["amount"])
order = Order.from_csv_row("1001,499.5")
print(order.order_id, order.amount) # 1001 499.5This is arguably better than overloading: Order.from_csv_row(...) tells the reader exactly what's happening, while new Order(String) in Java makes you check which constructor gets picked. You'll see this pattern in the standard library too, for example datetime.fromisoformat() and dict.fromkeys().
Which one should you use?
| You want to... | Use |
|---|---|
| Make some parameters optional | Default arguments |
| Accept any number of inputs | *args / **kwargs |
| Run different code for different input types | singledispatch / singledispatchmethod |
| Help your editor understand return types | typing.overload (plus one real function) |
| Create an object from different sources | @classmethod constructors |
What goes wrong
- Defining a method twice and expecting both to exist. Linters like Ruff and Pylint warn about this (it's usually flagged as a redefinition). Turn those warnings on.
- Using
singledispatchon a method. It will dispatch onselfand always pick the fallback. Usesingledispatchmethod. - Mutable defaults.
def add(self, items=[])shares one list across every call. Useitems=Noneand create the list inside.
Want to go deeper on hints? Read whether Python type hints change runtime behaviour or performance.
In one line: Python keeps one function per name, so instead of overloading you use defaults, *args, singledispatch for type-based behaviour, and @classmethod for alternative constructors.
Keep reading
New games, tools and tutorials
About one short email a week. No spam, one-click unsubscribe.