Worked examples — Method overriding — when and why
This page is one big practice arena. The parent note built the idea of overriding (same signature, real object wins, chosen at runtime). Here we hit every kind of situation overriding can throw at you — the clean cases and the traps. Before each example, you forecast the answer, then we grind through it.
The scenario matrix
Every row is a "case class" this topic can produce. Each worked example below is tagged with the cell it covers, and every example carries its own figure.
| # | Case class | What makes it tricky | Example |
|---|---|---|---|
| A | Pure specialize | child totally replaces parent body | Ex 1 |
| B | Extend with super |
child adds to parent body | Ex 2 |
| C | Dispatch through base variable | declared type ≠ real type | Ex 3 |
| D | Overload look-alike (NOT override) | different params → picked at compile time | Ex 4 |
| E | Abstract contract (degenerate base) | parent body is empty/forbidden | Ex 5 |
| F | Static "override" trap (hiding) | no dynamic dispatch happens | Ex 6 |
| G | Liskov limit (narrowing = illegal) | override must stay substitutable | Ex 7 |
| H | Real-world word problem | many shapes, one loop, zero if/else |
Ex 8 |
| I | Degenerate/limiting input | zero radius, empty list | Ex 9 |
| J | Exam-style twist | mixed override + overload + super in one trace |
Ex 10 |
| K | Blocked override (final / sealed) |
parent forbids overriding at all | Ex 11 |
Prerequisites you may want open: Inheritance, Polymorphism, Method overloading, Abstract classes and interfaces, Liskov Substitution Principle, Open-Closed Principle, super keyword. (These are cross-links to sibling notes in the same study vault — click one to review that concept if a word here feels new.)
Example 1 — Pure specialize (cell A) · 🟦 Python
Example 2 — Extend with super (cell B) · 🟦 Python
Example 3 — Dispatch through a base-typed variable (cell C) · 🟦 Python
Example 4 — Overload look-alike, NOT override (cell D) · 🟧 Java-style
Statement. Printer has two render methods (different signatures — see the definition up top); a child overrides one of them. Trace which body runs.
class Printer {
String render(String s) { return "str:" + s; }
String render(int n) { return "int:" + n; } // OVERLOAD (diff params)
}
class Fancy extends Printer {
@Override
String render(String s) { return "FANCY:" + s; } // OVERRIDE (same signature)
}
Printer p = new Fancy();
p.render("x"); // ?
p.render(7); // ?Forecast: which line uses runtime dispatch, which uses compile-time selection?
Picture it. The figure splits each call into two stages: compile time (blue) picks which slot by argument type — String vs int; runtime (orange) picks which body fills that slot by the real object. Only the String slot has a child override, so only that call changes.

p.render("x"). Argument isString, so at compile time therender(String)slot is chosen (overload resolution by argument type). At runtime the object isFancy, whose override fills that slot →"FANCY:x". Why this step? Overloading picks the slot by argument type (compile time); overriding decides which body fills that slot (runtime). Both mechanisms act — one after the other.p.render(7). Argument isint, so therender(int)slot is chosen.Fancynever overrode that signature → parent body →"int:7". Why this step? No child override exists for that signature, so the inheritedPrinter.render(int)runs.
Verify: results "FANCY:x" and "int:7" ✓. Same word render, two totally different resolution mechanisms.
Example 5 — Abstract contract, degenerate base body (cell E) · 🟦 Python
Statement. The base method is forbidden to call directly. Every child must override. What happens if we try the base version?
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
raise NotImplementedError # explicit "no body yet" — never meant to run
class Square(Shape):
def __init__(self, a): self.a = a
def area(self): return self.a ** 2
print(Square(6).area())
# Shape() # would raise TypeError: cannot instantiate abstract classWhy raise NotImplementedError and not ...? You will often see abstract bodies written as a bare ... (the Ellipsis object) or pass — both are just placeholders meaning "nothing here". For a beginner they are confusing because they look like the method quietly returns nothing. We instead write raise NotImplementedError, which makes the intent loud: if anyone ever does reach this body, it will crash with a clear message. All three forms work; the explicit raise is the friendliest to read.
Forecast: Square(6).area() = ? And can you even build a bare Shape()?
Picture it. The figure shows the abstract base as a box with a hole (dashed, no body). A red "✗ cannot instantiate" arrow bounces off Shape; the green path goes to Square, which fills the hole and returns 36.

