Constructor Overloading in Python: 5 Ways That Actually Work

Python has no real constructor overloading in Python the way Java or C++ does: a class keeps only one __init__, and a second definition silently replaces the first. You get the same effect with default arguments or with @classmethod alternative constructors:

class Order:
    def __init__(self, item, qty=1, price=0.0):
        ...

    @classmethod
    def from_csv(cls, line):
        item, qty, price = line.split(",")
        return cls(item, int(qty), float(price))

My test machine ran Python 3.12.5 and mypy 2.4.0 from Command Prompt; the customers, prices and dates in the samples are made up.

In this tutorial, we’ll see why two __init__ methods fail, then cover default arguments, *args and **kwargs, classmethod factories, functools.singledispatchmethod and typing.overload, plus a table to help you pick one.

Why doesn’t Python support constructor overloading?

Java picks a constructor by matching the argument list; Python never checks signatures. A class body runs top to bottom, so def __init__ just assigns a function to the name __init__, the same way it sets up any constructor in Python.

Define it twice and the second assignment wins. Here’s what happens when I try the Java style:

class Customer:
    def __init__(self, name):
        self.name = name
        self.email = None

    def __init__(self, name, email):
        self.name = name
        self.email = email


print(Customer("Emily Carter", "emily@example.com").email)
print(Customer("James Miller").name)

Output:

emily@example.com
Traceback (most recent call last):
  File "C:\pyguides\customers.py", line 12, in <module>
    print(Customer("James Miller").name)
          ^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: Customer.__init__() missing 1 required positional argument: 'email'
customers.py:6:9: F811 Redefinition of unused `__init__` from line 2: `__init__` redefined here
Found 1 error.
Command Prompt output of a class with two __init__ methods raising TypeError missing email, and ruff reporting F811 redefinition
The two-argument call works, the one-argument call raises a TypeError, and ruff’s F811 rule points at the redefined __init__.

The first __init__ is gone, so Customer("James Miller") fails with missing 1 required positional argument: 'email'. Python never warns you about this; a linter like ruff (rule F811, redefinition of an unused name) does.

Overload a constructor with default arguments

Default values are the simplest fix, and they cover most real cases. One __init__ accepts one, two or four arguments because the rest have fallbacks. It’s the same idea as Python class constructors with parameters, just with optional ones:

class Order:
    def __init__(self, item, qty=1, price=0.0, ship_to=None):
        self.item = item
        self.qty = qty
        self.price = price
        self.ship_to = ship_to or "Store pickup"

    def __repr__(self):
        total = self.qty * self.price
        return f"Order({self.item!r}, qty={self.qty}, total=${total:,.2f}, ship_to={self.ship_to!r})"


print(Order("Gift Card"))
print(Order("Coffee Mug", 2, 12.50))
print(Order("Desk Lamp", price=39.99, ship_to="Austin, TX"))

Output:

Order('Gift Card', qty=1, total=$0.00, ship_to='Store pickup')
Order('Coffee Mug', qty=2, total=$25.00, ship_to='Store pickup')
Order('Desk Lamp', qty=1, total=$39.99, ship_to='Austin, TX')

Keyword arguments like price=39.99 let callers skip the middle parameters. If you want to force keywords for the optional values, put a bare * before them: def __init__(self, item, *, qty=1, price=0.0).

Avoid mutable default arguments in __init__

Defaults are evaluated once, when Python runs the def line, not on each call. A list default is therefore shared by every object that doesn’t pass its own list:

class Cart:
    def __init__(self, owner, items=[]):          # bug: one list shared by every cart
        self.owner = owner
        self.items = items


class SafeCart:
    def __init__(self, owner, items=None):
        self.owner = owner
        self.items = [] if items is None else list(items)


emily, james = Cart("Emily"), Cart("James")
emily.items.append("Laptop Stand")
print("Cart:", james.items)

emily, james = SafeCart("Emily"), SafeCart("James")
emily.items.append("Laptop Stand")
print("SafeCart:", james.items)

Output:

Cart: ['Laptop Stand']
SafeCart: []
Command Prompt output showing a list default argument shared between two Cart objects and an empty list for SafeCart
James’s Cart shows Emily’s laptop stand, while SafeCart gives each customer an empty list.

Use None as the default and create the list inside the constructor. I also copy the passed list with list(items), so the caller’s list can’t change the cart later.

Overload __init__ with *args and **kwargs

When the variants have different shapes, *args collects positional values into a tuple and **kwargs collects keyword values into a dict. Then __init__ branches on what it got. If those two are new to you, read the difference between *args and **kwargs first:

class Price:
    def __init__(self, *args, **kwargs):
        if len(args) == 2:                                    # Price(19, 99)
            dollars, cents = args
            self.cents = dollars * 100 + cents
        elif len(args) == 1 and isinstance(args[0], str):     # Price("$1,249.50")
            self.cents = round(float(args[0].replace("$", "").replace(",", "")) * 100)
        elif not args and "cents" in kwargs:                  # Price(cents=1999)
            self.cents = kwargs["cents"]
        else:
            raise TypeError(f"Price() can't use args={args} kwargs={kwargs}")

    def __repr__(self):
        return f"Price(${self.cents / 100:,.2f})"


