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.

By CrunchCubeUpdated 21 October 20265 min read

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.

Interview tip

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))  # 6

One 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.5

singledispatch 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: 2

bool 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, x

It 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 7

The @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.5

This 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 optionalDefault arguments
Accept any number of inputs*args / **kwargs
Run different code for different input typessingledispatch / singledispatchmethod
Help your editor understand return typestyping.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 singledispatch on a method. It will dispatch on self and always pick the fallback. Use singledispatchmethod.
  • Mutable defaults. def add(self, items=[]) shares one list across every call. Use items=None and 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.