Square(6).area()runs the child override:6^2 = 36. Why this step? The contract forces a body, so dispatch always finds a real implementation.Shape()would fail at construction. Why this step? An abstract base with an unfilledabstractmethodis a degenerate type — it has a hole. Python refuses to make one, guaranteeing no caller ever hits an emptyarea.
Verify: 6^2 = 36 ✓. The degenerate base can never be instantiated, so the "empty body" case is unreachable — exactly the safety abstract classes give you.
Example 6 — Static "override" trap = hiding (cell F) · 🟧 Java-style
Statement. A static method is redeclared in a child. Is it overridden (runtime) or hidden (compile time)?
class Base { static String who() { return "Base"; } }
class Sub extends Base { static String who() { return "Sub"; } }
Base b = new Sub();
Base.who(); // ?
Sub.who(); // ?
b.who(); // ? (the "dangerous form" — calling a static via an instance)Forecast: for b.who(), does the real object Sub win like normal overriding — or does the declared type Base win?
Picture it. The figure contrasts two resolution paths: a normal instance method (orange) follows the real object to Sub; a static method (gray) ignores the object entirely and resolves by the declared type Base. The b.who() call is drawn taking the gray path, ending on "Base" — the surprise.

-
Base.who()→"Base";Sub.who()→"Sub". Why this step? Called on the class name — no object, no dispatch, purely the named class. -
b.who()→"Base", not"Sub". Why this step? Statics use hiding: resolution is by the declared type ofb(which isBase) at compile time. There is no vtable slot for a static, so the real object is ignored.
Why b.who() is the "dangerous form". It compiles fine and looks like a normal method call that would dispatch on the object — but it silently resolves by the declared type. A reader glancing at b.who() naturally expects "Sub" (the real object), yet gets "Base". Because the mismatch is invisible, it hides real behaviour and breeds bugs. Best practice: always call statics on the class name (Base.who()), never on an instance, so the resolution rule is obvious in the code.
Verify: outputs "Base", "Sub", "Base" ✓. The surprise is the third — proof that statics are hidden, not overridden.
Example 7 — Liskov limit: narrowing is illegal (cell G) · 🟧 Java-style (Java 5+)
Statement. Parent returns a Bird; a child wants to narrow the return to a Sparrow (a subtype). Legal? Then a child tries to narrow access from public to private. Legal? First we must define the classes so the snippet actually compiles:
// Define the type hierarchy the example relies on:
class Bird { }
class Sparrow extends Bird { } // Sparrow IS-A Bird
class Aviary { public Bird get() { return new Bird(); } }
class SparrowHouse extends Aviary {
@Override public Sparrow get() { return new Sparrow(); } // covariant return — OK (Java 5+)
}
// class Bad extends Aviary { private Bird get() {...} } // ILLEGAL: narrows accessWhat "covariant return" means and when it works. Before Java 5, an override had to return the exact same type as the parent. Since Java 5, an override may return a subtype of the parent's return type — this is a covariant return type. The compiler enforces it: it checks that Sparrow is a subtype of Bird; if it is, the override is accepted, and any caller written against Aviary.get() still receives something usable as a Bird. If you tried to return a non-subtype, compilation would fail. C# added the same feature in C# 9.
Forecast: which of the two changes keeps substitutability?
Picture it. The figure ranks access by reachable audience on a number-line (private → protected → public). A green arrow points "widen (allowed)" toward public; a red arrow points "narrow (illegal)" toward private. Alongside, a small tree shows Sparrow under Bird, marked ✓ covariant.

