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))Inheritance and shared interfaces
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__withsuper().__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.
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.
- Inherit.
LineModelhas nomseof its own, so the search goes on to its parent and findsModel.mse. - Override.
msecallsself.predict, andselfis the line model, so the search starts at the instance again and findsLineModel.predictbefore it reachesModel.predict. Onemseserves every child, each with its ownpredict.
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.
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.interceptsuper().__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.
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.valueStep 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
The lookup rule. For obj.name, Python searches
- the instance’s own table;
- its class;
- 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.
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} \]
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.
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
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.valueIt 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.5For 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.
Write mse once; override predict.
- Subclass:
class LineModel(Model):inherits every method it does not define. - Parent’s
__init__: callsuper().__init__(...)first. - Lookup: instance, class, parent, …
object; the first match wins. - Interface: code that only calls
fitandpredictworks 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”;
isinstanceandissubclass. - Python Software Foundation, Built-in Functions:
superreturns “a proxy object that delegates method calls to a parent or sibling class”, andisinstanceis 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 fittedcoef_andintercept_; and Developing scikit-learn estimators: every estimator implementsfit, which returnsself, and other methods “are implemented inBaseEstimator, 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