Inheritance and construction: build one object in order

Follow one Truck object as Vehicle's setup runs first and Truck's second, then see what breaks when a constructor skips the parent. Inheritance and polymorphism · Object-oriented programming.

A fleet app tracks cars and trucks. Every vehicle needs a plate that is never empty, and trucks also carry a load limit. Copying the plate check into each vehicle type invites mistakes. How can one class pass its setup to another, and what must finish before a truck is ready to use?

Inheritance lets a new class start from an existing one. The new class, the subclass or child, has what the existing superclass or parent offers, and then adds its own state and behaviour. Here Vehicle holds what every vehicle shares, and Truck builds on it.

One class builds on another

Vehicle checks the plate, stores it and can describe itself. Truck extends it with a load limit and a check against it.

class Vehicle {
    private String plate;

    Vehicle(String plate) {
        if (plate.isEmpty()) throw new IllegalArgumentException("plate required");
        this.plate = plate;
    }

    String describe() { return "vehicle " + plate; }
}

class Truck extends Vehicle {
    private int loadLimit;

    Truck(String plate, int loadLimit) {
        super(plate);
        this.loadLimit = loadLimit;
    }

    boolean canCarry(int kg) { return kg <= loadLimit; }
}
class Vehicle:
    def __init__(self, plate):
        if not plate:
            raise ValueError("plate required")
        self.plate = plate

    def describe(self):
        return "vehicle " + self.plate


class Truck(Vehicle):
    def __init__(self, plate, load_limit):
        super().__init__(plate)
        self.load_limit = load_limit

    def can_carry(self, kg):
        return kg <= self.load_limit

extends Vehicle in Java and Truck(Vehicle) in Python both say a truck is a vehicle with more. The Truck constructor takes both values, hands the plate to Vehicle's setup with super(plate) or super().__init__(plate), and then stores its own limit. The word super means "my parent class".

Java: Vehicle first, then Truck

Read the steps as text

  1. Vehicle declares plate and describe. Truck extends Vehicle and adds loadLimit and canCarry. No object exists yet.
  2. Running t = new Truck("KA-01", 900) creates one Truck object and starts Truck's constructor. Its rows already exist with default values, null or 0.
  3. Truck's first action, super(plate), starts Vehicle's constructor on this same object. Truck's own lines wait.
  4. Vehicle's check passes. The line this.plate = plate fills the plate row, which Vehicle declares: null becomes "KA-01".
  5. Vehicle's constructor finishes and hands control back to Truck's constructor. The line this.loadLimit = loadLimit fills the loadLimit row, which Truck declares: 0 becomes 900.
  6. Truck's constructor finishes, so the object is complete. Now t refers to the finished object.
  7. The call t.describe() runs describe, an inherited method written in Vehicle. It reads the plate row of this object: "KA-01".
  8. The call t.canCarry(400) runs canCarry, a method Truck adds. It reads the loadLimit row of this object: 900.

Writing new Truck("KA-01", 900) creates one object. Picture its rows labelled by the class that declared them: the plate row comes from Vehicle, the load-limit row from Truck. No second Vehicle object sits inside it; Vehicle's setup runs on this same truck. In the Java version plate is private, so only Vehicle's code can name it, yet every truck still has one.

Truck t = new Truck("KA-01", 900);
t.describe();      // "vehicle KA-01", written in Vehicle
t.canCarry(400);   // true, written in Truck
t = Truck("KA-01", 900)
t.describe()       # "vehicle KA-01", written in Vehicle
t.can_carry(400)   # True, written in Truck

The truck answers both calls: describe is inherited behaviour and canCarry is added behaviour. Nothing was copied into Truck. describe is written inside Vehicle, so it may read the private plate of any Vehicle, including this truck.

Parent first, then child

Construction fills a new object in a fixed order, and the order matters because the child's setup may rely on the parent's rows being ready. For new Truck("KA-01", 900):

  1. One truck object is created: in Java its rows hold default values such as 0 and null; in Python it has no attributes yet.
  2. Truck's constructor starts, and its first action is the call to Vehicle's setup.
  3. Vehicle's setup checks the plate and fills the plate row, then returns.
  4. Back in Truck's constructor, the next line fills the load-limit row.
  5. The finished truck is handed back and t refers to it.

With more levels the same rule repeats: each constructor first reaches its parent's, so the topmost class (in Java, ultimately Object) finishes first and the class you named in new finishes last.

The general rule is parent setup first, child setup second. The languages differ in who arranges step 2. In Java, a constructor reaches a superclass constructor before the child sets any of its own rows, normally by starting with super(...). If you write none, the compiler inserts super() with no arguments. If a Python child writes its own __init__, Python runs only that one and never calls Vehicle.__init__ for you, so you call super().__init__(plate) yourself, normally as the first line, because the child's code may rely on the parent's rows. A child that writes no __init__ inherits Vehicle's.

When the chain breaks

Suppose Truck leaves the parent call out. Java and Python react very differently.

class Truck extends Vehicle {
    private int loadLimit;

    Truck(String plate, int loadLimit) {   // no super(plate) written
        this.loadLimit = loadLimit;
    }
}
class Truck(Vehicle):
    def __init__(self, plate, load_limit):
        self.load_limit = load_limit   # super().__init__(plate) is missing

t = Truck("KA-01", 900)
t.describe()    # AttributeError: no attribute 'plate'

