Skip to content
VLSI Mentor

Python Part I · Module 16 · Object-Oriented Programming

Inheritance

Two kinds of job in a flow — a simulation and a synthesis run — share most of what they do and differ in a little. Writing both from scratch duplicates the shared part; writing one and special-casing it with flags produces a class that is three classes in a trench coat. Inheritance is the third option: declare that one class is built on another, and it receives everything the other has without a line of code. What it receives, and exactly when Python decides where a name comes from, is more worth knowing than the syntax — and the feature is also the most over-used in the module, so the test for when not to reach for it arrives in the same chapter.

Intermediate16 min readPythonPart IFundamentalsOOPInheritance

Part I · Module 16 · Chapter 16.9 · Inheritance

1. You Have Already Written One

Chapter 14.8 defined a custom exception:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ToolError(RuntimeError):
    """A tool we invoked failed."""


e = ToolError("vcs exited 137")
print("message :", e)
print("class   :", type(e).__name__)
print("is a RuntimeError:", isinstance(e, RuntimeError))
print("is an Exception  :", isinstance(e, Exception))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
message : vcs exited 137
class   : ToolError
is a RuntimeError: True
is an Exception  : True

ToolError has a body of one docstring and nothing else, yet it stores a message, prints it, and can be caught as a RuntimeError. All of that came from the name in the parentheses.

That is inheritance, and this chapter is about what it did.

2. The Syntax and the Claim

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    def __init__(self, name):
        self.name = name

    def run(self):
        return f"running {self.name}"

    def describe(self):
        return f"job {self.name}"


class SimulationJob(Job):
    pass


s = SimulationJob("smoke")
print("run()     :", s.run())
print("describe():", s.describe())
print("name      :", s.name)
print("own body defines:", sorted(n for n in SimulationJob.__dict__ if not n.startswith("__")))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
run()     : running smoke
describe(): job smoke
name      : smoke
own body defines: []

SimulationJob defines nothing — the last line proves it. It has run, describe, and an __init__ that stores name, because class SimulationJob(Job): says it is built on Job.

Vocabulary: Job is the parent (or base, or superclass); SimulationJob is the child (or derived class, or subclass).

Three blocks in a row. In the centre is the parent class Job, which defines run and describe. On the left, SimulationJob, which adds a seed; on the right, SynthesisJob, which adds a target library. An arrow labelled inherits points from each child inward to Job, because each one receives everything Job defines.SimulationJobadds a seedJob (the parent)defines run(), describe()SynthesisJobadds a target libraryinheritsinherits12
Figure 1 — one parent, two children. Each child receives everything Job defines without restating it and adds only what differs. Both arrows point at the parent, which is the direction Python searches when it looks a name up.

3. What the Child Receives

Everything, and it is worth being precise about the list:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    default_timeout = 3600          # a class attribute

    def __init__(self, name):       # a constructor
        self.name = name

    def run(self):                  # a method
        return f"running {self.name}"

    def __repr__(self):             # a special method
        return f"Job({self.name!r})"


class SimulationJob(Job):
    pass


s = SimulationJob("smoke")

print("class attribute :", s.default_timeout)
print("instance attr   :", s.name)
print("method          :", s.run())
print("special method  :", repr(s))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class attribute : 3600
instance attr   : smoke
method          : running smoke
special method  : Job('smoke')

All four worked. Note the __repr__: it says Job(...) even though the object is a SimulationJob, because the inherited method hard-codes the word. That is a real wart, and Chapter 16.11 fixes it by overriding.

4. How Python Finds a Name

Not by copying — by searching, at the moment of the call:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    def run(self):
        return "Job.run"


class SimulationJob(Job):
    pass


print("search order:", [c.__name__ for c in SimulationJob.__mro__])
print("same function object:", SimulationJob.run is Job.run)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
search order: ['SimulationJob', 'Job', 'object']
same function object: True

__mro__ is the method resolution order: the list of classes Python walks, in order, looking for a name. SimulationJob first, then Job, then object — the class every class inherits from, which is where Chapter 16.8's default __repr__ came from.

