Skip to main content

Inheritance and shared interfaces

A subclass gets its parent’s methods and may replace them. Build a parent Model whose mse calls self.predict, two children for the study table, MeanModel (mse 170.0) and LineModel (mse 8.0), and see that scikit-learn’s models share the same fit and predict.
P01.9.4L0 Foundation25 minIn review

Before this lesson

P01.9.3 Special methods: objects that act like built-ins

After this lesson you can

  • Write a subclass that names its parent in the class header, inherits the parent’s methods and overrides some of them.
  • Call the parent’s __init__ with super().__init__ and explain what goes wrong without it.
  • Trace where Python finds a method: in the instance, its class, then each parent in turn.
  • Use objects of different classes through one shared interface, including scikit-learn’s DummyRegressor and LinearRegression.

In review. The maths and code on this page are checked by automated tests on every change, but an independent reader has not signed it off yet. Spot a mistake or a confusing step? Tell us.

The question

Here are two models of the study table. One ignores the hours and always predicts the mean score, 60. The other is the course’s line, \({9 \times \text{hours} + 33}\). Both need the same yardstick, the mean squared error, and the code for it does not depend on how a model predicts. Must you write mse twice, and fix every bug twice? And why can scikit-learn’s many models all be used with the same two calls, fit and predict?

From the last lesson

1. Which method does v + w call, and on which object?

Show answer

__add__, on v, the value on the left, with w as the argument: v.__add__(w).

2. Without __eq__, when is v == w True?

Show answer

Only when v and w are the same object, that is when v is w.

Intuition

A class can name another class as its parent. The new class, a subclass or child, gets every method of the parent without writing it again, and may replace any of them with its own. So mse is written once, in a parent Model, and each child supplies only what differs: how it fits and how it predicts.

A class tree. At the top the class Model holds __init__, fit, predict, mse and report. Below it, two child classes joined to it by arrows labelled parent: MeanModel, holding __init__, fit and predict, and LineModel, holding __init__ and predict. On the right the instance line, with slope 9 and intercept 33, has a dashed arrow to LineModel. A green path shows the lookup of line.mse: not in line, not in LineModel, found in Model. A second green arrow shows that inside mse, self.predict is found in LineModel, the first place the search looks.
P01.9.4-F1

The lookup of line.mse(...): not in the instance, not in LineModel, found in Model. Inside mse, self.predict starts the search again at the instance, so it finds LineModel’s own predict.

The picture shows the two halves of the idea.

  1. Inherit. LineModel has no mse of its own, so the search goes on to its parent and finds Model.mse.
  2. Override. mse calls self.predict, and self is the line model, so the search starts at the instance again and finds LineModel.predict before it reaches Model.predict. One mse serves every child, each with its own predict.

Build it up

Step 1. The parent. Model holds what all models share: a name, a fit that learns nothing, mse, and report. Its predict raises an error, because only a particular kind of model knows how to predict.

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

    def fit(self, hours, scores):
        return self

    def predict(self, hours):
        raise NotImplementedError('each kind of model defines predict')

    def mse(self, hours, scores):
        total = 0
        for h, s in zip(hours, scores):
            total += (s - self.predict(h)) ** 2
        return total / len(scores)

    def report(self, hours, scores):
        print(self.name, 'model: mse', self.mse(hours, scores))

fit returns self, the model itself, so that a call can be followed by another on the same line, as in MeanModel().fit(hours, scores) below. scikit-learn’s fit does the same.

Step 2. A child. The parent’s name goes in brackets in the class header. LineModel writes only __init__ and predict; it gets fit, mse and report from Model.

class LineModel(Model):
    def __init__(self, slope, intercept):
        super().__init__('line')
        self.slope = slope
        self.intercept = intercept

    def predict(self, hours):
        return self.slope * hours + self.intercept

super().__init__('line') runs the parent’s __init__ on this same object, so that it gets the attribute name that the parent’s methods expect. Then the child adds its own attributes.

Two columns, each a search down four places: the instance line, the class LineModel, the class Model and the class object. Left, for line.predict: the instance is crossed, LineModel has a tick, and the search stops there, so Model and object are grey. Right, for line.mse: the instance and LineModel are crossed, Model has a tick, and object is grey. A green arrow runs down each column to the place where the name is found.
P01.9.4-F2