-
Covariant return
Sparrow. ASparrowis aBird, so any caller expecting aBirdfromget()still gets a validBird. Why this step? Returning a subtype only ever gives callers more than promised — never less. Substitutability holds. This is the one legal tightening. -
Narrowing access
public → private. A caller ofAviary.get()could no longer call it on aBadobject. Why this step? That removes an ability the parent promised, soBadis not a drop-in replacement. The compiler rejects it.
Think "reachable audience", not sets. Rank access by how many callers can reach the method: private (only inside the class) → protected (subclasses too) → public (everyone). An override may keep the audience the same or larger (widen), never smaller (narrow), because parent code counted on that audience being allowed in. Widening public is fine; shrinking to private locks out callers the parent invited — that breaks substitutability.
Verify: covariant return compiles (Sparrow is-a Bird → callers still satisfied); access-narrowing rejected. Direction rule as booleans: "widen allowed" = True, "narrow allowed" = False.
Example 8 — Word problem: billing many shapes, zero if/else (cell H) · 🟦 Python
Statement. A tiling company charges $5 per square metre. Given a mixed list [Circle(1), Rectangle(2,3), Square(2)], compute the total bill using one loop and no type checks.
import math
class Shape:
def area(self): raise NotImplementedError
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return math.pi*self.r**2
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w*self.h
class Square(Shape):
def __init__(self, a): self.a = a
def area(self): return self.a**2
shapes = [Circle(1), Rectangle(2,3), Square(2)]
total_area = sum(s.area() for s in shapes)
bill = 5 * total_area
print(round(total_area, 4), round(bill, 4))Forecast: add the three areas, multiply by 5. Estimate the bill first.
Picture it. The figure is a conveyor belt: three different shapes ride into one area() call, each dispatching to its own body, and their numbers drop into a single running sum (green), then scale by $5. No if/else diamond appears anywhere.

- Circle(1):
pi * 1^2 = pi = 3.1416. Why this step? Dispatch →Circle.area. - Rectangle(2,3):
2 * 3 = 6. Square(2):2^2 = 4. Why this step? Each element dispatches to its own body — the loop never asks "what type is this?". - Total area:
pi + 6 + 4 = 10 + pi = 13.1416. - Bill:
5 * (10 + pi) = 50 + 5*pi = 65.708. Why this step? This is Open/Closed in action — a new shape is a new class, and this billing loop never changes.
Verify: total area = 10 + pi ≈ 13.1416 ✓, bill = 50 + 5*pi ≈ 65.708 ✓. Units: m² × $/m² = $ ✓.
Example 9 — Degenerate / limiting inputs (cell I) · 🟦 Python
Statement. Two edge cases: (a) a Circle(0) — zero radius; (b) an empty list of shapes fed to the billing sum.
import math
print(math.pi * 0**2) # zero radius
print(sum(s.area() for s in [])) # empty listForecast: what is the area of a zero-radius circle, and the total of no shapes?
Picture it. The figure shows a circle shrinking to a single point as r → 0 (its area label falling to 0), and beside it an empty box feeding into sum, whose output stays pinned at the starting value 0.

