2026, Jan 02 21:00
super() vs Base.__init__(self): Why Python Needs self for Base Constructors and How to Avoid TypeError
Learn why calling Base.__init__() in Python needs explicit self, how super() auto-injects it, and how to fix TypeError: missing self with clear examples.
Why does calling a base class constructor sometimes require passing self explicitly? In Python OOP, two patterns are commonly seen in derived classes: super().__init__() and Base.__init__(self). The first looks cleaner, the second is more direct. Yet Base.__init__() without an argument crashes with a missing self error. Let’s unpack why.
Reproducing the issue
The behavior becomes obvious with a short example. The logic is straightforward: two valid constructor calls and one that raises a TypeError when self is not provided.
class ParentType:
def __init__(self, /):
print("bootstrapping")
class ChildType(ParentType):
def __init__(self):
super().__init__()
ParentType.__init__(self)
ParentType.__init__()
unit = ChildType()
The last call to ParentType.__init__() fails with TypeError because no self was supplied.
What’s really going on
To see the core rule, look at how instance methods work in Python. An instance method declares self explicitly as its first parameter. Python automatically supplies that instance when you call the method from an object. These two forms are equivalent in effect: the first uses an instance, the second uses the class and passes the instance manually.
class Gadget:
def ping(self):
print("ping")
g = Gadget()
g.ping()
Gadget.ping(g)
The instance form g.ping() implicitly passes g as the first argument. The class form Gadget.ping(g) passes g explicitly. That’s the same principle at play with __init__.
Why super() works but Base.__init__() doesn’t
In the problematic call, ParentType is a class. When you invoke ParentType.__init__(...), you’re calling a method on the class, so Python does not inject self for you. If you want that method to run for the current object, you must pass self explicitly.
super(), on the other hand, acts as a proxy tied to the current instance, effectively behaving like an instance for method resolution. Because it behaves like an instance, super().__init__() automatically supplies self under the hood. That’s why the implicit form succeeds while the class-qualified call without self fails.
The fix
Both of the following forms are valid. Either rely on super() or call the base class initializer with self passed explicitly. The only failing variant is the one that omits self when calling the base class directly.
class ParentType:
def __init__(self, /):
print("bootstrapping")
class ChildType(ParentType):
def __init__(self):
super().__init__()
ParentType.__init__(self)
unit = ChildType()
Why it’s worth knowing
This distinction explains a common TypeError: missing 1 required positional argument: 'self'. It also clarifies the difference between calling methods via an instance versus via a class. Understanding that super() behaves like an instance-facing proxy helps you avoid subtle initialization bugs and keeps constructors predictable.
Takeaways
In Python, instance methods expect the instance as their first argument. When you call through an instance, Python provides it implicitly; when you call through a class, you must pass it explicitly. super().__init__() works without mentioning self because super() stands in for the current instance, while ParentType.__init__(self) requires you to pass self by hand. Avoid ParentType.__init__() without arguments—there’s no implicit self in that form.