Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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()}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Job            job
SimulationJob  simulation job

SimulationJob.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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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__])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[job] alu
[simulation] smoke
[synthesis] top

report 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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()}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
signature.py (BROKEN ON PURPOSE)
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")])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
promise.py (BROKEN ON PURPOSE)
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")])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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")])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
'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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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")])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: A          | call_who sees A
B: B          | call_who sees B
C: C via B    | call_who sees C via B

call_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 self resolves 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 wanted super().
  • 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 None where 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 with type(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.