Python Part I · Module 16 · Object-Oriented Programming
Method Overriding
A child class that inherits a method it does not want simply writes its own, and the syntax is nothing more than defining a function with the same name. What makes this worth a chapter is that the new method inherits an obligation along with the name: somewhere there is code holding one of these objects without knowing which subclass it has, calling that method and expecting a particular kind of answer. The language will not check whether you kept that bargain, and the ways of breaking it are quiet.
Intermediate15 min readPythonPart IFundamentalsOOPInheritance
Part I · Module 16 · Chapter 16.11 · Method Overriding
1. Defining It Again Is All It Takes
class Job:
def __init__(self, name):
self.name = name
def describe(self):
return "job"
class SimulationJob(Job):
def describe(self):
return "simulation job"
for obj in (Job("alu"), SimulationJob("smoke")):
print(f"{type(obj).__name__:<14} {obj.describe()}")Job job
SimulationJob simulation jobSimulationJob.describe overrides Job.describe. Chapter 16.9 Section 4
explained the mechanism: Python walks the MRO and stops at the first class that
defines the name, so the child's version is found first.
Nothing is removed. The parent's method is still there and still reachable:
class Job:
def describe(self):
return "job"
class SimulationJob(Job):
def describe(self):
return "simulation job"
s = SimulationJob()
print("child's :", s.describe())
print("parent's :", Job.describe(s))
print("different :", SimulationJob.describe is not Job.describe)
print("mro :", [c.__name__ for c in SimulationJob.__mro__])child's : simulation job
parent's : job
different : True
mro : ['SimulationJob', 'Job', 'object']2. Why Overriding Is Useful: the Parent Calls It
If overriding only changed what you call, it would be a convenience. The power is that a method written in the parent picks up the child's version:
class Job:
def __init__(self, name):
self.name = name
def describe(self):
return "job"
def report(self):
"""Written once, in the parent. Never overridden."""
return f"[{self.describe()}] {self.name}"
class SimulationJob(Job):
def describe(self):
return "simulation"
class SynthesisJob(Job):
def describe(self):
return "synthesis"
for obj in (Job("alu"), SimulationJob("smoke"), SynthesisJob("top")):
print(obj.report())[job] alu
[simulation] smoke
[synthesis] topreport exists in one place and gives three answers. Neither subclass
mentions report; they changed describe, and report calls self.describe()
— which resolves against the actual object's class at call time.
That is Chapter 16.9 Section 8's fact doing real work, and it is the shape behind most useful inheritance: a parent defines the skeleton and children fill in the parts that differ.
3. Override or Extend
Chapter 16.10 Section 5 made this choice inside __init__. The same choice
applies to every method:
class Job:
def __init__(self, name):
self.name = name
def describe(self):
return f"job {self.name}"
class Extends(Job):
"""Keeps the parent's answer and adds to it."""
def describe(self):
return "VERBOSE: " + super().describe()
class Replaces(Job):
"""Discards the parent's answer entirely."""
def describe(self):
return f"<{self.name}>"
for obj in (Job("alu"), Extends("smoke"), Replaces("cdc")):
print(f"{type(obj).__name__:<9} {obj.describe()}")Job job alu
Extends VERBOSE: job smoke
Replaces <cdc>Both are legitimate. super() in an override says the parent's work still
applies; omitting it says mine replaces it.
A useful habit: if your override repeats logic that is already in the parent,
you wanted super().
4. Keep the Signature Compatible
Here is the contract the canonical purpose insists on. A caller holding "some
job" calls describe() with no arguments:
class Job:
def __init__(self, name):
self.name = name
def describe(self):
return f"job {self.name}"
class SimulationJob(Job):
def describe(self, verbosity): # added a REQUIRED parameter
return f"simulation {self.name} at verbosity {verbosity}"
def print_all(jobs):
"""Holds jobs without knowing which subclass each one is."""
for j in jobs:
print(j.describe())
print_all([Job("alu"), SimulationJob("smoke")])job alu
Traceback (most recent call last):
File "signature.py", line 20, in <module>
print_all([Job("alu"), SimulationJob("smoke")])
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "signature.py", line 17, in print_all
print(j.describe())
~~~~~~~~~~^^
TypeError: SimulationJob.describe() missing 1 required positional argument: 'verbosity'The first job printed and the second raised. print_all is correct code
written against the parent's interface; the subclass broke it by demanding an
argument the parent never did.
The fix is a default, which keeps the no-argument call working:
class Job:
def __init__(self, name):
self.name = name
def describe(self):
return f"job {self.name}"
class SimulationJob(Job):
def describe(self, verbosity=0):
if verbosity:
return f"simulation {self.name} (verbosity {verbosity})"
return f"simulation {self.name}"
def print_all(jobs):
for j in jobs:
print(j.describe())
print_all([Job("alu"), SimulationJob("smoke")])
print("explicitly:", SimulationJob("cdc").describe(2))job alu
simulation smoke
explicitly: simulation cdc (verbosity 2)An override may add optional parameters. It must not add required ones, and it must not remove any the parent accepts.
5. Honour the Promise, Not Just the Parameters
A compatible signature is necessary and not sufficient. The parent also promises something about the return:
class Job:
def __init__(self, name):
self.name = name
def describe(self):
"""Always returns a string."""
return f"job {self.name}"
class QuietJob(Job):
def describe(self):
"""Returns nothing when there is nothing to say -- breaks the promise."""
return None
def shout(jobs):
for j in jobs:
print(j.describe().upper())
shout([Job("alu"), QuietJob("smoke")])JOB ALU
Traceback (most recent call last):
File "promise.py", line 21, in <module>
shout([Job("alu"), QuietJob("smoke")])
~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "promise.py", line 18, in shout
print(j.describe().upper())
^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'upper'The signature matched perfectly. QuietJob.describe takes no arguments, just
like the parent — and returns None where the parent returns a string, so every
caller that treats the result as text fails.
'NoneType' object has no attribute is Chapter 14.3's most common error,
reached here through a design decision rather than a forgotten return.
The fix is to return the same kind of thing:
class Job:
def __init__(self, name):
self.name = name
def describe(self):
return f"job {self.name}"
class QuietJob(Job):
def describe(self):
return "" # still a string
def shout(jobs):
for j in jobs:
print(repr(j.describe().upper()))
shout([Job("alu"), QuietJob("smoke")])'JOB ALU'
''An empty string is falsy (Chapter 9.5), so a caller can still detect "nothing to say" — without the method lying about its type.
6. Overriding a Special Method
The same rules apply to the dunder methods from Chapter 16.8, and this fixes the wart Chapter 16.9 Section 3 left behind:
class Job:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"Job({self.name!r})"
class Inherited(Job):
pass
class Overridden(Job):
def __repr__(self):
return f"{type(self).__name__}({self.name!r})"
class Generic(Job):
pass
print("inherited :", Inherited("smoke"))
print("overridden:", Overridden("cdc"))
print("generic :", Generic("top"))inherited : Job('smoke')
overridden: Overridden('cdc')
generic : Job('top')Inherited and Generic both claim to be Job, which is wrong and misleading
in a log. Overridden reports its real class.
Better still, write the parent's __repr__ using type(self).__name__ in the
first place, and every subclass gets a correct one with no override at all:
class Job:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"{type(self).__name__}({self.name!r})"
class SimulationJob(Job):
pass
class SynthesisJob(Job):
pass
print([Job("alu"), SimulationJob("smoke"), SynthesisJob("top")])[Job('alu'), SimulationJob('smoke'), SynthesisJob('top')]One method, three correct answers — Section 2's principle applied to __repr__.
7. Extending a Special Method
Section 6 overrode __repr__ outright. The super() form from Section 3 works
on special methods too, and it is how a child adds its own fields to a parent's
representation without restating them:
class Job:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"{type(self).__name__}(name={self.name!r})"
class SimulationJob(Job):
def __init__(self, name, seed):
super().__init__(name)
self.seed = seed
def __repr__(self):
return super().__repr__()[:-1] + f", seed={self.seed})"
print("parent:", Job("alu"))
print("child :", SimulationJob("smoke", 1234))
print("in a list:", [Job("alu"), SimulationJob("cdc", 5678)])parent: Job(name='alu')
child : SimulationJob(name='smoke', seed=1234)
in a list: [Job(name='alu'), SimulationJob(name='cdc', seed=5678)]The child's __repr__ takes the parent's answer, drops its closing bracket and
appends its own field. name is formatted in exactly one place, so a change to
how the parent renders it reaches every subclass.
Note this works only because the parent used type(self).__name__ rather than
a hard-coded "Job" — Section 6's fix. The two techniques compose: the parent
makes the class name correct for every descendant, and each child adds what it
knows.
The [:-1] is a little crude, and the alternative is worth naming: a parent
method returning the parts rather than the finished string lets children
assemble rather than edit text. For two fields, trimming a bracket is fine; for
six, return a list.
8. Predict, Run, Observe
class A:
def who(self):
return "A"
def call_who(self):
return f"call_who sees {self.who()}"
class B(A):
def who(self):
return "B"
class C(B):
def who(self):
return "C via " + super().who()
for obj in (A(), B(), C()):
print(f"{type(obj).__name__}: {obj.who():<10} | {obj.call_who()}")
print("A.who on a C:", A.who(C()))A: A | call_who sees A
B: B | call_who sees B
C: C via B | call_who sees C via Bcall_who is defined only in A and reports three different things,
because self.who() is resolved against the real object each time.
C.who calls super().who(), which is B's — not A's. super() means
"the next class in the MRO", and for C that is B.
The last line calls A's version explicitly on a C instance, bypassing the
override. It prints A, which is occasionally useful in debugging and almost
never what you want in real code.
9. Exercises
Exercise 1 — override one. Write a parent with two methods and a child that overrides one. Confirm the other is still inherited.
Exercise 2 — the parent is still there. Call the parent's version explicitly on a child instance and show it differs.
Exercise 3 — the template. Write a report() in the parent that calls a
method three children override. Confirm you never touch report().
Exercise 4 — extend versus replace. Write both forms of an override and say in one sentence when you would choose each.
Exercise 5 — break the signature. Add a required parameter to an override and call it through code written for the parent. Record the error, then fix it with a default.
Exercise 6 — break the promise. Make an override return None where the
parent returns a string. Show the caller failing and say which error it is.
Exercise 7 — __repr__ for a family. Write a parent __repr__ using
type(self).__name__ and confirm two subclasses report themselves correctly
without overriding anything.
Exercise 8 — super in a chain. Build A/B/C where C's override calls
super(). Say which class it reaches and why.
10. Summary
- Defining a method with the same name overrides the parent's; the MRO finds the child's first.
- The parent's version is not removed and is still reachable as
Parent.method(obj). - A method in the parent picks up the child's override, because
selfresolves against the real class at call time. - That is the main payoff: a skeleton written once, with children filling in the parts that differ.
super()in an override extends the parent's answer; omitting it replaces it — both are legitimate, and repeating the parent's logic means you wantedsuper().- An override may add optional parameters, never required ones, or callers written for the parent break on exactly the subclass they were meant to accept.
- A compatible signature is not enough — the override must return the same kind of thing the parent promised.
- Returning
Nonewhere a string was promised produces'NoneType' object has no attribute, far from the class that caused it. - Special methods override like any other, and a parent
__repr__written withtype(self).__name__is correct for every subclass. - A child can extend a special method through
super(), adding its own fields to the parent's representation without restating them.
The next chapter is what all of this was for: code that works with any object offering the right method, without asking which class it is.
Where this fits
Part of the Python curriculum.
