Discord

2Foundational Skills 2.1Python Basics

2.1.2 Object-Oriented Programming, Classes, and Inheritance

Learn object-oriented Python with one main course, then take a short tour of the classes, methods and inheritance you will meet in scikit-learn and PyTorch.

Why learn object-oriented programming?

In Python Fundamentals, we worked with lists, strings and library functions. That is enough to write plenty of useful Python. But the machine-learning libraries we will use next are built around objects and classes.

You will not write large programs full of classes at IOAI. You will, however, read and complete small classes all the time:

  • In scikit-learn, every model is an object. KNeighborsClassifier(n_neighbors=3) creates one, model.fit(X, y) trains it, and model.predict(X) uses it.
  • In PyTorch, we describe a neural network with a class that inherits from nn.Module. We often describe a dataset with a class containing __len__ and __getitem__.

So our goal is modest: you should be able to read those patterns, change them and write a small class of your own. The rest of object-oriented programming can wait.

What you need to know

By the end of this module, you should be comfortable with these ideas:

Idea In one line
Objects and classes A class describes a kind of value; an object is one value made from it.
Attributes Store data inside an object, such as a student's name or a model's learned coefficients.
Methods Put functions inside a class and call them with a dot.
self and __init__ Set up each object and work with its own data.
Special methods Let Python operations such as len(obj) and obj[i] work with your class.
Inheritance Build a child class from a parent class and reuse its behaviour.
super() Run the parent class's setup from a child class.

That list is enough for the scikit-learn and PyTorch code in this guide.

Learn it properly

This page gives you a guided tour, but a course gives you the practice that makes the ideas stick. The tour will help you recognise a class; the course is where you learn to write one without being led line by line.

Pick one main course

As with Python Fundamentals, do not collect five resources and half-finish all of them. Pick one course, type the examples yourself and complete its exercises.

Our pick: CS50P week 8

Start with Harvard's CS50P week 8: Object-Oriented Programming. It gives you a lecture, written notes and a problem set in one place, and it continues naturally from the CS50P material recommended in Python Fundamentals.

Focus on classes, inheritance and operator overloading. Operator overloading is where you meet special methods such as __add__. Decorators, class methods and static methods are useful, but you do not need to master them yet.

After the lesson, try Problem Set 8. Watching the lecture may make classes look obvious; writing one from a blank file tells you whether self, attributes and methods have actually clicked.

If you prefer something else

  • Short videos: watch Corey Schafer's Classes and Instances, then Inheritance. The same example continues across both videos.
  • A written tutorial: use Real Python's Object-Oriented Programming in Python. It is longer than you need, so concentrate on defining classes, instance attributes, methods and inheritance.
  • Official reference: read sections 9.3 and 9.5 of the Python tutorial. The official tutorial is especially useful when you already know the idea and want to check the exact syntax.

A quick tour

The rest of this page shows what the important ideas look like in code. You do not need to learn OOP from these snippets alone. Go through your chosen course first, then return here and check that you can explain each example and change it yourself.

Objects you already use

You have already written code like this:

INPUTPython
scores = [72, 85, 90]
scores.append(64)
print(type(scores))
# <class 'list'>
print("ioai".upper())
# IOAI

scores is an object: a value that holds some data (the numbers) and comes with things it can do (append, sort, and so on). The things it can do are called methods, and you call them with a dot. The type of the object, list, is its class.

Object-oriented programming (OOP) means writing our own classes. A class describes a kind of object, and every object we create from it is an instance of that class.

Your first class

INPUTPython
class Student:
    def __init__(self, name, scores):
        self.name = name
        self.scores = scores

    def average(self):
        return round(sum(self.scores) / len(self.scores), 1)

asha = Student("Asha", [72, 85, 90])
ravi = Student("Ravi", [60, 75])

print(asha.name, asha.average())
# Asha 82.3
print(ravi.name, ravi.average())
# Ravi 67.5

Let us read it line by line:

  • class Student: starts the blueprint. Class names are written in CapitalisedWords by convention.
  • __init__ is a special method that runs automatically when we make a new object, as in Student("Asha", [72, 85, 90]). Its job is to set the object up.
  • self is the object itself. self.name = name stores the name inside this particular student. Values stored like this are called attributes.
  • average is an ordinary method. It also takes self, so it can read that student's own scores.