Where Python finds a method: the instance, then its class, then each parent in turn, and the first match wins. Every class ends in object, the parent of all classes.

Step 3. Override. MeanModel replaces the parent’s fit with one that learns something: the mean of the scores, stored in an attribute, as a fitted model does (P01.9.2).

class MeanModel(Model):
    def __init__(self):
        super().__init__('mean')

    def fit(self, hours, scores):
        self.value = sum(scores) / len(scores)
        return self

    def predict(self, hours):
        return self.value

Step 4. Use them. Both children answer the same calls.

hours = [2, 4, 5, 1, 3]
scores = [55, 65, 80, 40, 60]

mean_model = MeanModel().fit(hours, scores)
line = LineModel(9, 33).fit(hours, scores)
mean_model.report(hours, scores)
line.report(hours, scores)
print(vars(mean_model), vars(line))
print(isinstance(line, Model), isinstance(line, MeanModel), issubclass(LineModel, Model))
mean model: mse 170.0
line model: mse 8.0
{'name': 'mean', 'value': 60.0} {'name': 'line', 'slope': 9, 'intercept': 33}
True False True

The formal statement

NoteDefinitions: subclass, inheritance, overriding

class Child(Parent): makes Child a subclass of Parent, its parent or base class. The subclass inherits every attribute of the parent that it does not define itself. Defining a method of the same name in the subclass overrides the parent’s: for the subclass’s instances, the subclass’s version is found first.

The lookup rule. For obj.name, Python searches

  1. the instance’s own table;
  2. its class;
  3. that class’s parent, then the parent’s parent, and so on, up to object.

The first match wins. The Tutorial (9.5): “if a requested attribute is not found in the class, the search proceeds to look in the base class. This rule is applied recursively”. Every class has object at the end of its chain, which is where the defaults of P01.9.3 come from: object’s __repr__ writes <... object at 0x...>, and its __eq__ compares identity. The order is kept in the class’s __mro__, its “method resolution order”:

for cls in LineModel.__mro__:
    print(cls.__name__)
LineModel
Model
object

super(). Inside a method, super() gives an object that looks the name up starting after the current class. super().__init__('line') therefore runs Model.__init__ on self. An overriding method can use it to extend the parent’s version instead of replacing it.

isinstance and issubclass. isinstance(obj, C) is True when the class of obj is C or a subclass of C; type(obj) is C asks for C alone. issubclass(Child, Parent) asks about two classes.

A shared interface. mse never asks which class self belongs to; it only calls self.predict. Any object that has the methods a piece of code calls can be used there. The set of methods that code relies on, here fit and predict, is an interface.

Worked example

Work out both mean squared errors by hand. Each row is one student: the error is \({\text{score} - \text{prediction}}\), then it is squared.

A table with one row per student, hours 2, 4, 5, 1 and 3 and scores 55, 65, 80, 40 and 60. For MeanModel the prediction is always 60, the errors are -5, 5, 20, -20 and 0, and the squares 25, 25, 400, 400 and 0, with total 850 and mse 850 / 5 = 170.0. For LineModel the predictions are 51, 69, 78, 42 and 60, the errors 4, -4, 2, -2 and 0, and the squares 16, 16, 4, 4 and 0, with total 40 and mse 40 / 5 = 8.0.
P01.9.4-F3

The worked example as a trace: one row per student, one pair of columns per model. The same mse code produces both totals; only predict differs.

For MeanModel, fit stores \({300 / 5} = 60.0\), and every prediction is 60. The squared errors add up to \({25 + 25 + 400 + 400 + 0} = 850\), so

\[ \begin{aligned} \text{mse} &= 850 / 5\\ &= 170.0. \end{aligned} \]

For LineModel, fit is the parent’s and learns nothing; the line was given. The predictions are 51, 69, 78, 42 and 60, the squared errors add up to \({16 + 16 + 4 + 4 + 0} = 40\), and

\[ \begin{aligned} \text{mse} &= 40 / 5\\ &= 8.0. \end{aligned} \]

Two scatter plots of the study table, score against hours, five blue points. Left, MeanModel: a flat vermillion line at 60 and reddish purple segments from each point to the line, long for hours 5 and 1; mse 170.0. Right, LineModel: the vermillion line 9 times hours plus 33 passes close to every point, with short reddish purple segments; mse 8.0.
P01.9.4-F5

