super() is the builtin used to call a parent class's methods from inside a subclass. It appears most often in __init__ chains, in extended overrides that keep the parent's behavior, and in any class that participates in cooperative multiple inheritance. A lot of subtle behavior is packed into it: how the zero-argument form figures out the right class without help, why it uses the MRO instead of a fixed parent reference, and what goes wrong when you swap it out for a direct parent call. This chapter covers all of that.
super() Doessuper() returns a proxy object. The proxy lets you call methods as if you were a parent class, but with the current instance still bound to self. When you write super().method(args), Python finds the next class after the current one in the instance's MRO, looks up method on that class, and calls it with the current self. The instance is the same, the data is the same, only the method lookup starts higher up the chain.
A minimal example:
Inside DigitalProduct.describe, super() returns a proxy bound to the current instance and pointing to the next class in the MRO, which is Product. super().describe() calls Product.describe(self) and returns its string. The override prepends "digital, " and returns the combined result.
Two points matter here. First, super() always carries the current self along, so attributes set on the instance by either class are visible to both. Second, super() doesn't search for "a parent that happens to define this method"; it walks the MRO one step at a time. If the next class in the MRO doesn't define the method, the lookup continues to the class after that, and so on, until it finds a match or runs off the end and raises AttributeError.
In modern Python, you write super() with no arguments. Inside any regular method of a class, that bare call returns the right proxy without further input. The compiler captures the class the method belongs to and the first argument (self), and passes them to super for you.
Before Python 3, the equivalent was the two-argument form: super(DigitalProduct, self). You named the current class explicitly and passed self. Both forms do the same thing, and the two-argument form still works in Python 3:
The zero-argument form is cleaner, safer, and the default choice. It avoids two specific bugs that the two-argument form invites:
super(OldName, self) call inside it must be updated, or methods on the wrong class get called without warning. The zero-argument form needs no edits.The two-argument form still has narrow uses. Inside a function that isn't a method (a module-level helper called from somewhere), there's no implicit class to capture, so it must be named. Inside class methods (@classmethod), the form is super(Cls, cls), and even there the zero-argument form usually works. In practice, super() is almost always the form used and the older syntax can be forgotten.
super() Uses the MRO, Not "the Parent"In single inheritance, super() and "the parent class" mean the same thing, so the distinction doesn't matter. In multiple inheritance, they diverge, and confusing the two is a common cause of broken cooperative hierarchies.
super() returns a proxy that walks the method resolution order of the instance, not the source-code parent list. The "next class" depends on the type of the actual object, not on where the method is written.
A diamond example shows the difference:
Consider DigitalProduct.describe. The method itself doesn't change between the two cases, but super().describe() resolves differently. For a DigitalProduct instance, the MRO is DigitalProduct, Product, object, so super() points to Product. For an Ebook instance, the MRO is Ebook, DigitalProduct, DiscountableProduct, Product, object, so super() from inside DigitalProduct.describe points to DiscountableProduct, not Product.
This is the practical difference. super() doesn't have an opinion about which class is "the parent". It walks the MRO and uses whatever comes next. The same method can deliver super() calls to different classes depending on the instance type, which is what cooperative multiple inheritance needs.
The MRO is set at class-definition time. The super() call uses the instance type at runtime. The two come together at the call site: super() reads the MRO of type(self) and finds the next class after the one the current method lives in.
Inside a method, the resolution target of super() can be read from type(self).__mro__. Compare that list with the class the method is defined on, and the next entry in the list is what super() points to.
super().__init__(...) in Subclass InitThe most common use of super() is in a subclass's __init__ to run the parent's initialization. Without that call, the parent never gets to set up its own attributes, and the subclass starts life with a half-initialized instance.
A small order hierarchy makes the pattern concrete:
GiftOrder.__init__ calls super().__init__(order_id, total) first. That runs Order.__init__, which sets self.order_id and self.total. Then the subclass adds self.recipient. All three attributes end up on the same instance because both __init__ calls receive the same self.
Two conventions matter here:
__init__ decides what arguments it needs. The subclass takes care of any extras and forwards the rest. Don't redefine parameters on the subclass to pass them through unchanged; let them flow.The wrong pattern is to skip super() and retype the parent's setup:
This works for trivial cases, but it drifts out of sync without warning. If Order.__init__ adds validation, a computed attribute, or a default value, the subclass loses the new behavior. super().__init__(...) is what keeps the subclass in step with the parent.
The other broken pattern is forgetting to call super().__init__ at all:
Python does not call the parent's __init__ automatically when the subclass defines its own. The fix is to add super().__init__(order_id, total) before any subclass-specific work.
super() in Multiple InheritanceCooperative super() is the pattern where every class in an inheritance chain calls super() for the same method, and each class forwards the call along the MRO. The result is that a single call from the bottom of the hierarchy reaches every class once, in MRO order, instead of stopping at the first parent.
Cooperative super() is the reason multiple inheritance works at all in Python. The MRO gives the search order; cooperative super() is the agreement among classes to follow it.
Two rules make it work:
*args, **kwargs to forward.** If one class takes verbose=False and another doesn't, the call breaks unless the participating classes use **kwargs to pass through arguments they don't recognize.A cooperative __init__ chain illustrates both rules:
Each class takes the keyword arguments it cares about (using keyword-only syntax with *), then forwards the rest with **kwargs. The super().__init__(**kwargs) call sends the leftover arguments along the MRO. The chain ends at object.__init__, which accepts no extra arguments, so every keyword must be consumed by the time the call reaches object.
Think of each __init__ as a station along a track. Arguments enter at the bottom (the Laptop call), and each station takes its passengers (its own keyword arguments) and forwards the rest down the line. If one station forgets to forward, everyone after it gets stranded.
The same shape works for any cooperative method, not only __init__. If three classes all contribute to a validate method or a to_dict method, each one calls super().validate(...) or super().to_dict() and adds its own piece.
Each to_dict calls super().to_dict() to get the dictionary built so far, then adds its own field. The MRO of Phone is Phone, TaxableProduct, ShippableProduct, Product, object, so the call walks through TaxableProduct.to_dict, ShippableProduct.to_dict, and Product.to_dict in that order. Each class contributes one key to the dictionary, and the result is the combined view.
Cooperative super() adds one Python call per class in the MRO. For a three- or four-class chain, the overhead is negligible. Deep cooperative chains (eight or more classes) can show up in profiles, but few hierarchies are that deep.
super()In Python 3, super() is always bound. The proxy returned by super() inside a method carries both the class and the instance, so super().method(args) passes self automatically. The form super().method(self, args) is not used; the self is implicit, like any normal method call.
The "unbound" form involves the two-argument syntax outside a method, or omitting the self argument. The two-argument form has variants:
super(Cls, instance) returns a proxy bound to instance. Calling methods on it passes instance automatically. This form matches the zero-argument super() inside a method.super(Cls, subclass) (where subclass is a class, not an instance) returns a proxy useful for @classmethods. It binds to the subclass instead of an instance.super(Cls) with one argument returns an unbound proxy. It would have to be invoked on a specific instance later, which is uncommon.The zero-argument form is used inside instance methods and the rest can be ignored. One situation where the two-argument form appears is inside a @classmethod that calls a parent class method:
Inside a classmethod, super() still works without arguments. It returns a proxy that uses cls instead of self, and methods called on it receive cls as their first argument. The same zero-argument call adapts to the kind of method it's used in.
For instance methods, the bound form applies. The proxy keeps self attached, no explicit pass is needed, and the method on the next class in the MRO runs as if it were called normally. The rule is simple: use super() with no arguments inside methods, and pass the same arguments as the method being forwarded to.
super() is small, but it has a handful of failure modes. Most come from mixing cooperative super() with hard-coded parent calls, or from forgetting that super() walks the MRO instead of going to the literal source-code parent.
Forgetting to call `super().__init__()`. Covered above. The fix is to add the call at the top of the subclass __init__. Watch for it when copying an existing subclass and changing its parent: a super() call that was correct in the original might need different arguments in the new version.
Replacing `super()` with `Parent.method(self)`. Both forms can call a parent's method. In single inheritance, the difference is invisible. In multiple inheritance, the explicit call skips part of the MRO and breaks cooperation. Use super() unless there is a specific reason to bypass the MRO.
For this particular MRO, the output happens to look right, but the chain is fragile. Adding a new mixin between GiftWrappableMixin and Product means the hard-coded Product.describe(self) call still jumps straight to Product and skips the new class. The same code with super() would adapt automatically.
Mixing cooperative and non-cooperative classes. If half the classes call super() and the other half don't, the chain breaks at the boundary. The output is partial: classes before the non-cooperative one ran, classes after it didn't. The fix is consistency. Either every class in a cooperative chain participates, or none do.
Signature mismatches. If one class's __init__ accepts weight_kg and another's accepts tax_rate, and neither uses **kwargs to forward, the chain throws TypeError when called. The fix is to use **kwargs in every cooperative __init__ and let each class take only the arguments it cares about, as in the cooperative example above.
Calling `super()` outside a method. The zero-argument form needs an enclosing method to capture the class and self. A call to super() from a regular function (or a nested function inside a method) raises RuntimeError: super(): no arguments. The fix is to move the call back inside a method or use the two-argument form with the class and instance named explicitly.
A small example of the signature mismatch:
The fix is to give each __init__ **kwargs and call super().__init__(**kwargs) to forward the leftovers, the pattern from the cooperative example earlier. Without that, the first class in the MRO consumes all the arguments and the rest never run.
10 quizzes