Skip to content
VLSI Mentor

Python Part I · Module 16 · Object-Oriented Programming

Child Classes and super()

A child class that receives everything is useful for about five minutes, because the whole reason for creating one is that it differs. The moment it needs its own constructor, a decision appears that Python will not make for you: does the parent's setup still happen, or does yours take over entirely? Both are legal, neither produces an error, and getting it wrong builds an object that is half-constructed and will not say so until something reaches for a field that was never set.

Intermediate15 min readPythonPart IFundamentalsOOPInheritance

Part I · Module 16 · Chapter 16.10 · Child Classes and super()

1. The Child Needs Its Own Fields

Chapter 16.9's SimulationJob inherited everything and added nothing. A real one has a seed:

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

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


class SimulationJob(Job):
    def __init__(self, name, seed):
        super().__init__(name)
        self.seed = seed


s = SimulationJob("smoke", 1234)
print("name    :", s.name)
print("status  :", s.status)
print("seed    :", s.seed)
print("inherited:", s.describe())
print("storage :", s.__dict__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
name    : smoke
status  : PENDING
seed    : 1234
inherited: job smoke (PENDING)
storage : {'name': 'smoke', 'status': 'PENDING', 'seed': 1234}

Three fields are stored and only one of them is set by SimulationJob. super().__init__(name) ran the parent's constructor, which set name and status; the child then added seed.

2. Defining __init__ Replaces the Parent's

The key fact: a child's __init__ overrides the parent's. It does not extend it automatically.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    def __init__(self, name):
        print("  Job.__init__ ran")
        self.name = name
        self.status = "PENDING"


class WithSuper(Job):
    def __init__(self, name, seed):
        print("  WithSuper.__init__ ran")
        super().__init__(name)
        self.seed = seed


class WithoutSuper(Job):
    def __init__(self, name, seed):
        print("  WithoutSuper.__init__ ran")
        self.seed = seed


print("with super():")
a = WithSuper("smoke", 1234)
print("  stored:", a.__dict__)

print("without super():")
b = WithoutSuper("smoke", 1234)
print("  stored:", b.__dict__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
with super():
  WithSuper.__init__ ran
  Job.__init__ ran
  stored: {'name': 'smoke', 'status': 'PENDING', 'seed': 1234}
without super():
  WithoutSuper.__init__ ran
  stored: {'seed': 1234}

Job.__init__ never ran for b. The object exists, it is a Job by isinstance, and it has no name and no status.

3. The Half-Built Object

That omission produces no error at construction, which is what makes it dangerous:

Azvya Education Pvt. Ltd.VLSI Mentor
halfbuilt.py (BROKEN ON PURPOSE)
class Job:
    def __init__(self, name):
        self.name = name
        self.status = "PENDING"

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


class SimulationJob(Job):
    def __init__(self, name, seed):
        self.seed = seed            # forgot super().__init__(name)


s = SimulationJob("smoke", 1234)
print("constructed fine:", s.seed)
print("is a Job        :", isinstance(s, Job))
print("storage         :", s.__dict__)
print(s.describe())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
constructed fine: 1234
is a Job        : True
storage         : {'seed': 1234}
Traceback (most recent call last):
  File "halfbuilt.py", line 19, in <module>
    print(s.describe())
          ~~~~~~~~~~^^
  File "halfbuilt.py", line 7, in describe
    return f"job {self.name} ({self.status})"
                  ^^^^^^^^^
AttributeError: 'SimulationJob' object has no attribute 'name'

Three lines of successful output and then a failure inside an inherited method the child never wrote. The traceback points at Job.describe, which is correct and unhelpful: the bug is the missing line in the child's constructor.

This is Chapter 16.3 Section 7's silent failure repeating one level up — and Chapter 14.2's symptom-versus-cause problem, with the cause now in a different class.

A child __init__ that does not call super().__init__() is asserting that the parent's setup is unnecessary. Make sure that is what you mean.

4. Why super() and Not the Parent's Name

Both of these work:

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


class WithSuper(Job):
    def __init__(self, name, seed):
        super().__init__(name)
        self.seed = seed


class WithExplicitName(Job):
    def __init__(self, name, seed):
        Job.__init__(self, name)       # names the parent; also works
        self.seed = seed


a = WithSuper("smoke", 1234)
b = WithExplicitName("cdc", 5678)

print("super()  :", a.__dict__)
print("explicit :", b.__dict__)
print("identical shape:", sorted(a.__dict__) == sorted(b.__dict__))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
super()  : {'name': 'smoke', 'status': 'PENDING', 'seed': 1234}
explicit : {'name': 'cdc', 'status': 'PENDING', 'seed': 5678}
identical shape: True

The explicit form is not wrong; it is brittle. Two reasons:

It names the parent twice. The class header already says (Job). Writing Job.__init__ again means a change to the hierarchy must be made in two places, and missing one produces a class that calls a grandparent it no longer descends from.

It passes self manually. Job.__init__(self, name) is the long form from Chapter 16.1 Section 5, and forgetting the self is a TypeError you have to diagnose.

super() asks the MRO (Chapter 16.9 Section 4) for "the next class after this one", which is the right answer even when the hierarchy is rearranged — and is the only correct choice when a class has more than one parent, a case this curriculum does not otherwise cover.

5. Add or Replace: Both Are Legitimate

super() is not mandatory. The question is whether the parent's work still applies:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    def __init__(self, name):
        self.name = name
        self.status = "PENDING"
        self.attempts = 0


class RetryingJob(Job):
    """ADDS: the parent's setup still applies, plus a retry budget."""

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


class CachedResult(Job):
    """REPLACES: a cached result was never pending and has no attempts."""

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


a = RetryingJob("smoke", 3)
b = CachedResult("cdc", "PASS")

print("adds    :", a.__dict__)
print("replaces:", b.__dict__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
adds    : {'name': 'smoke', 'status': 'PENDING', 'attempts': 0, 'max_retries': 3}
replaces: {'name': 'cdc', 'status': 'PASS'}

RetryingJob is the normal case: the parent's three fields plus one more.

CachedResult deliberately skips super(), because a cached result genuinely has no attempts and was never PENDING. That is a defensible design — and it is also a warning sign, because a class that discards most of its parent's setup usually should not be a subclass at all (Chapter 16.9 Section 7).

6. super() Works in Any Method

Not just __init__:

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 VerboseJob(Job):
    def describe(self):
        return "VERBOSE: " + super().describe()


class SilentJob(Job):
    def describe(self):
        return "-"


for obj in (Job("alu"), VerboseJob("smoke"), SilentJob("cdc")):
    print(f"{type(obj).__name__:<11} {obj.describe()}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Job         job alu
VerboseJob  VERBOSE: job smoke
SilentJob   -

VerboseJob wraps the parent's answer; SilentJob discards it. Same choice as Section 5, applied to an ordinary method — and that is Chapter 16.11's subject in full.

7. Call super() First

Ordering matters when the child's setup depends on the parent's:

Azvya Education Pvt. Ltd.VLSI Mentor
ordering.py (BROKEN ON PURPOSE)
class Job:
    def __init__(self, name):
        self.name = name


class Labelled(Job):
    def __init__(self, name, suffix):
        self.label = f"{self.name}-{suffix}"     # uses self.name too early
        super().__init__(name)


j = Labelled("smoke", "nightly")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Traceback (most recent call last):
  File "ordering.py", line 12, in <module>
    j = Labelled("smoke", "nightly")
  File "ordering.py", line 8, in __init__
    self.label = f"{self.name}-{suffix}"     # uses self.name too early
                    ^^^^^^^^^
AttributeError: 'Labelled' object has no attribute 'name'

self.name did not exist yet, because the line that sets it had not run. Note the message names 'Labelled' — the object's actual class, as Chapter 16.1 Section 6 described, even though the attribute it is missing would have been set by the parent. The error tells you what the object is, not whose job it was to finish building it.

Put super().__init__(...) first, before any line that reads an inherited attribute:

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


class Labelled(Job):
    def __init__(self, name, suffix):
        super().__init__(name)
        self.label = f"{self.name}-{suffix}"


j = Labelled("smoke", "nightly")
print(j.label)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke-nightly

8. The Child Depends on the Parent's Internals

super() couples the two classes, and the coupling runs deeper than the constructor call. A child that uses a field the parent created is depending on the parent continuing to create it:

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


class Tagged(Job):
    def __init__(self, name, tag):
        super().__init__(name)
        self.tags.append(tag)          # relies on the parent making self.tags


t = Tagged("smoke", "nightly")
print("works:", t.name, t.tags)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
works: smoke ['nightly']

Now the parent is edited — by someone else, in another file, for a good reason:

Azvya Education Pvt. Ltd.VLSI Mentor
fragile.py (BROKEN ON PURPOSE)
class Job:
    def __init__(self, name):
        self.name = name
        # self.tags removed -- nothing used it HERE


class Tagged(Job):
    def __init__(self, name, tag):
        super().__init__(name)
        self.tags.append(tag)


Tagged("smoke", "nightly")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Traceback (most recent call last):
  File "fragile.py", line 13, in <module>
    Tagged("smoke", "nightly")
    ~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "fragile.py", line 10, in __init__
    self.tags.append(tag)
    ^^^^^^^^^
AttributeError: 'Tagged' object has no attribute 'tags'

The parent's author did nothing wrong by their own lights — they removed a field nothing in Job used. The breakage is in a different class, in a different file, and the traceback points at the child.

This is the fragile base class problem, and it is the real cost of inheritance: a child's correctness depends on parent internals that the parent never promised to keep. Two mitigations:

  • Have the child create what the child needs. self.tags = [tag] in Tagged.__init__ depends on nothing.
  • Treat anything a subclass uses as part of the parent's public surface (Chapter 16.7) — and therefore as something you may not quietly remove.

The second is why a widely-subclassed base class is harder to change than an ordinary one, and a reason Chapter 16.13 prefers composition where either would work.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class A:
    def __init__(self):
        self.trail = ["A"]


class B(A):
    def __init__(self):
        super().__init__()
        self.trail.append("B")


class C(B):
    def __init__(self):
        super().__init__()
        self.trail.append("C")


class D(B):
    def __init__(self):
        self.trail = ["D only"]


print("C:", C().trail)
print("D:", D().trail)
print("B:", B().trail)
print("mro:", [k.__name__ for k in C.__mro__])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
C: ['A', 'B', 'C']
D: ['D only']
B: ['A', 'B']
mro: ['C', 'B', 'A', 'object']

C's trail is built from the bottom up. C.__init__ called B's, which called A's, which created the list — then each appended on the way back out. The order of the list is the reverse of the call order, which is worth pausing on.

D skipped the chain entirely and built its own list. No error, and D has no "A" in it despite being a B and an A by isinstance.

The MRO is the path super() walks, and each class appears exactly once.

10. Exercises

Exercise 1 — add a field. Write a child with its own __init__ that calls super() and adds one attribute. Print __dict__.

Exercise 2 — omit super. Remove the super() call and print __dict__ again. Then call an inherited method and record the error.

Exercise 3 — the trace. Put a print in both constructors and confirm the order they run in.

Exercise 4 — explicit parent. Write the same child using Parent.__init__(self, ...). Make it fail by omitting self, and record the error.

Exercise 5 — add or replace. Write one child that extends the parent's setup and one that deliberately replaces it. Justify the second in a sentence.

Exercise 6 — super in a method. Write a child whose describe wraps the parent's answer rather than replacing it.

Exercise 7 — ordering. Reproduce Section 7's failure, then fix it. Explain why the message named the parent class.

Exercise 8 — three levels. Build an A/B/C chain where each __init__ appends to a shared list. Predict the order before running.

11. Summary

  • A child's __init__ overrides the parent's; it does not extend it automatically.
  • super().__init__(...) runs the parent's constructor, after which the child adds its own fields.
  • Omitting it builds a half-constructed object with no error — the failure arrives later as an AttributeError, often inside an inherited method.
  • super() does not name the parent, so the class survives a change to the hierarchy and works with multiple parents.
  • Parent.__init__(self, ...) also works but is brittle and makes you pass self by hand.
  • Modern super() takes no arguments; the two-argument form is Python 2.
  • Replacing the parent's setup is legitimate when its fields genuinely do not apply — but it is a sign the class may not belong in the hierarchy.
  • super() works in any method, not only __init__.
  • Call super() first, before reading any inherited attribute.
  • super() walks the MRO, so a chain of constructors runs parent-first and returns child-last.
  • A child that uses a field the parent created depends on parent internals — the fragile base class problem, where a reasonable edit upstream breaks a class in another file.

The next chapter is about replacing a parent's method outright, and keeping the signature compatible when you do.

Where this fits

Part of the Python curriculum.