Because the search happens at call time, changing the parent changes existing children immediately:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    def run(self):
        return "original"


class SimulationJob(Job):
    pass


s = SimulationJob()
print("before:", s.run())

Job.run = lambda self: "replaced at runtime"

print("after :", s.run())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before: original
after : replaced at runtime

The instance already existed and picked up the new version, because the lookup walks the MRO every time rather than being resolved once at creation.

5. isinstance Follows the Chain

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    pass


class SimulationJob(Job):
    pass


s = SimulationJob()

print("isinstance(s, SimulationJob):", isinstance(s, SimulationJob))
print("isinstance(s, Job)          :", isinstance(s, Job))
print("type(s) is Job              :", type(s) is Job)
print("type(s) is SimulationJob    :", type(s) is SimulationJob)
print("issubclass(SimulationJob, Job):", issubclass(SimulationJob, Job))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
isinstance(s, SimulationJob): True
isinstance(s, Job)          : True
type(s) is Job              : False
type(s) is SimulationJob    : True
issubclass(SimulationJob, Job): True

isinstance(s, Job) is True — a SimulationJob is a Job. That is the whole promise, and the reason Chapter 14.8's except FlowError could catch a ParseError.

type(s) is Job is False — type() asks for the exact class and ignores the hierarchy. Writing type(x) is SomeClass where you meant isinstance is a common way to break polymorphic code, because it rejects every subclass.

6. The Inherited Mutable Attribute

Chapter 16.4 Section 5 showed a mutable class attribute is shared by all instances. With inheritance it is shared across the whole family, and the sharing is invisible from the child's source:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    log = []                    # one list, for every descendant

    def __init__(self, name):
        self.name = name

    def record(self, msg):
        self.log.append(f"{self.name}: {msg}")


class SimulationJob(Job):
    pass


class SynthesisJob(Job):
    pass


a = SimulationJob("smoke")
b = SynthesisJob("top")

a.record("started")

print("a.log      :", a.log)
print("b.log      :", b.log)
print("Job.log    :", Job.log)
print("one object :", a.log is b.log is Job.log)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a.log      : ['smoke: started']
b.log      : ['smoke: started']
Job.log    : ['smoke: started']
one object : True

A synthesis job is reporting a simulation job's log line. Nothing in SynthesisJob's source mentions a list at all — the shared state is in the parent, possibly in another file.

The fix is Chapter 16.4's: mutable state belongs in __init__, one list per instance.

7. The Test: Is-A, Not Has-A

Inheritance is over-used immediately after it is learned. The test:

Can every instance of the child be used wherever the parent is expected?

If yes, it is a subclass. If you catch yourself thinking "but this one does not really have a run()", it is not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    def __init__(self, name):
        self.name = name

    def run(self):
        return f"running {self.name}"


class SimulationJob(Job):
    """A simulation IS a job -- it runs, it has a name."""


class Regression:
    """A regression HAS jobs. It is not one."""

    def __init__(self, jobs):
        self.jobs = jobs

    def run_all(self):
        results = []
        for j in self.jobs:
            results.append(j.run())
        return results


r = Regression([SimulationJob("smoke"), SimulationJob("cdc")])
print(r.run_all())
print("Regression is a Job:", isinstance(r, Job))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['running smoke', 'running cdc']
Regression is a Job: False

Regression could have been written class Regression(Job): and it would have worked — it would have acquired a name and a run() that mean nothing for a collection. The substitution test rejects it: handing a Regression to code expecting a Job would call a run() that runs one imaginary job instead of all of them.

That is composition, and Chapter 16.13 is about why it is the default.

8. Inheriting From a Built-In

The parent does not have to be your class. Python's own types are classes (Chapter 16.1 Section 2), and you can build on them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Filelist(list):
    """A list of source files that can emit itself in the tool's format."""

    def as_text(self):
        return "\n".join(self)