Each object keeps its own data. If we change Ravi's scores, Asha's scores stay untouched:

INPUTPython
ravi.scores.append(95)
print(ravi.average(), asha.average())
# 76.7 82.3

fit and predict

Now we can make a tiny "model" with the same shape as a scikit-learn model. It learns the mean of the training values and predicts that number every time:

INPUTPython
class MeanPredictor:
    def fit(self, values):
        self.value_ = sum(values) / len(values)
        return self

    def predict(self, how_many):
        return [self.value_] * how_many

model = MeanPredictor()
model.fit([3, 5, 10])
print(model.value_)
# 6.0
print(model.predict(2))
# [6.0, 6.0]

fit creates the attribute value_ from the data, and predict uses it later. This is exactly how scikit-learn works: whatever a model learns during fit is stored as an attribute whose name ends in an underscore, like coef_ in linear regression (see Fitted attributes). scikit-learn even has this exact model:

INPUTPython
from sklearn.dummy import DummyRegressor

reg = DummyRegressor(strategy="mean")
reg.fit([[0], [1], [2]], [3, 5, 10])
print(reg.predict([[7], [8]]))
# [6. 6.]

It prints [6. 6.] because scikit-learn returns a NumPy array rather than a list. A model this simple is called a baseline, and you will use one in every task to check that a real model is actually better (Terminology explains why).

Special methods

Methods whose names start and end with two underscores, like __init__, are special methods (people also call them "dunder" methods, for "double underscore"). Python calls them for you in certain situations. Two of them matter a lot:

  • __len__ runs when you call len(obj).
  • __getitem__ runs when you write obj[i].
INPUTPython
class ScoreData:
    def __init__(self, names, scores):
        self.names = names
        self.scores = scores

    def __len__(self):
        return len(self.names)

    def __getitem__(self, i):
        return self.names[i], self.scores[i]

data = ScoreData(["Asha", "Ravi", "Mei"], [90, 75, 82])
print(len(data))
# 3
print(data[0])
# ('Asha', 90)

Keep this example in mind. Later, we will see that a PyTorch Dataset follows exactly this pattern: it knows how many samples it has and how to return sample number i.

Inheritance

Suppose we also want a model that predicts the median. Its predict method would be identical; only fit changes. Instead of copying the shared code, we can put it in a parent class and let two child classes inherit it:

INPUTPython
class ConstantPredictor:
    def predict(self, how_many):
        return [self.value_] * how_many

class MeanPredictor(ConstantPredictor):
    def fit(self, values):
        self.value_ = sum(values) / len(values)
        return self

