Method overriding is how a subclass replaces or extends a method it would otherwise inherit unchanged. This lesson covers how Python decides which method to call, the difference between replacing and extending behavior, the Liskov substitution principle as a guide for safe overrides, the special case of overriding __init__ and dunder methods, and common pitfalls.
When a method is called on an instance, Python finds the most specific version of that method in the class hierarchy and runs it. The rule is the one shown earlier: walk from the instance's class up through its parents (and grandparents, and so on), and use the first matching method name. The walk stops at the first match; classes further up the chain are ignored for this call.
Both objects have a price of 19.99, but they're different classes. Python finds display_price on Product for the first object and on DiscountedProduct for the second. The override produces the different output; nothing about the call site changes.
This is called dynamic dispatch (also "late binding" in some textbooks). The decision about which method to run isn't made when the code is written; it's made when the call happens, based on the actual class of the object. Python looks up the method through the object's type, not through the variable name used to refer to it.
The two calls follow the same algorithm but find their match at different classes. The walk stops at the first definition; on the second call, Python never reaches Product.display_price because the subclass has its own.
The same dispatch happens when methods on the parent class call other methods. If Product.format_label calls self.display_price(), that call goes through the same dispatch and picks up DiscountedProduct.display_price for a discounted instance. The parent's code can call methods that the subclass has overridden, and the subclass's version is what runs.
This last point is what makes the Template Method pattern work. A parent class defines a method that orchestrates several steps; subclasses override individual steps without rewriting the orchestration.
Two common reasons to override a method. The first is replacement: the parent's version is wrong for this subclass, and the subclass needs to do something entirely different. The second is extension: the parent's version is right, but the subclass adds more work (before, after, or around the parent's logic).
Replacement looks like writing a brand-new method that ignores the parent:
CreditCard.charge and GiftCard.charge don't call super().charge(amount). They have nothing to extend; the parent's generic implementation is a placeholder, not something to build on. This is full replacement.
Extension uses super() to run the parent's logic and adds to the result:
DiscountedProduct.describe calls super().describe() to run Product.describe, then appends the discount tag. The override keeps the parent's format intact. If Product.describe ever changes (different currency formatting, added stock count), the override still produces sensible output because it doesn't duplicate any of the parent's logic.
The choice between replacement and extension is about whether the parent's logic is useful for this subclass.
super().method() and add to its result).super()).A third pattern, wrapping, has the subclass add work both before and after the parent's call:
The override runs code, calls super(), then runs more code. The before/after work bookends the parent's logic. The pattern is useful for cross-cutting concerns like logging, timing, or error handling.
Overriding is a sharp tool. It lets behavior change, but the change has to stay compatible with what callers of the parent class expect. The Liskov substitution principle, introduced in the single-inheritance lesson, matters most here.
The principle, stated briefly: anywhere code expects a parent instance, an instance of any subclass should work without surprises. In practical terms, an override:
Product.describe returns a string, DiscountedProduct.describe must also return a string, not a dictionary or None.charge never raises an error for valid amounts, the override shouldn't raise either.The following violation runs without errors but breaks calling code:
OverdraftAccount.withdraw substitutes for Account.withdraw in a way the caller of transfer couldn't anticipate. The parent guarantees that a successful return means the balance wasn't overdrawn; the subclass allows negative balances without signaling. Any auditing code, reporting code, or downstream logic that relied on the parent's guarantee produces wrong results without warning.
The fix is either to make the subclass honor the contract (raise a different exception when overdraft limits are hit, instead of allowing arbitrary negative balances), or to recognize that OverdraftAccount isn't a kind of Account in the strict sense and shouldn't inherit from it.
Liskov doesn't need to be memorized as a formal rule. The intuition is enough: if the override would surprise code calling the parent's interface, the contract is broken.
super().method()The mechanics of super() are the same as in the inheritance basics lesson, but the patterns are worth restating because overriding is where super() matters most.
The simplest pattern is "run the parent's version, then add to its result":
super() here is a shorthand. The full form is super(FlashDealProduct, self), but Python 3 allows super() without arguments inside any method and figures out the rest from context.
A second pattern is "wrap the parent": run code, call super(), run more code. The TimestampedLogger example earlier showed this. It's the same idea as decorators applied to methods, expressed through inheritance.
A third pattern is "delegate conditionally": call super() only in some cases. The pattern fits when the override sometimes wants the parent's behavior and sometimes doesn't:
For one item, the subclass falls back to the parent's calculation. For more, it applies its own discount logic. The override picks when to defer and when to take over, instead of always doing one or the other.
super().method() is one extra function call per level. The overhead is negligible for normal use. In hot loops where every fraction of a microsecond matters, profile first before optimizing.
A common pattern built on top of method overriding is the Template Method pattern. The parent class defines a method that lays out the overall flow of an operation, calling smaller methods at each step. Subclasses override only the small steps they care about and inherit the rest. The overall flow stays consistent across all subclasses, and each subclass customizes the pieces that differ.
ReportGenerator.generate is the template. It always does the same three steps in the same order: collect rows, format them, wrap the result. The subclasses don't override generate at all. Each one customizes the two steps that differ for its output format, and the orchestration stays untouched.
This is the dispatch rule (from earlier) doing useful work. When generate calls self.format_rows(rows), dispatch picks the right override for whichever subclass the instance belongs to. CsvReport().generate(data) runs ReportGenerator.generate, which calls self.format_rows and gets CsvReport.format_rows. The parent doesn't need to know what subclasses exist; it calls through self and lets dispatch sort it out.
The methods the parent calls are sometimes called hook methods: places where subclasses are expected to plug in their customization. A well-designed template documents which methods are hooks (intended to be overridden) and which are part of the orchestration (not meant to be touched). Without that documentation, the structure has to be reconstructed by tracing through the code.
__init____init__ is the method most commonly overridden, and the rules around it are worth restating because the consequences of getting them wrong are visible right away.
The key facts:
__init__ replaces the parent's entirely. Python doesn't call the parent's __init__ automatically when both classes have one.super().__init__(...) with whatever arguments the parent expects.super().__init__(...) is called at the top of the subclass's __init__, before any subclass-specific work. This ensures parent attributes exist before the subclass touches anything.A correct subclass __init__:
GiftOrder.__init__ first runs super().__init__(order_id, customer) so that order_id, customer, and items are set by Order.__init__. Then it adds the gift-specific attributes. Order matters: accessing self.items before super().__init__(...) would raise AttributeError because the attribute wouldn't exist yet.
A common variation is to fix some of the parent's arguments and not expose them to the subclass's caller:
The subclass doesn't have to pass through every parent argument unchanged. It can hard-code defaults or set fields the parent left out. The only requirement is that whatever the parent's __init__ needs has to come from somewhere.
Consider this broken code:
The override omitted super().__init__(name, email), so Customer.__init__ never ran. bob.tier works because the override did set that. bob.name fails because nothing set it. The fix is the standard pattern:
This bug is a frequent mistake when overriding __init__. It's easy to spot once recognized, but it doesn't fail until something tries to read a missing attribute, which can be far from the actual __init__ definition.
Dunder (double-underscore) methods are how classes integrate with Python's operators and builtins. Overriding them is how objects behave correctly with ==, +, len(), print(), and others. The rules for overriding dunder methods differ a bit because Python itself calls them, not user code, so the contract has to be tight.
A common override is __eq__, which controls what == means for the class. The default (inherited from object) compares object identity, which is rarely the right behavior for value-like classes:
Two Product instances with the same data still compare as unequal because object.__eq__ compares whether they're the same object in memory. Overriding __eq__ defines equality by content:
A few important details about overriding __eq__:
other isn't a Product at all. Returning NotImplemented (a special builtin singleton) tells Python "comparison undefined here; try the other operand's __eq__". Returning False would say "they're definitely unequal", which is a stronger claim and can interact badly with other comparisons.__hash__ for set membership and dict keys, and the contract is that equal objects must hash to the same value. Overriding __eq__ alone causes Python to set __hash__ to None, making instances unhashable (they can't be put in sets or used as dict keys). The fix is usually to override __hash__ to return a hash of the same fields compared in __eq__.Two of the three products are considered equal (same product_id), so the set keeps only one of them and the pen, giving a size of two.
Other dunders that often get overridden:
| Dunder | Called by | Purpose |
|---|---|---|
__str__ | str(), print() | Human-readable string representation |
__repr__ | repr(), REPL display | Unambiguous representation for debugging |
__eq__ | == | Equality comparison |
__hash__ | hash(), set/dict keys | Integer hash for use in hash-based containers |
__lt__, __le__, etc. | <, <=, sorting | Ordering comparisons |
__len__ | len() | Object's "size" |
__iter__ | for, list comprehensions | Iteration |
__contains__ | in | Membership testing |
Each of these has a specific contract Python expects. Overriding them is how classes become first-class participants in the language. The course has a separate dunder methods chapter that goes deeper; the relevant point for overriding is that dunder methods are still methods, and the same dispatch rule applies. A subclass can override __eq__ to provide a more specific notion of equality, like any other method.
A few patterns to watch for when overriding methods.
Changing the signature. If the parent's method is def save(self, validate=True): and the subclass overrides it as def save(self):, calls that pass validate=False to the parent's interface fail on subclass instances. The override has narrowed the API by accident. Subclasses should accept at least everything the parent accepted, even if they ignore some of it.
The fix is to accept the same parameters, even if the override doesn't use them:
Returning a different type. Callers of the parent's method have built expectations around its return type. Changing it in the override breaks every assumption built on top. If the parent returns a dict, the override should return a dict (or a subclass of dict), not a list or None.
Calling `self.method()` instead of `super().method()`. Inside an override, self.method(...) calls the method through dispatch, which finds the override itself, causing infinite recursion. To call the parent's version explicitly, use super().method(...).
The correct version:
Forgetting that the parent's code might call the overridden method. As shown in the Template Method section, if the parent's code calls self.format_rows(...) and the subclass overrides format_rows, the override is what runs. This is usually the goal, but it can also surprise code that didn't expect the parent to call into the override. Read the parent class to see which of its methods call other methods on self, especially when those other methods are being overridden.
Overriding a method by accident. A method on a subclass with the same name as something a parent already defined (even when unintended) shadows the parent. This is most painful when the parent is object or a builtin like list: subclasses of list that define a method called index or pop are shadowing the list's own without warning. Use a name that doesn't conflict, or confirm the override is intentional.
The pattern across all of these is the same: an override changes how a method behaves, and other code (the parent's own code, the language runtime, callers) might depend on the original behavior in ways that aren't obvious from a single file. Treat overrides as contracts being modified, not as new methods being freely written.
10 quizzes