All 24 modules are open from the start — nothing here is
locked, and nothing costs anything. Sign in so your progress, titles and
credentials stay with you, on every device you use.
Free forever, with your Google account. No password, no payment.
When data and the things you do to it belong together
You already have containers for data and functions for behaviour. A
class is for the cases where the two keep travelling together — where every
function takes the same dictionary as its first argument, and every caller has
to remember which keys are supposed to be in it. That is the signal. Not every
piece of data needs one.
__init__ runs when an instance is created. It is not a
constructor in the sense other languages mean — the object already
exists by then; __init__ is where you set its attributes.
self is the instance the method was called on. It is an
ordinary first parameter, and Python passes it for you:
order.total() is Order.total(order). Forget to
declare it and you get
TypeError: total() takes 0 positional arguments but 1 was
given, which is confusing until you know that the one argument is
the object itself.
Attributes assigned through self belong to that instance
alone. Two orders have two name attributes with no
connection between them.
The signal that you want a class
Every function takes the same dictionary as its first argument, and
every caller has to remember which keys ought to be in it. A class
gives that shape a name, puts the functions with it, and makes
__init__ the one place the shape is decided.
Quick check
What is self?
2
The Two Methods Worth Writing Every Time
Print an instance of a class with no __repr__ and you get
<Order object at 0x7f3c…>. That is useless in a log,
useless in a debugger, and useless in a list of them.
The convention is that __repr__ reads like the code that
would rebuild the object. !r inside the f-string calls
repr() on the value, which is what puts the quotes around
the string — without it you get name=tea and cannot tell a
string from a name.
The second one is equality. By default two objects are equal only if they
are the same object, so two orders built from identical data are
not equal:
def __eq__(self, other):
if not isinstance(other, Order):
return NotImplemented
return (self.name, self.quantity) == (other.name, other.quantity)
The isinstance guard matters. Without it, comparing an
Order to a string raises an AttributeError
rather than simply answering False. Returning
NotImplemented tells Python to try the other object's
comparison, and to fall back to "not equal" if that also declines.
Quick check
Two Order objects built with identical arguments. Are they equal?
3
The Attribute Every Instance Shares
An attribute assigned in the class body, rather than in
__init__, belongs to the class — one copy,
shared by every instance ever created.
class Item:
tags = [] # ONE list, for every Item
def __init__(self, name):
self.name = name # one per instance
a = Item("tea")
b = Item("cup")
a.tags.append("hot")
b.tags # ['hot'] — b's tags, changed by a
This is the mutable default argument from module 3-02 wearing a different
hat, and it has the same fix: anything mutable belongs in
__init__, where it is built fresh per instance.
Class attributes are genuinely useful for things that really are shared
and really do not change — a constant, a default, a counter of how many
instances exist. The rule is the same as everywhere else: shared
and mutable is the dangerous combination.
One list, shared
class Item: tags = []
Every instance appends to the same list, forever.
One list each
def __init__(self): self.tags = []
Built fresh every time an instance is created.
One more distinction worth having. A method that returns
a value and changes nothing can be called twice safely; one that changes
the instance cannot. Name them so the difference is visible —
total() against add_item() — and do not have
the second one return the object, for the same reason
list.sort() returns None.
Quick check
Where should self.history = [] go?
4
A Property, and Knowing When to Stop
@property turns a method into something that reads like an
attribute. It is for a value derived from the others, so it
cannot drift out of step with them:
@property
def total(self):
return self.quantity * self.price
order.total # no brackets — and always current
Storing self.total in __init__ would work until
somebody changed the quantity, at which point the total is a snapshot of
a price that no longer applies — module 1-02's stale-total bug, living
inside an object.
And the important half of this module: most data does not need a
class.
A class with one method
and no state worth keeping is a function with extra steps.
A class that only holds data
is a dictionary, or a dataclass — which is the next module.
Data and behaviour that travel together
and a shape worth naming and validating in one place.
Several instances with independent state
each doing the same things to different data.
The honest test
Write the class. Then look at whether any method uses more than one
attribute, and whether any two methods use the same one. If the answer
is no both times, you have a namespace rather than an object, and a few
plain functions would say the same thing with less ceremony.
Classes are not the goal. Naming a shape once, validating it once, and
keeping the functions that need it nearby — that is the goal.
Quick check
Why make total a property rather than setting self.total in __init__?
0 of 9 completed
Real Python runs right here in your browser — nothing to install, nothing
sent to a server. The interpreter downloads once the first time you press
Run, then stays cached.
01
To do
__init__ runs when an instance is created, and
self is the instance. Attributes assigned through it belong
to that instance alone.
class Order: def __init__(self, name, quantity): self.name = name self.quantity = quantity
Your task: write Order taking a name, a
quantity and a price, build two of them, and print four lines:
tea 3 whisk 990
your_code.py
PythonCtrl↵ to run
Hint
def __init__(self, name, quantity, price): then three lines assigning each one to self.
Output
02
To do
A method is a function defined inside the class, taking
self first. It can reach every attribute of the instance it
was called on.
The convention is that it reads like the code that would rebuild the
object. !r calls repr() on the value, which is
what puts the quotes round the string — without it you get
name=tea and cannot tell text from a name.
Your task: add __repr__, then print one
order and a list of two — a list uses each item's repr, which is why this
is the method that matters:
One f-string: f"Order(name={self.name!r}, quantity={self.quantity}, price={self.price})". Only the name needs !r — the numbers print the same either way.
Output
04
To do
order.total() is Order.total(order). Python
always passes the instance as the first argument, so a method that does
not declare a parameter for it receives one it has no room for.
The message —
total() takes 0 positional arguments but 1 was given —
is exactly accurate and confusing until you know that the one argument is
the object itself.
Your task: run it, read the error, and fix both methods:
3750 tea
your_code.py
PythonCtrl↵ to run
Hint
Both methods need self as their first parameter. The bodies already use it — they were just never given it.
Output
05
To do
An attribute assigned in the class body belongs to the
class — one copy, shared by every instance ever created.
For a constant that is fine. For a list it is the mutable default
argument from module 3-02, wearing a different hat.
Run it and watch the second item report a tag it was never given.
Your task: give each item its own list, so the output
is:
['hot'] [] 1 0
your_code.py
PythonCtrl↵ to run
Hint
Move the list into __init__ as self.tags = [], and delete the class-body line. __init__ runs once per instance, so each one gets its own.
Output
06
To do
By default, two objects are equal only if they are the
same object. So two orders built from identical arguments are
not equal, which is rarely what anyone means.
def __eq__(self, other): if not isinstance(other, Order): return NotImplemented return (self.name, self.quantity) == (other.name, other.quantity)
The isinstance guard matters: without it, comparing an order
to a string raises an AttributeError instead of simply
answering False.
Your task: add __eq__ comparing all three
attributes, then print four comparisons:
True False False True
The last one is an order compared with a string, which must answer
False rather than raising.
your_code.py
PythonCtrl↵ to run
Hint
Guard with isinstance and return NotImplemented when it is not an Order. Then compare the three attributes as tuples — Python compares tuples element by element.
Output
07
To do
Some methods work something out; others change the instance. The
difference should be visible in the name, and a method that changes
things should not also hand the object back — for the same reason
list.sort() returns None.
Your task: write a Basket with an
items list of its own, an add() that appends
and returns nothing, and a total() that works the value out
without changing anything:
None 2 4000 4000
The last two lines are total() called twice — it must give
the same answer both times.
your_code.py
PythonCtrl↵ to run
Hint
__init__ sets self.items = []. add appends a tuple of (name, quantity, price) and has no return at all. total loops the items and adds quantity * price.
Output
08
To do
@property turns a method into something read like an
attribute, with no brackets. It is for a value derived from the others,
so it cannot drift out of step with them.
Storing self.total in __init__ works until
somebody changes the quantity — at which point the total is a snapshot of
a state that no longer exists. That is module 1-02's stale-total bug,
living inside an object.
Your task: make total a property, then print
it, change the quantity, and print it again:
3750 12500
your_code.py
PythonCtrl↵ to run
Hint
Remove the self.total line from __init__ and add a method called total with @property on the line above it. The two prints then need no brackets and no other change.
Output
09
To do
This class holds no state between calls. Every method takes what it needs
as arguments, uses no attribute, and could be called on any instance with
the same result.
That is not an object. It is a namespace with ceremony — and two plain
functions say the same thing with less of it.
The honest test: does any method use more than one attribute, and do any
two methods use the same one? Here, neither.
Your task: rewrite it as two module-level functions with
the same names, and update the two calls:
3750 3375.0
your_code.py
PythonCtrl↵ to run
Hint
Take the two methods out of the class, drop the self parameter from each, delete the class and the instance, and call them directly.
Output
The order class
To do
One class, doing everything this module covered: validating its shape once,
printing usefully, comparing by contents, keeping a derived value honest,
and having exactly one method that changes anything.
Write Order:
__init__(self, name, quantity, price) — stores all three,
and gives every instance its own empty notes list. It raises
ValueError naming the offending value when the quantity is
negative or the price is zero or less.
total — a property, quantity times price.
add_note(self, text) — appends to this order's notes and returns nothing.
__eq__ — equal when the three values match. Comparing to
anything that is not an Order answers False
rather than raising.
Then print exactly six lines:
Order(name='tea', quantity=3, price=1250) 3750 12500 True ['checked', 'packed'] price must be positive: 0
In order: the repr, the total, the total again after the quantity changes
to 10, an equality check against an identical order, one order's notes
after two are added, and the message from a rejected order.
your_code.py
PythonCtrl↵ to run
Hint
In __init__, validate before assigning anything: raise ValueError(f"quantity must not be negative: {quantity}") and the same shape for the price. self.notes = [] belongs there too, so each order gets its own. total gets @property and takes only self. add_note appends and has no return. __eq__ guards with isinstance and returns NotImplemented, then compares the three values as tuples.