The two fitted models on the study table. Each purple segment is one error; the mse is the mean of their squares.

The line’s errors are far smaller, which is what the two numbers say: 8.0 against 170.0. In ML01.4 you will see why \({9 \times \text{hours} + 33}\) is the line with the smallest mse of all.

Code

scikit-learn, installed with pip (P01.5.3), holds about a hundred models that predict, all with the same interface: fit(X, y) learns from the table X and the targets y, and predict(X) answers for new rows. X has one row per student and one column per input, so it is a list of lists, here with one column, the hours.

from sklearn.base import BaseEstimator
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import LinearRegression

X = [[2], [4], [5], [1], [3]]
y = [55, 65, 80, 40, 60]

for model in [DummyRegressor(), LinearRegression()]:
    model.fit(X, y)
    guess = model.predict([[2]])
    total = 0
    for target, prediction in zip(y, model.predict(X)):
        total += (target - prediction) ** 2
    print(type(model).__name__, 'predicts', round(float(guess[0]), 6), 'for 2 hours; mse',
          round(float(total / len(y)), 6))

dummy = DummyRegressor().fit(X, y)
linear = LinearRegression().fit(X, y)
print(round(float(dummy.constant_[0][0]), 6), round(float(linear.coef_[0]), 6),
      round(float(linear.intercept_), 6))
print(isinstance(linear, BaseEstimator), isinstance(dummy, BaseEstimator))
DummyRegressor predicts 60.0 for 2 hours; mse 170.0
LinearRegression predicts 51.0 for 2 hours; mse 8.0
60.0 9.0 33.0
True True

The loop never asks which model it holds; it only calls fit and predict, as mse only calls self.predict. DummyRegressor, whose default rule “always predicts the mean of the training set”, learns 60, the job of MeanModel. LinearRegression learns a slope of 9 and an intercept of 33: the course’s line, found from the data. Both are subclasses of scikit-learn’s BaseEstimator, as our two are of Model. The learned numbers sit in attributes that end in an underscore, constant_, coef_ and intercept_ (P01.9.2). They are floats worked out by NumPy, so the page rounds them to six places; the last digits could differ from one machine to another.

Three model objects side by side, each a box listing fit and predict: MeanModel, a child of our Model, which learns value 60.0; DummyRegressor, which learns constant_ 60; and LinearRegression, which learns coef_ 9 and intercept_ 33. The last two are children of scikit-learn's BaseEstimator. Above them, the loop for model in models: model.fit(...), model.predict(...), with green arrows to all three: the same two calls work on each.
P01.9.4-F6

One interface, many classes. Code that calls only fit and predict works with any of them.

Explore

This module’s interactive, the instance workbench, lives in P01.9.2. Its class RunningMean would make a fine parent: which of its methods would a child RunningMeanWithMax, that also tracks the largest score, inherit unchanged, and which would it override?

Pitfalls

Two rows. Wrong, with a cross: a LineModel whose __init__ sets slope and intercept but does not call super().__init__. Its instance holds only slope 9 and intercept 33, and line.report(...) stops with AttributeError, because report reads self.name. Right, with a tick: the same __init__ starting with super().__init__('line'). Its instance holds name 'line', slope 9 and intercept 33, and report prints line model: mse 8.0.
P01.9.4-F4

Wrong beside right. A child’s __init__ replaces the parent’s; without super().__init__, the parent’s part of the object is never filled in.

“A child’s __init__ adds to the parent’s.”

It replaces it, like any override. If LineModel.__init__ leaves out super().__init__('line'), the object never gets name, and the inherited report stops with AttributeError. Call super().__init__(...) first, then add the child’s own attributes.

“I overrode predict.”

Only if the spelling matches exactly. A child that defines predicts or Predict adds a new method and leaves the parent’s predict in force, which here raises NotImplementedError. The error at least says so; a parent method that quietly returned something would not.

“isinstance(line, Model) and type(line) is Model mean the same.”

isinstance accepts subclasses, so it is True for a LineModel; type(line) is Model is False. Ask with isinstance when any kind of model will do.

“To share code, every class should inherit from another.”

Inherit when the child really is a kind of the parent: a line model is a model. To use another object’s abilities, keep it in an attribute instead. And code that only calls fit and predict needs no common parent at all, as the loop over scikit-learn’s models shows.