class MedianPredictor(ConstantPredictor):
    def fit(self, values):
        ordered = sorted(values)
        self.value_ = ordered[len(ordered) // 2]
        return self

mean_model = MeanPredictor().fit([3, 5, 10])
median_model = MedianPredictor().fit([3, 5, 10])
print(mean_model.predict(2), median_model.predict(2))
# [6.0, 6.0] [5, 5]
print(isinstance(median_model, ConstantPredictor))
# True

class MeanPredictor(ConstantPredictor): means "a MeanPredictor is a kind of ConstantPredictor". It gets predict for free and adds its own fit. (This median is only right for an odd number of values, which is fine for the example.)

super()

When the parent class has an __init__ of its own, the child usually needs to run it too. super() lets us reach the parent, so super().__init__(...) runs the parent's setup before the child adds anything:

INPUTPython
class Table:
    def __init__(self, rows):
        self.rows = rows

    def __len__(self):
        return len(self.rows)

class LabelledTable(Table):
    def __init__(self, rows, labels):
        super().__init__(rows)
        self.labels = labels

    def __getitem__(self, i):
        return self.rows[i], self.labels[i]

t = LabelledTable([[5.1, 3.5], [6.2, 2.9]], ["setosa", "versicolor"])
print(len(t), t[1])
# 2 ([6.2, 2.9], 'versicolor')

LabelledTable stores the rows through Table's __init__, inherits __len__, and adds labels and __getitem__.

What this looks like in PyTorch

We now have every Python idea used by the two common PyTorch patterns. You do not need to understand the network itself yet; for now, just recognise the shape:

INPUTPython
import torch
from torch import nn

class TinyNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = nn.Linear(3, 1)

    def forward(self, x):
        return self.layer(x)

net = TinyNet()
x = torch.ones(2, 3)
print(net(x).shape)
# torch.Size([2, 1])
  • TinyNet(nn.Module): your network inherits from PyTorch's nn.Module.
  • super().__init__(): runs nn.Module's own setup first. Forget it and PyTorch stops with an error (see the table below).
  • self.layer = ...: the network's parts are stored as attributes, just like self.name in Student.
  • forward: the method that says what the network does with an input. Calling net(x) runs it.

A dataset looks almost the same as ScoreData, with one important addition on the first line:

INPUTPython
from torch.utils.data import Dataset

class ScoreDataset(Dataset):
    def __init__(self, names, scores):
        self.names = names
        self.scores = scores

    def __len__(self):
        return len(self.names)

    def __getitem__(self, i):
        return self.names[i], self.scores[i]

ds = ScoreDataset(["Asha", "Ravi", "Mei"], [90, 75, 82])
print(len(ds), ds[2])
# 3 ('Mei', 82)

Why inherit from Dataset? It is PyTorch's way of saying "this class is a dataset". PyTorch's tools, such as the DataLoader that later feeds your data to a network in batches, are built to work with Dataset objects. They only ever ask a dataset two things, len(ds) and ds[i], which is exactly why the two methods we write are __len__ and __getitem__.

We will build real versions in Dataset and DataLoader and A Simple MLP in PyTorch. You do not need PyTorch installed for this module; the Python ideas are what matter here.

Common mistakes

Classes introduce a few mistakes that almost everyone makes at first. Each message below is exactly what Python prints, so learning to recognise it will save you time.

Mistake What Python says
Forgot self in the method's brackets: def average(): TypeError: Student.average() takes 0 positional arguments but 1 was given
Forgot self. when using an attribute: sum(scores) NameError: name 'scores' is not defined. Did you mean: 'self.scores'?
Called predict before fit AttributeError: 'MeanPredictor' object has no attribute 'value_'
Forgot the brackets when making the object: model = MeanPredictor TypeError: MeanPredictor.fit() missing 1 required positional argument: 'values'
Forgot super().__init__() in a PyTorch model AttributeError: cannot assign module before Module.__init__() call

Notice how helpful the second message is: Python even suggests self.scores as the fix.

How to practise

  • Type the classes yourself. Class syntax is easy to recognise and surprisingly easy to forget when you have to begin from a blank cell.
  • Make two objects. Give them different attributes and change one. This is the quickest way to see that each instance keeps its own state.
  • Predict before running. Before each print, say what you expect. If Python disagrees, step through the code and find the point where your picture was wrong.
  • Change one part at a time. Add a method, rename an attribute or make a child class override a parent method.
  • Explain the pattern aloud. You should be able to say what self, __init__, inheritance and super() are doing without relying on the exact example.

Check yourself

Try each exercise before opening its answer. If one feels difficult, return to the matching part of your main course and then try again from a blank cell.

If these exercises felt comfortable, you know enough OOP to continue. You do not need to memorise every special method or learn elaborate class design before moving on.

What comes next

Resources

SourceTitleWhy read it
HarvardCS50P, week 8: Object-Oriented ProgrammingOur recommended main course. It includes a lecture, written notes and a problem set.
Corey SchaferPython OOP Tutorial 1: Classes and InstancesA clear, short video on __init__, self and methods.
Corey SchaferPython OOP Tutorial 4: InheritanceSubclasses and super(), with the same example continued.
Real PythonObject-Oriented Programming (OOP) in PythonA long written tutorial, if you prefer reading.
Python.orgThe Python Tutorial, chapter 9: ClassesSections 9.3 (A First Look at Classes) and 9.5 (Inheritance).
PyTorchBuild the Neural NetworkWhere you will meet nn.Module classes for real. Read it later, in the deep learning part.

Practice problems

SolvedSourceProblemDifficultyTags
CS50P Problem Set 8 Medium classes

Finished this module? Mark it done.

Comments

Stuck on something, spotted a mistake, or found a resource that helped you? Tell us below.

Loading comments