Loading content...
Method overriding gives subclasses the power to replace inherited behavior entirely. But what if you don't want to replace—what if you want to extend? You want the parent's logic to still execute, but with additional behavior before or after it.
This is where the super keyword becomes essential. It provides a reference to the parent class's implementation, allowing you to call the method you're overriding from within your override. This enables a powerful pattern: building on top of inherited behavior rather than discarding it.
By the end of this page, you will understand how to use the super keyword to invoke parent class methods, when to call super (before, after, or conditionally), constructor chaining with super, and best practices for extending inherited behavior without duplicating code.
In most object-oriented languages, super (or equivalents like base in C#, parent:: in PHP) provides access to the parent class's members. When used in an overriding method, super.methodName() invokes the parent's version of that method instead of recursively calling the override.
Key uses of super:
super.process() calls the parent's process() implementationsuper(args) invokes the parent class constructor (required in most languages)super.fieldName accesses a parent field if shadowed by a subclass field12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
class Logger { protected String prefix; public Logger(String prefix) { this.prefix = prefix; } public void log(String message) { System.out.println(prefix + ": " + message); } public void logWithTimestamp(String message) { String timestamp = java.time.LocalDateTime.now().toString(); System.out.println(timestamp + " " + prefix + ": " + message); }} class FileLogger extends Logger { private String filePath; public FileLogger(String prefix, String filePath) { super(prefix); // Call parent constructor this.filePath = filePath; } @Override public void log(String message) { // Call parent's log method first (prints to console) super.log(message); // Then add file-specific behavior writeToFile(message); } @Override public void logWithTimestamp(String message) { // Add pre-processing before parent's logic String enrichedMessage = "[FILE] " + message; // Call parent's implementation with enriched message super.logWithTimestamp(enrichedMessage); // Add post-processing after parent's logic writeToFile(enrichedMessage); } private void writeToFile(String message) { // Simulating file write System.out.println(" --> Written to " + filePath); }} // Usagepublic class Demo { public static void main(String[] args) { Logger logger = new FileLogger("APP", "/var/log/app.log"); logger.log("Application started"); // Output: // APP: Application started // --> Written to /var/log/app.log logger.logWithTimestamp("Processing request"); // Output: // 2024-01-15T10:30:00 APP: [FILE] Processing request // --> Written to /var/log/app.log }}While super.method() looks like a method call on an object, super itself isn't a standalone object reference. You can't assign it to a variable or pass it around. It's a language keyword that specifically redirects method resolution to the parent class's implementation.
The placement of your super call within an override affects the order of operations. Different scenarios call for different patterns:
Call super first when you want to build upon completed parent behavior:
123456789101112131415161718
class DataProcessor: def process(self, data: dict) -> dict: """Validate and normalize data.""" if not data: raise ValueError("Data cannot be empty") return {k.lower(): v for k, v in data.items()} class EnrichedDataProcessor(DataProcessor): def process(self, data: dict) -> dict: # Super FIRST: rely on parent's validation and normalization normalized = super().process(data) # THEN add enrichment (depends on normalized data) normalized['processed_at'] = datetime.now().isoformat() normalized['processor'] = 'EnrichedDataProcessor' return normalizedConstructors have special rules for super calls. When you create a subclass object, the parent class's constructor must be invoked to properly initialize the inherited portion of the object. This is constructor chaining.
Key rules:
super() explicitlysuper(args) explicitlysuper() call must be the first statement in the constructor (in most languages)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
class Vehicle { protected String brand; protected int year; // No-argument constructor public Vehicle() { this.brand = "Unknown"; this.year = 2000; System.out.println("Vehicle() constructor called"); } // Parameterized constructor public Vehicle(String brand, int year) { this.brand = brand; this.year = year; System.out.println("Vehicle(brand, year) constructor called"); }} class Car extends Vehicle { private int numDoors; // If no super() call, Java implicitly calls super() public Car() { // Implicit: super(); this.numDoors = 4; System.out.println("Car() constructor called"); } // Explicit super() call to no-arg constructor public Car(int numDoors) { super(); // Calls Vehicle() this.numDoors = numDoors; System.out.println("Car(numDoors) constructor called"); } // Explicit super() call to parameterized constructor public Car(String brand, int year, int numDoors) { super(brand, year); // Calls Vehicle(brand, year) this.numDoors = numDoors; System.out.println("Car(brand, year, numDoors) constructor called"); }} class ElectricCar extends Car { private int batteryCapacity; public ElectricCar(String brand, int year, int numDoors, int batteryCapacity) { super(brand, year, numDoors); // Calls Car's 3-arg constructor this.batteryCapacity = batteryCapacity; System.out.println("ElectricCar constructor called"); }} // Creating an ElectricCar triggers the entire constructor chain:// 1. Vehicle(brand, year) → 2. Car(brand, year, numDoors) → 3. ElectricCar(...)public class Demo { public static void main(String[] args) { ElectricCar tesla = new ElectricCar("Tesla", 2024, 4, 100); // Output: // Vehicle(brand, year) constructor called // Car(brand, year, numDoors) constructor called // ElectricCar constructor called }}Unlike Java (which has implicit super() calls), Python requires you to explicitly call super().init(). Forgetting this is a common bug that leaves parent attributes uninitialized, causing AttributeError exceptions later.
When an object is created, initialization happens in a specific, deterministic order. Understanding this order is crucial for avoiding subtle bugs, especially when overriding methods that might be called during construction.
Typical initialization order (Java/C#/similar):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
class Base { protected String baseField = initBaseField(); public Base() { System.out.println("1. Base constructor"); initialize(); // ⚠️ DANGER: Calling overridable method! } private String initBaseField() { System.out.println("0. Base field initializer"); return "base"; } protected void initialize() { System.out.println("2. Base.initialize()"); }} class Derived extends Base { private String derivedField = initDerivedField(); private int value; public Derived(int value) { super(); // Base constructor runs BEFORE derivedField is initialized System.out.println("4. Derived constructor"); this.value = value; } private String initDerivedField() { System.out.println("3. Derived field initializer"); return "derived"; } @Override protected void initialize() { // ⚠️ PROBLEM: This runs BEFORE derivedField is initialized! System.out.println("2. Derived.initialize()"); System.out.println(" derivedField = " + derivedField); // null! System.out.println(" value = " + value); // 0! }} public class Demo { public static void main(String[] args) { Derived obj = new Derived(42); // Output: // 0. Base field initializer // 1. Base constructor // 2. Derived.initialize() <-- Called during Base constructor! // derivedField = null <-- Not initialized yet! // value = 0 <-- Not initialized yet! // 3. Derived field initializer // 4. Derived constructor }}Calling overridable methods from a constructor is dangerous! If a subclass overrides the method, the override runs before the subclass is fully initialized, potentially accessing uninitialized fields. This is a classic OOP anti-pattern. Use private or final methods in constructors instead.
Languages that support multiple inheritance (like Python) face an additional complexity: which parent's method does super() call? Python solves this with the Method Resolution Order (MRO)—a deterministic algorithm (C3 linearization) that defines the order in which parent classes are searched.
super() doesn't just go to the "immediate parent"—it goes to the next class in the MRO. This enables the cooperative multiple inheritance pattern, where each class in the hierarchy calls super() to ensure all classes get a chance to run their code.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
class A: def process(self): print("A.process") class B(A): def process(self): print("B.process - before super") super().process() # Goes to next in MRO, not necessarily A print("B.process - after super") class C(A): def process(self): print("C.process - before super") super().process() # Goes to next in MRO print("C.process - after super") class D(B, C): # Multiple inheritance: D inherits from both B and C def process(self): print("D.process - before super") super().process() # Goes to B (first parent) print("D.process - after super") # View the MROprint("D's MRO:", [cls.__name__ for cls in D.__mro__])# Output: D's MRO: ['D', 'B', 'C', 'A', 'object'] # Execute processd = D()d.process()# Output:# D.process - before super# B.process - before super# C.process - before super <-- B's super() goes to C, not A!# A.process# C.process - after super# B.process - after super# D.process - after super # The call chain follows the MRO:# D → B → C → A (not D → B → A followed by D → C → A)For multiple inheritance to work correctly, ALL classes in the hierarchy should call super() using the cooperative pattern. If any class breaks the chain (doesn't call super()), classes later in the MRO won't get invoked. This is why understanding MRO is essential for Python multiple inheritance.
Why MRO matters:
The MRO ensures that:
class D(B, C) means B before C)Proper use of super leads to maintainable, extensible inheritance hierarchies. Here are the best practices distilled from years of object-oriented design experience:
Remember that calling super() creates a dependency on the parent's implementation, not just its interface. If the parent class changes what happens in the method, your behavior changes too. This coupling is inherent to inheritance and is one reason composition is sometimes preferred.
The super keyword is your bridge to parent class implementations, enabling extension rather than replacement of inherited behavior. Used correctly, it creates powerful, maintainable inheritance hierarchies.
What's next:
The final topic in this module covers covariant return types—the ability of an overriding method to return a more specific type than the parent method declared. This feature enables more precise APIs while maintaining substitutability.
You now understand how to use super to invoke parent implementations, constructor chaining requirements, initialization order considerations, and best practices. Next, we'll explore covariant return types—returning more specific types from overrides.