print(Price(19, 99))
print(Price("$1,249.50"))
print(Price(cents=1999))
try:
    Price(19.99)
except TypeError as err:
    print("TypeError:", err)

Output:

Price($19.99)
Price($1,249.50)
Price($19.99)
TypeError: Price() can't use args=(19.99,) kwargs={}

It works, but notice the cost. The signature says nothing about the accepted forms, IDE autocomplete can’t help callers, and every new form adds another branch. I keep this pattern for small value classes with two or three forms.

Always end with an else that raises TypeError. Without it, Price(19.99) would create an object with no cents attribute and fail somewhere far away.

Use @classmethod alternative constructors (the Pythonic way)

A classmethod receives the class itself as cls, so it can parse any input and then call cls(...). Each way of building the object gets its own clear name, like from_csv or from_dict. The Python classmethod() function guide covers the decorator itself:

from datetime import date


class Employee:
    def __init__(self, first, last, salary, hired):
        self.first, self.last = first, last
        self.salary, self.hired = salary, hired

    @classmethod
    def from_csv(cls, line):
        first, last, salary, hired = line.split(",")
        return cls(first, last, float(salary), date.fromisoformat(hired))

    @classmethod
    def from_dict(cls, data):
        return cls(data["first"], data["last"], data["salary"], date.fromisoformat(data["hired"]))

    def __repr__(self):
        return (f"{type(self).__name__}({self.first} {self.last}, "
                f"${self.salary:,.2f}, hired {self.hired:%b %d, %Y})")


class Contractor(Employee):
    pass


print(Employee("Emily", "Carter", 98500.0, date(2026, 1, 12)))
print(Employee.from_csv("James,Miller,87250.50,2026-03-02"))
print(Employee.from_dict({"first": "Olivia", "last": "Brown", "salary": 112000, "hired": "2026-06-15"}))
print(Contractor.from_csv("Noah,Davis,64000,2026-09-08"))

Output:

Employee(Emily Carter, $98,500.00, hired Jan 12, 2026)
Employee(James Miller, $87,250.50, hired Mar 02, 2026)
Employee(Olivia Brown, $112,000.00, hired Jun 15, 2026)
Contractor(Noah Davis, $64,000.00, hired Sep 08, 2026)
Command Prompt output of Employee objects created with __init__, from_csv and from_dict, and a Contractor built by an inherited classmethod
All four objects print with their salary in USD and hire date; the last one is a Contractor, not an Employee.

Look at the last line. Contractor inherited from_csv, and because the method calls cls instead of Employee, it returns a Contractor. A staticmethod can’t do that, which is the main point in Python class method vs static method.

The standard library uses this pattern everywhere: dict.fromkeys(), datetime.fromisoformat() and int.from_bytes() are all alternative constructors.

Dispatch __init__ on argument type with singledispatchmethod

functools.singledispatchmethod (Python 3.8+) picks an implementation from the type of the first argument after self. You register one function per type, using the type hint, and it works on __init__ too.

Plain functools.singledispatch would dispatch on self. Methods get self first, as the difference between functions and methods explains, which is why this method variant exists:

from datetime import date, datetime, timedelta
from functools import singledispatchmethod


class Invoice:
    @singledispatchmethod
    def __init__(self, due):
        raise TypeError(f"Invoice() can't use a {type(due).__name__} due date")

    @__init__.register
    def _(self, due: date):
        self.due = due

    @__init__.register
    def _(self, due: datetime):
        self.due = due.date()

    @__init__.register
    def _(self, due: str):
        self.due = date.fromisoformat(due)

    @__init__.register
    def _(self, due: int):
        self.due = date(2026, 11, 1) + timedelta(days=due)    # net-N days from Nov 1, 2026


for value in (date(2026, 11, 15), datetime(2026, 11, 20, 17, 30), "2026-12-01", 30, 4.5):
    try:
        print(f"{value!r:40} -> due {Invoice(value).due}")
    except TypeError as err:
        print(f"{value!r:40} -> TypeError: {err}")

Output:

datetime.date(2026, 11, 15)              -> due 2026-11-15
datetime.datetime(2026, 11, 20, 17, 30)  -> due 2026-11-20
'2026-12-01'                             -> due 2026-12-01
30                                       -> due 2026-12-01
4.5                                      -> TypeError: Invoice() can't use a float due date
Command Prompt output of an Invoice class whose __init__ dispatches on date, datetime, str and int, with a TypeError for a float
Each input type is routed to its own registered __init__, and the float falls through to the base version that raises TypeError.

Two details matter here. Dispatch follows the class hierarchy, so datetime would match the date version if I hadn’t registered it separately. And only the first argument counts, so you can’t dispatch on the second one.

Add typing.overload signatures for type checkers

typing.overload doesn’t change what runs. It lists the valid call signatures for mypy, pyright and your editor, followed by one real implementation without the decorator. It pairs well with other hints, such as Python tuple type hints:

from typing import overload


class