Circle(0):pi * 0^2 = 0. Why this step? Overriding doesn't change arithmetic — a degenerate circle collapses to a point, area 0. No special-case code needed.- Empty list:
sum(...)of nothing= 0. Why this step?sumstarts from a starting value (its identity element) of0and then adds each item to it. With zero items, nothing is added, so it simply returns that starting0. That is why it does not crash and does not returnNone— the empty case is defined to give the additive identity0, so the polymorphic loop degrades gracefully.
Verify: area = 0 ✓, empty total = 0 ✓. Both limiting inputs are safe — no NotImplementedError, no divide-by-zero.
Example 10 — Exam twist: override + overload + super in one trace (cell J) · 🟦 Python
Statement. Trace the exact output. B overrides greet() and calls super(); it also gives tag a second parameter so one method handles two call shapes (Python's stand-in for overloading).
class A:
def greet(self): return "A"
def tag(self): return "tagA"
class B(A):
def greet(self): # OVERRIDE (same signature as A.greet)
return super().greet() + "B" # deliberately runs the parent body first
def tag(self, x=None): # replaces A.tag; default arg fakes overloading
return "tagB" if x is None else f"tagB:{x}"
b = B()
print(b.greet()) # ?
a = B() # a genuine B, viewed through the A interface
print(a.greet()) # ?
print(b.tag()) # ?
print(b.tag(9)) # ?Forecast: four lines. Watch the "real object always wins" rule on the second one.
Picture it. The figure is a trace table: each of the four calls is a row, with a small arrow showing where dispatch lands (B.greet, B.greet, B.tag, B.tag) and the produced string in a green cell. The super() hop inside greet is drawn as a dashed arrow up to A.greet.

b.greet(). Real object isB, so its override runs. Inside it,super().greet()returns"A", then+ "B"gives"AB". Why this step? Override runs (dispatch on real object) andsuperdeliberately pulls the parent body — both mechanisms in a single line.a.greet()whereais anotherB→ still"AB". Why this step? Runtime object decides, exactly like Example 3. Even if you think ofathrough the baseAinterface, dispatch uses the realB.b.tag()→xdefaults toNone→ returns"tagB". Why this step? Python has no compile-time overloading; the onetaguses its default parameter to cover the "no argument" call shape.b.tag(9)→x = 9(notNone) → returns"tagB:9". Why this step? Same single method, different argument path — theif/elseonxdoes the work a second overloaded method would do in Java.
Verify: outputs "AB", "AB", "tagB", "tagB:9" ✓.
Example 11 — Blocked override: final / sealed (cell K) · 🟧 Java / C#
Statement. The parent forbids overriding a method. What happens when a child tries anyway?
// Java
class Ledger {
final int checksum() { return 42; } // 'final' = cannot be overridden
}
class HackedLedger extends Ledger {
int checksum() { return 0; } // COMPILE ERROR: cannot override final
}// C# (modern): a virtual method may be 'sealed' in a subclass to stop further overriding
class Ledger { public virtual int Checksum() => 42; }
class SafeLedger : Ledger { public sealed override int Checksum() => 42; }
class Evil : SafeLedger { public override int Checksum() => 0; } // COMPILE ERROR: sealedForecast: does the child's checksum ever run, or does the code even compile?
Picture it. The figure draws a "locked door": Ledger.checksum has a padlock icon; the child's attempted override (red) bounces off the padlock with a "✗ compile error" label, while the legal value 42 sits safely inside.

-
Java
final. The compiler rejectsHackedLedger.checksum()outright. Why this step?finalon a method is a hard promise: "this body is the only body, forever." No override slot is ever reserved for a child version, so there is nothing to override. The program does not build. -
C#
sealed override.SafeLedgeroverrodeChecksumonce, then sealed it — closing the door for its own subclasses.Evil.Checksum()fails to compile. Why this step?sealedlets a class stop the override chain at itself: the method stays polymorphic up toSafeLedger, butEvilis blocked.
Why block overriding at all? For security and invariants — a checksum, an equality rule, or a licensing check must not be silently swapped out by a subclass. final/sealed is the OO way of saying "this behaviour is load-bearing; do not touch." Trying to override it is a compile-time error, so the trap is caught before the program ever runs.
Verify: neither snippet compiles; the correct, expected checksum value is 42 in the legal (un-overridden) classes, never 0.
Recall
Runtime type of the object decides which override runs ::: Yes — the declared/static type only controls whether the call compiles (in typed languages).
A method's signature is ::: its name plus the number/order/types of its parameters (return type usually excluded).
How does Python find which area to run? ::: It walks the object's class MRO (e.g. Circle → Shape → object) and takes the first match — not a fixed vtable.
A zero-radius circle's overridden area() ::: 0 (no special case needed).
Static method redeclared in a child ::: Hidden, resolved by declared type at compile time — not overridden.
Calling a static via an instance (b.who()) is dangerous because ::: it compiles but resolves by declared type, hiding behaviour and misleading readers.
super().greet() inside an override ::: Deliberately runs the parent's body, then the child adds to it.
When did Java allow covariant return types, and who enforces them? ::: Java 5+; the compiler checks the child's return is a subtype of the parent's.
Legal way to "tighten" an override's return type ::: Return a subtype (covariant return) — never narrow access.
Overriding a final (Java) / sealed (C#) method ::: Impossible — it is a compile-time error; the parent forbids it to protect an invariant.
Why sum([]) returns 0 and not None ::: sum starts from the additive identity 0 and adds nothing, so it returns that 0.
Matrix walk: Specialize, Super-extend, dispatch-through-Base, Overload-not-override, Abstract-contract, Static-hides, Liskov-limits, Word-problem, Degenerate-safe, Exam-trace, Blocked-by-final. Eleven cells — press every button once and nothing surprises you.