How the Python Default Constructor Works
Understand the python default constructor, what arguments it accepts, and when to define your own __init__ for reliable state initialization.
When you define a class in Python without an __init__ method, the class still has a constructor. Calling ClassName() creates an instance, and the behavior comes from the object base class. This implicit behavior is the python default constructor, and understanding it matters because it determines what arguments your class accepts, what state a new instance has, and when you must override it.
What the Default Constructor Does
In Python, every class inherits from object. When you do not define __init__, the class uses object.__init__, which accepts no arguments besides the instance itself. The same applies to __new__: object.__new__ allocates the instance.
class Point: pass p = Point() print(p) # <__main__.Point object at 0x...>
The instance is created successfully, but it has no attributes. Accessing an attribute before assigning it raises AttributeError:
p.x = 3 # works, sets an attribute print(p.x) # 3
The default constructor does not initialize any state. The instance is an empty container until you assign attributes.
What Arguments the Default Constructor Accepts
Because object.__init__ takes no arguments, a class without an __init__ definition accepts only zero arguments. Passing arguments raises a TypeError:
class Point: pass Point(1, 2)
This fails with:
TypeError: object.__init__() takes exactly one argument (the instance to initialize)
The error message comes from object.__init__, which is the default constructor. This is a common source of confusion for developers coming from languages where a default constructor accepts nothing but the language silently ignores extra arguments. Python does not ignore them; it raises an error.
The Role of __new__ in the Default Constructor
The term "constructor" in Python technically covers two methods: __new__ and __init__. __new__ is responsible for creating the instance, and __init__ for initializing it. The default __new__ from object creates an instance of the calling class. The default __init__ does nothing.
class Point: def __new__(cls): print("creating instance") return super().__new__(cls) def __init__(self): print("initializing instance") p = Point()
This prints:
creating instance
initializing instance
Most classes only need to override __init__. Overriding __new__ is reserved for cases like immutable types or singleton patterns. When you rely on the default constructor, both methods come from object, and the sequence is: object.__new__ allocates, then object.__init__ does nothing.
When You Must Override the Default Constructor
The default constructor is sufficient when a class needs no required state. Simple data holders, mixins, or marker classes work fine without __init__. The moment an instance must have certain attributes to be usable, you should define __init__.
class Point: def __init__(self, x, y): self.x = x self.y = y
Now Point(1, 2) works, and Point() raises TypeError because x and y are required. The default constructor is gone; you replaced it with one that enforces the state contract.
A common pattern is to provide default values so the constructor works with or without arguments:
class Point: def __init__(self, x=0, y=0): self.x = x self.y = y p1 = Point() p2 = Point(3, 4)
This is often what developers mean when they search for a "default constructor": a constructor with default parameter values. That is a different concept from the implicit object constructor, but the two are frequently conflated.
The Mutable Default Argument Pitfall
When you give __init__ default values, be careful with mutable defaults. A list or dictionary used as a default is created once at function definition time and shared across all instances.
class ShoppingCart: def __init__(self, items=[]): self.items = items cart1 = ShoppingCart() cart1.items.append("apple") cart2 = ShoppingCart() print(cart2.items) # ['apple']
Both carts share the same list. The fix is to use None as the default and create the list inside __init__:
class ShoppingCart: def __init__(self, items=None): self.items = items if items is not None else []
This keeps the default constructor behavior predictable: each instance gets its own list, and the constructor still works with zero arguments.
Runtime Cost and Maintainability Considerations
The default constructor has essentially no runtime cost beyond object allocation. object.__init__ performs no work, so there is no performance reason to avoid it. The maintainability concern is different: if you rely on the default constructor, every attribute assignment happens after construction, scattered across the calling code. That makes the class's required state implicit rather than explicit.
Defining __init__ centralizes initialization in one place. It also gives you a natural place to validate arguments, set derived attributes, and document what state a valid instance has. For a class that will be instantiated in many places, an explicit __init__ is usually the better choice even when all parameters have defaults.
The tradeoff is verbosity. A class with no __init__ is shorter, and for a simple namespace or a class used only for type checking, that brevity is reasonable. The decision depends on whether the class has meaningful state to enforce.
How the Default Constructor Interacts with Inheritance
When a subclass does not define __init__, it inherits the parent's __init__. If the parent defines a constructor with required parameters, the subclass must accept those same parameters.
class Base: def __init__(self, name): self.name = name class Child(Base): pass c = Child("test") # works c = Child() # TypeError: __init__() missing 1 required positional argument: 'name'
If the subclass defines its own __init__ and needs the parent's initialization, it must call super().__init__() explicitly. The default constructor does not chain automatically beyond the normal method resolution order lookup. This is a common place where a missing super() call leaves parent attributes unset.
class Child(Base): def __init__(self, name, age): super().__init__(name) self.age = age
Understanding the default constructor means understanding that object.__init__ is the ultimate fallback. Any class that does not define __init__ somewhere in its method resolution order ends up at object.__init__, which accepts no arguments. That is why a class with no __init__ anywhere in its hierarchy can only be instantiated with zero arguments.