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.

__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: []

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)

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

__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