Check yourself

Recall

1. Where does Python look for line.mse, in order?

Show answer

In the instance line, then in its class LineModel, then in the parent Model, where it is found. (Had it not been there, object would come next.)

2. What does super().__init__('line') do?

Show answer

It runs the parent’s __init__ on the same object, which sets self.name to 'line'.

3. Inside Model.mse, why does self.predict call LineModel.predict for a line model?

Show answer

The search for predict starts at the instance, whose class is LineModel, and LineModel defines predict, so it is found before Model.predict.

4. What are the mse of MeanModel and of LineModel on the study table?

Show answer

\({850 / 5} = 170.0\) and \({40 / 5} = 8.0\).

5. What do DummyRegressor and LinearRegression learn from the study table?

Show answer

The mean, 60, and the line with slope 9 and intercept 33, stored in constant_, coef_ and intercept_.

Exercises

1. ★ Write a child ConstantModel(Model) whose __init__ takes a value and whose predict always returns it. Which methods does it inherit?

Show solution
class ConstantModel(Model):
    def __init__(self, value):
        super().__init__('constant')
        self.value = value

    def predict(self, hours):
        return self.value

It inherits fit (which learns nothing), mse and report from Model.

2. ★★ Work out by hand the mse of ConstantModel(50) on the study table, then check it with mse.

Show solution

The errors \({\text{score} - 50}\) are 5, 15, 30, -10 and 10; their squares 25, 225, 900, 100 and 100 add up to 1,350, so the mse is \({1350 / 5} = 270.0\). ConstantModel(50).mse(hours, scores) gives 270.0. Predicting the mean, 60, gives 170.0: of all constant predictions, the mean has the smallest mse.

3. ★★★ The root mean squared error is \({\sqrt{\text{mse}}}\), in the same units as the scores. Give Model a method rmse that uses mse, without copying its loop, and find the line’s rmse. Which classes must change?

Show solution

Only Model; both children inherit the new method.

    def rmse(self, hours, scores):
        return self.mse(hours, scores) ** 0.5

For the line, \({\sqrt{8.0}}\) is about 2.83 (2.8284271247461903): a typical prediction is about 3 marks off. For the mean model, \({\sqrt{170}}\) is about 13.04.

Summary card

A subclass inherits its parent’s methods and overrides what differs.

The anchor figure again: Model with fit, predict, mse and report; its children MeanModel and LineModel; the lookup of line.mse ending in Model, and of self.predict ending in LineModel.
P01.9.4-F1

Write mse once; override predict.

  • Subclass: class LineModel(Model): inherits every method it does not define.
  • Parent’s __init__: call super().__init__(...) first.
  • Lookup: instance, class, parent, … object; the first match wins.
  • Interface: code that only calls fit and predict works with any model, including scikit-learn’s.

Sources and next steps

Sources. Each was opened and checked against this lesson on 12 November 2026.

  • Python Software Foundation, The Python Tutorial, chapter 9, 9.5 Inheritance: the search for an attribute “proceeds to look in the base class”, applied recursively; derived classes “may override methods of their base classes”; isinstance and issubclass.
  • Python Software Foundation, Built-in Functions: super returns “a proxy object that delegates method calls to a parent or sibling class”, and isinstance is True for an instance of the class “or of a (direct, indirect, or virtual) subclass thereof”.
  • scikit-learn developers, version 1.9.1: DummyRegressor, whose default strategy “mean” “always predicts the mean of the training set” and stores it in constant_; LinearRegression, ordinary least squares, with the fitted coef_ and intercept_; and Developing scikit-learn estimators: every estimator implements fit, which returns self, and other methods “are implemented in BaseEstimator, which you should inherit from”.
  • Ana Bell, MIT OpenCourseWare 6.100L, Fall 2022, lecture notes 19: a child class “inherits all data and behaviors of parent class” and may override them; to find a method, “look for method name up the hierarchy (in parent, then grandparent, and so on)”.

Next. Module P01.10 uses special methods again: a class with __iter__ and __next__ can be looped over. The models of this lesson return in ML01, where they are trained and compared properly.

This lesson unlocks

P01.10 Iterators, generators and comprehensions ML01.1 What learning from data means

Your progress is saved in this browser only. No account, no tracking.
Back to top