Java adds the implicit super(), but Vehicle has no constructor without arguments, so the class does not compile and nothing can run; the compiler reports that constructor Vehicle in class Vehicle cannot be applied to given types. Python has no such check. Construction finishes happily, t.load_limit works, and vars(t), which lists the attributes the object actually has, shows no plate, because that row was never created. The mistake surfaces only later, when describe looks for plate and Python raises an AttributeError, its error for asking an object for an attribute it does not have.

Python: the forgotten super call

Read the steps as text

  1. Vehicle declares plate and describe. Truck extends Vehicle and adds load_limit and can_carry. No object exists yet.
  2. Running t = Truck("KA-01", 900) creates one Truck object and starts Truck's __init__. It has no attributes yet. The line self.load_limit = load_limit creates the load_limit row, which Truck declares, holding 900.
  3. Truck's __init__ finishes without an error, so construction ends. Now t refers to the object. Vehicle's __init__ never ran, so plate was never created.
  4. The call t.describe() runs describe, written in Vehicle, which reads the plate row. This object has no plate attribute, so Python raises AttributeError.
  5. Truck's __init__ now begins with super().__init__(plate).
  6. Running t = Truck("KA-01", 900) creates one Truck object and starts Truck's __init__. This is a brand-new object, number 2; the earlier one is not changed. It has no attributes yet.
  7. The call super().__init__(plate) starts Vehicle's __init__ on this same object. Truck's own lines wait. Vehicle's check passes. The line self.plate = plate creates the plate row, which Vehicle declares, holding "KA-01".
  8. Vehicle's __init__ finishes and hands control back to Truck's __init__. The line self.load_limit = load_limit creates the load_limit row, which Truck declares, holding 900.
  9. Truck's __init__ finishes, so the object is complete. Now t refers to the finished object.
  10. The call t.describe() runs describe, an inherited method written in Vehicle. It reads the plate row of this object: "KA-01".

Leaving a required value out of the parent call is the same slip in a different place.

Truck(String plate, int loadLimit) {
    super();                       // does not compile: Vehicle needs a plate
    this.loadLimit = loadLimit;
}
def __init__(self, plate, load_limit):
    super().__init__()             # TypeError: plate was not passed on
    self.load_limit = load_limit

Java refuses at compile time. Python raises a TypeError, its error for a call with the wrong arguments, the moment that line runs, before the load-limit row is filled. A parent can also reject a bad value on purpose: new Truck("", 900) in Java, or Truck("", 900) in Python, makes Vehicle's check throw or raise. In each of these runtime failures the creation expression ends with an error and returns nothing, so t is never bound to a half-built truck. The failure leaves no usable object behind.

A call that leaves out plate

Read the steps as text

  1. Vehicle declares plate and describe. Truck extends Vehicle and adds load_limit and can_carry. No object exists yet.
  2. Running t = Truck("KA-01", 900) creates one Truck object and starts Truck's __init__. It has no attributes yet.
  3. The call super().__init__() passes no value for plate, so Python raises TypeError at the call, before Vehicle's __init__ starts.
  4. The error leaves Truck's __init__ too. Nothing is returned, so t is not assigned.

Put shared rules in the parent

The plate check now lives once, in Vehicle. A new Car adds only what is its own.

class Car extends Vehicle {
    private int seats;

    Car(String plate, int seats) {
        super(plate);
        this.seats = seats;
    }
}
class Car(Vehicle):
    def __init__(self, plate, seats):
        super().__init__(plate)
        self.seats = seats

new Car("", 4) is rejected by the same code that protects trucks, and a changed plate rule is edited in one place. A subclass can still add its own checks after the parent call, for example refusing a load limit below zero.

Recap

  • A subclass object is one object: it has the parent's rows and its own, and answers the parent's methods and its own.
  • Parent setup finishes before the child's; Java arranges the parent call, and Python needs super().__init__(...) called, normally first.
  • A Java subclass whose parent needs arguments must pass them with super(...), or it does not compile.
  • A forgotten super().__init__ in Python leaves the parent's attributes missing until something reads them.
  • Put shared checks in the parent constructor so every subclass gets them, and a failed construction hands back no object.
Go deeper

Python creates an object in two steps: __new__ makes the empty instance and __init__ fills it in. Writing only __init__ is the usual case. Java has a detail of its own: right after the parent constructor returns, the subclass's field initializers (such as int trips = 0;) run, and only then the rest of the constructor body.

The Java rules in this lesson are the long-standing ones (Java 17 to 21). Newer Java versions loosen what may appear before super(...); this lesson sticks to the classic order.

One hazard follows from the order. If a parent constructor calls a method that the child redefines, the child's version runs before the child's own setup has happened. Which version is chosen is the subject of the overriding lesson, so treat this as a warning to avoid for now.

class Base {
    Base() { System.out.println(label()); }    // prints null
    String label() { return "base"; }
}
class Child extends Base {
    private String name = "child";
    @Override String label() { return name; }
}
class Base:
    def __init__(self):
        print(self.label())                    # AttributeError
    def label(self):
        return "base"

class Child(Base):
    def __init__(self):
        super().__init__()
        self.name = "child"
    def label(self):
        return self.name

Calling new Child() or Child() reaches the child's label while its name row is still unset. Keep constructors free of calls to methods a subclass may replace.

Next, overloading and overriding asks which version of a method runs when a subclass writes its own, a question this lesson deliberately left aside. A class hierarchy is a tree, so Tree Fundamentals practises reading parent and child links.