f = Filelist(["rtl/alu.sv", "rtl/decoder.sv"])
f.append("tb/env.sv")

print("len        :", len(f))
print("indexing   :", f[0])
print("is a list  :", isinstance(f, list))
print("mro        :", [c.__name__ for c in Filelist.__mro__])
print("own method :")
print(f.as_text())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
len        : 3
indexing   : rtl/alu.sv
is a list  : True
mro        : ['Filelist', 'list', 'object']
own method :
rtl/alu.sv
rtl/decoder.sv
tb/env.sv

Filelist received every list method — append, indexing, len(), iteration — and added one of its own. isinstance(f, list) is True, so it can be passed to anything expecting a list.

This is occasionally exactly right, and more often a trap. The substitution test from Section 7 passes easily here, but a second question applies: do you want every list method? Filelist has inherited sort, reverse, clear and pop, none of which it has an opinion about, and a caller can empty it.

For most "a collection plus a bit of behaviour" cases, composition (Chapter 16.13) is the better answer — hold a list rather than being one. Inherit from a built-in when you genuinely want the full interface, which is rarer than it looks.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Base:
    kind = "base"

    def describe(self):
        return f"I am {self.kind}"


class Child(Base):
    kind = "child"


class GrandChild(Child):
    pass


for obj in (Base(), Child(), GrandChild()):
    names = [c.__name__ for c in type(obj).__mro__]
    print(f"{type(obj).__name__:<11} {obj.describe():<14} mro={names}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Base        I am base      mro=['Base', 'object']
Child       I am child     mro=['Child', 'Base', 'object']
GrandChild  I am child     mro=['GrandChild', 'Child', 'Base', 'object']

describe is defined once, in Base, and gives different answers.

It reads self.kind, and self.kind resolves by walking the MRO of the actual object — so a Child finds its own kind before reaching Base's.

GrandChild says "child" because it defines no kind and the search reaches Child first.

This is the mechanism behind every remaining chapter in the module: a method written in a parent calls back into whatever the real object turns out to be.

10. Exercises

Exercise 1 — the empty child. Write a parent with two methods and a child whose body is pass. Call both methods on the child.

Exercise 2 — what was received. Print the child's __dict__ and confirm it defines nothing while the methods still work.

Exercise 3 — the MRO. Print __mro__ for a three-level hierarchy and say which class supplies each name.

Exercise 4 — same function. Show that Child.method is Parent.method for an inherited method, and explain what that proves about copying.

Exercise 5 — late change. Create an instance, then replace a method on the parent. Confirm the existing instance picks it up.

Exercise 6 — isinstance versus type. For a child instance, evaluate isinstance(x, Parent) and type(x) is Parent. Say which you would use in a function accepting any job.

Exercise 7 — the shared list. Reproduce Section 6. Then fix it and prove two siblings no longer share.

Exercise 8 — is-a or has-a. For each, choose: (a) TimeoutError and Exception (b) Regression and Job (c) SynthesisJob and Job (d) Report and a list of results (e) PosixPath and Path.

11. Summary

  • class Child(Parent): builds one class on another; the child receives methods, class attributes, __init__ and special methods.
  • A child with a body of pass already works — you wrote one in Chapter 14.8.
  • Nothing is copied. Python searches the MRO at call time, so Child.method is Parent.method.
  • Because lookup is at call time, changing a parent changes existing instances immediately.
  • object is at the end of every MRO, which is where the default __repr__ comes from.
  • isinstance follows the chain; type(x) is C does not — the second rejects every subclass.
  • A mutable class attribute is shared across the whole family, invisibly from the child's own source.
  • A method in the parent reads self, so it resolves against the real object's class — the mechanism behind overriding and polymorphism.
  • You can inherit from a built-in such as list, receiving its entire interface — including the methods you did not want.
  • The test is substitution: can every child be used wherever the parent is expected?
  • Inheriting merely to reuse code is the common mistake; that is composition.

The next chapter adds to a parent's behaviour instead of replacing it.

Where this fits

Part of the Python curriculum.