The super() function is how a class reaches the behaviour it inherits, and it is the tool that makes overriding safe. Used properly it lets a subclass extend a parent's method instead of replacing it, and it does so in a way that keeps working when there is more than one parent in the chain.
Two ideas sit underneath this page: what super() actually refers to, and what happens when a chain of classes each call it. Both matter more than the syntax, because the syntax is easy and the chain is where the bugs live.
The word "super" is misleading, because super() does not mean "my parent class". It means "the next class in the method resolution order after this one" — the class Python would look in next when resolving a name. For a class with a single parent that happens to be the parent, which is why the simpler mental model usually works right up until you write multiple inheritance.
What does super() actually give you?
super() returns a proxy object that stands in for whichever class sits next in the MRO. Calling super().method(args) invokes that class's method, with self still bound to your current object. You never pass self yourself.
class Logger:
def __init__(self, name):
self.name = name
def log(self, message):
return f"[{self.name}] {message}"
class Service(Logger):
def __init__(self, name, url):
super().__init__(name)
self.url = url
def log(self, message):
return super().log(f"{self.url} -> {message}")
svc = Service("api", "https://example.com")
print(svc.name)
print(svc.url)
print(svc.log("request sent"))
Note the shape of the call. super().__init__(name) — not super().__init__(self, name). The self is supplied automatically, and passing it again is an error that shows up as a confusing "takes 2 positional arguments but 3 were given".
There is a small practical benefit to using super() even when you have exactly one parent: it keeps the code correct if you later add a second parent. Writing Parent.method(self) hard-codes the parent class into the child's body, and that line becomes wrong the moment the hierarchy changes shape.
How do you extend a method instead of replacing it?
Call super()'s version first, then add your own behaviour to the result. That is the pattern behind every "decorate and delegate" method you will write: compute the base value, transform it, return it.
class Invoice:
def __init__(self, subtotal):
self.subtotal = subtotal
def total(self):
return self.subtotal
class DiscountedInvoice(Invoice):
def __init__(self, subtotal, percent):
super().__init__(subtotal)
self.percent = percent
def total(self):
return super().total() * (1 - self.percent / 100)
print("standard:", Invoice(200).total())
print("discounted:", DiscountedInvoice(200, 25).total())
Compare the two classes. StandardInvoice inherits total() unchanged. DiscountedInvoice overrides it, calls super().total() to get the pre-discount amount, and applies its own reduction. If the parent's calculation changes, the subclass automatically follows.
The alternative — writing Parent.total(self) explicitly — works too, but it hard-codes a parent and it breaks the moment a second parent appears in the chain.
How does super() behave in an __init__ chain?
Each class in the chain calls super().__init__() and passes along whatever it received plus its own extra arguments. The chain stops when a class's __init__ does not call super() at all, or when it reaches object, whose __init__ takes no arguments.
class User:
def __init__(self, name):
self.name = name
class Customer(User):
def __init__(self, name, email):
super().__init__(name)
self.email = email
class PremiumCustomer(Customer):
def __init__(self, name, email, tier):
super().__init__(name, email)
self.tier = tier
p = PremiumCustomer("Asha", "[email protected]", "gold")
print(p.name, p.email, p.tier)
print("all three attributes are set:", vars(p))
Two things to notice. The arguments get forwarded as you go up: PremiumCustomer takes a tier and passes it to Customer.__init__, which takes a name and passes it to object.__init__. And the order of execution runs bottom-up — the most derived class's __init__ body runs first, then the parent's, then the grandparent's.
That ordering matters when the child does work in __init__ that depends on a parent's attributes. Since the parent's __init__ has not run yet at the start of the child's body, anything the parent sets will not exist yet. Set your own attributes after the super() call, or validate before it.
What happens if a class forgets to call super()?
The chain stops there. Any parent below that point never gets initialised, so its attributes are missing and the failure appears later, somewhere else, as an AttributeError that has nothing to do with the real mistake.
class User:
def __init__(self, name):
self.name = name
self.level = "standard"
def greet(self):
return f"hello {self.name} ({self.level})"
class Sloppy(User):
def __init__(self, name):
self.name = name
try:
print(Sloppy("Ben").greet())
except AttributeError as exc:
print("AttributeError:", exc)
The error message in that traceback points at the line that reads the missing attribute, not at the class that forgot to call super(). This is why "always call super().__init__()" is good advice even when you think you do not need the parent's setup — the parent might set up something you have not noticed yet.
There is a second failure mode worth knowing: a subclass that calls super().__init__() with the wrong arguments. The parent's signature is the contract, and when a grandchild later appears in the chain it will be the parent's signature that everything has to keep matching. If you need a different setup, give the parent a keyword argument with a default rather than changing the order of the positional ones.
Is super() only for __init__?
No. It works in any method, and it is how a subclass shares behaviour across siblings that inherit from the same parent. This is sometimes called the mixin pattern: several small classes contribute behaviour, and the concrete class at the end combines them.
class BaseProcessor:
def process(self, item):
return f"processed {item}"
class LoggingMixin:
def process(self, item):
return f"logged({super().process(item)})"
class TimingMixin:
def process(self, item):
return f"timed({super().process(item)})"
class Worker(LoggingMixin, TimingMixin, BaseProcessor):
def process(self, item):
return super().process(item)
print(Worker().process("job-7"))
Every one of those methods calls super() and hands its value onward, so the chain composes cleanly. If one of them forgot to call super(), the methods below it in the chain would never run and the combined behaviour would be silently incomplete.
What to carry forward
super()means "next in the MRO", not "my parent". They coincide in simple cases.- Never pass
selftosuper()— it is supplied for you. - Call
super()inside__init__and forward your extra arguments up the chain. - To extend rather than replace, compute the base result and transform it.
- A missing
super()call breaks the chain silently, and the error surfaces far away.
Try it yourself: build a cooperative chain
Write three classes: Notifier with an __init__ taking a channel and a send(message) method that returns the message wrapped in the channel name, then LoggingNotifier and RetryingNotifier, both inheriting from Notifier and overriding send() to call super().send() first.
Now write a fourth class that inherits from LoggingNotifier and calls super().send() again, so the three behaviours stack. Check the final output contains all three contributions. Then temporarily delete the super() call from the middle class, run it again, and note which part of the behaviour disappears. That missing piece is exactly what a broken chain costs you.