Python Module 9 – Object-oriented Python and the data model
Classes, objects and __init__
Define Python classes with __init__, attributes and methods, see how self is passed on every call, and keep class and instance attributes apart.
What you will learn
- Define a class with __init__, attributes and methods, and create instances of it
- Explain what self is and how a method call passes the instance
- Predict whether reading an attribute finds the instance's value or the class's
- Distinguish class attributes from instance attributes and avoid sharing a mutable one by mistake
Before you start
What you’ll learn · Before you start
What you will learn
- Define a class with __init__, attributes and methods, and create instances of it
- Explain what self is and how a method call passes the instance
- Predict whether reading an attribute finds the instance's value or the class's
- Distinguish class attributes from instance attributes and avoid sharing a mutable one by mistake
Before you start
On this page
Forty students, each with a name and a list of marks, can live in two lists side by side, until someone sorts one of
them and the marks no longer line up with the names. A class: A type of object defined with a class statement. The class holds the methods and the class attributes, and calling it, as in Student("Asha", [78, 91, 85]), creates a new object of that type. keeps each student’s data
together with the operations on it: class Student: defines a new type of object, and calling it, as in
Student("Asha", [78, 91, 85]), creates one object of that type, an instance: An object made by calling a class. Each instance keeps attributes of its own in its own dictionary; type(obj) gives the class it was made from, and isinstance(obj, Student) checks it., then runs
the class’s __init__ method to give the object its first attribute: A name attached to an object and read with a dot, as in asha.name. A read looks in the object first and then in its class; an assignment stores the name in the object itself..
Functions written inside the class are its method: A function that belongs to a value and is called with a dot after it, such as text.strip(). String methods return a new string and leave the original as it was. More in “Essential string methods for cleaning text”: asha.average() runs average with
asha passed in as the first parameter, which by convention is named self: The usual name of a method's first parameter: the instance the method was called on. A call such as asha.average() passes asha in as self, so the method works on that object's attributes.. Below you will
step through what a call of a class does, spell a method call out the long way, follow Python’s search for an attribute, keep the attributes
every instance shares apart from each object’s own, and reproduce the bug that puts one customer’s shopping in every
basket.
Calling a class makes a new object and fills it in
Here is a small class and two students made from it:
class Student:
"""A student and the marks they scored."""
def __init__(self, name, marks):
self.name = name
self.marks = list(marks) # a list of this student's own
def average(self):
return sum(self.marks) / len(self.marks)
def add_mark(self, mark):
self.marks.append(mark)
asha = Student("Asha", [78, 91, 85])
ravi = Student("Ravi", [64, 70])
asha.add_mark(96)
print(asha.name, asha.marks, asha.average())
print(ravi.name, ravi.marks, ravi.average())
print(type(asha).__name__, isinstance(asha, Student))
print(asha is ravi, asha.marks is ravi.marks)
print(vars(ravi)) Output
Asha [78, 91, 85, 96] 87.5
Ravi [64, 70] 67.0
Student True
False False
{'name': 'Ravi', 'marks': [64, 70]}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 student.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
- The
classstatement runs its body once and creates a class object calledStudent. Class names use CapWords, as PEP 8 recommends, which tells a reader at a glance thatStudent(...)creates an object. Student("Asha", [78, 91, 85])makes a new, empty instance and calls__init__on it with the arguments, so inside__init__the nameselfis the new object.__init__creates attributes by assigning toself, and the call of the class returns the finished object.- Methods are ordinary functions defined in the class body. Each one receives the instance as its first parameter, so
self.marksinsideaverage()means the marks of whichever student the method was called on. - Each instance has attributes of its own.
asha.add_mark(96)changed Asha’s list only, because__init__built a new list for every student withlist(marks), which also copies the caller’s list instead of keeping a reference to it. type(asha)is the class,isinstance(asha, Student)asks whether an object is an instance of a class, andvars(ravi)shows the attributes of one instance as a dictionary.
Step through the first call of the program and watch the new object, on the right, fill in:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The call. Your code calls the class with two arguments, the name "Asha" and the list [78, 91, 85]. Calling a class is how an object of that class is made.
- An empty object. The class first makes a new, empty object of type Student. That is the job of __new__, a special method that classes rarely replace. The object has no attributes yet.
- __init__ on the new object. Python then calls __init__ with the new object as self, followed by the same two arguments, so inside __init__ the name is "Asha" and marks is the caller's list.
- self.name = name. Assigning to self.name stores "Asha" in the object's own dictionary, the one that vars(asha) shows.
- self.marks = list(marks). list(marks) builds a new list with the same three marks, so the object owns its list. The later asha.add_mark(96) changes this list, never the caller's.
- __init__ returns None. __init__ ends without a return statement, so it returns None, as it must. Returning any other value would make the call of the class fail with TypeError.
- The finished object. The call of the class returns the object that __init__ filled in, and the assignment gives it the name asha. Student("Ravi", [64, 70]) repeats every step with a new, empty object of its own.
Strictly, creating the empty object is the job of another special method, __new__, which a class rarely needs to
replace; __init__ only fills the object in, as the second and third steps showed. That is why __init__ must return
None: a return with any other value makes the call fail with TypeError.
Checkpoint
Check: Calling a class builds an instance
self is the object the method was called on
The name self is a convention, not a keyword. What is special is the call itself: when you call a method through
an instance, Python passes that instance as the first argument. One function, stored once in
the class, serves every student. Step through the two calls of average() in the program above and watch what self
is in each:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- One function, two objects. asha and ravi each hold their own marks, which __init__ set. The class holds one function, average, which neither object stores.
- asha.average(). Python finds no average in asha's own dictionary, so it looks in the class and finds the function there.
- self is asha. The call becomes Student.average(asha): the object before the dot is passed as the first argument, which the method names self.
- asha's marks. Inside the method, self.marks is asha's list, so sum(self.marks) / len(self.marks) is 350 / 4 = 87.5, the average the program printed for Asha.
- ravi.average(). The same function runs for ravi. Nothing is copied into ravi; only the object passed in as self changes.
- self is ravi. Now self is ravi, so self.marks is [64, 70] and the method returns 134 / 2 = 67.0, the second average the program printed.
Both calls ran the same function, and only self differed, so each read its own marks. These three calls of a
counter’s method do the same thing:
class Counter:
def __init__(self, start=0):
self.value = start
def step(self, by=1):
self.value += by
return self.value
c = Counter(10)
print(c.step(5)) # the usual call
print(Counter.step(c, 5)) # the same call, spelled out
print(type(c).step(c, 5)) # type(c) is Counter
bound = c.step # a bound method: the function plus the instance
print(bound.__self__ is c, bound.__func__ is Counter.step)
print(bound()) # self is already filled in
try:
Counter.step() # through the class nothing fills in self
except TypeError as error:
print("TypeError:", error) Output
15 20 25 True True 26 TypeError: Counter.step() missing 1 required positional argument: 'self'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 binding.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
c.step(5) is shorthand for Counter.step(c, 5), and because type(c) is Counter, also for
type(c).step(c, 5). Reading c.step without calling it gives a bound method: What reading a method through an instance gives: the function together with that instance, so calling it later passes the instance in as self. c.step is a bound method; c.step(5) calls it., a
small object that holds the function in __func__ and the instance in __self__, so calling it later fills in self
by itself. Through the class nothing is filled in: Counter.step() has no instance to pass, and Python names the
missing argument.
The same rule explains two messages that beginners meet. A method written without the parameter, such as
def step(by):, receives the instance in by, and a call with one more argument fails with “takes 1 positional
argument but 2 were given”. And a name without self. in front of it, inside a method, is a local variable, not an
attribute:
class Product:
def __init__(self, name, price):
name = name # BUG: a local variable, gone when __init__ returns
self.price = price
def label(self):
return f"{self.name}: {self.price}"
pen = Product("pen", 20)
print(vars(pen))
print(pen.label()) Output (exit status 1)
{'price': 20}
Printed as an error (standard error)
Traceback (most recent call last):
File "forgot_self.py", line 12, in <module>
print(pen.label())
~~~~~~~~~^^
File "forgot_self.py", line 7, in label
return f"{self.name}: {self.price}"
^^^^^^^^^
AttributeError: 'Product' object has no attribute 'name'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 forgot_self.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
name = name assigned the parameter to itself and vanished when __init__ returned, so vars(pen) holds only
price. Nothing fails until another method reads self.name, which can be far from the line with the bug. When an
AttributeError names an attribute you are sure you set, print vars() of the object and check each assignment in
__init__ for a missing self..
An attribute set without self.
name = name in __init__ only assigns the parameter to itself, a local variable that is gone when the method
returns. Write self.name = name: only an assignment through self stores the value in the object, where other
methods can read it.
Checkpoint
Check: What self is
Python looks in the object first, then in its class
An instance keeps its own attributes in a dictionary, obj.__dict__, which vars(obj) returns. The class has a
dictionary too, holding its methods and its class attribute: An attribute assigned in the class body. Its one value is kept in the class and shared by every instance that has no attribute of its own with that name.. Reading an attribute
through an instance checks the two in order:
Where Python looks for asha.average
Text description of the diagram
The diagram is a flow from top to bottom.
- It starts at the expression asha.average.
- Step 1 looks in asha's own attributes, the dictionary that vars(asha) shows. If the name is there, its value is used.
- If it is not there, step 2 looks in the attributes of the class, vars(Student), where the methods and the class attributes live. If the name is there, it is used; a function found this way comes back as a bound method, with asha filled in as self.
- If neither has the name, Python raises AttributeError.
Assignment never follows this path: asha.x = 1 always writes to asha's own attributes.
Follow three reads and one assignment through those two steps, on the asha of the first program:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- asha.name. Every read through an object starts with the object's own dictionary, the one vars(asha) shows. __init__ stored name there.
- Found in step 1. name is one of asha's own attributes, so the search stops at once and the read gives 'Asha'. The class is never asked.
- asha.average. asha.average starts the same way, but asha's own dictionary has no average. Methods are stored once, in the class, never in the objects.
- Found in step 2. The class has average, so the read gives that function bound to asha, a bound method, which passes asha in as self when it is called.
- asha.grade. Neither asha's dictionary nor the class has grade. With nowhere left to look, the read fails with AttributeError, 'Student' object has no attribute 'grade'.
- A write goes to the object. Assignment never searches. asha.grade = "A" stores grade in asha's own dictionary, so the next read of asha.grade stops at step 1.
Methods are found in the second step, which is why every instance can call them without storing a copy. Writing
follows a different rule: asha.x = 1 stores x in the instance’s own dictionary and never touches the class.
Read through to the class, write to the instance: those two rules explain the rest of this lesson. Later lessons of
this module build on them: inheritance adds the parent classes to the second step, and a property, in the next
lesson, lets a class run a method of its own when a name is read or assigned.
Version note
New in Python 3.13: every class has __static_attributes__, a tuple of the names that its methods assign through
self., collected when the class is compiled. It includes attributes that a method sets later, which vars() of a
new instance does not show yet.
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = list(marks)
def reset(self):
self.marks = []
self.flagged = False
print(Student.__static_attributes__)
print(vars(Student("Asha", [90]))) Output
('flagged', 'marks', 'name')
{'name': 'Asha', 'marks': [90]}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 static_attributes.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Key idea
Reading obj.name looks in the object’s own dictionary first and then in its class; assigning obj.name = …
stores the name in the object itself.
Checkpoint
Check: Object first, then its class
A class attribute is shared until an object assigns its own
A value assigned in the class body is a class attribute, one value for every instance; a value assigned through self
is an instance attribute: An attribute stored in one object's own dictionary, usually set on self in __init__. Each instance has its own value, and vars(obj) shows them., one per
object. Here two accounts share one interest rate, until one of them gets a rate of its own:
class Account:
interest_rate = 0.03 # class attributes: one value, shared
minimum_balance = 500
def __init__(self, owner):
self.owner = owner # instance attribute: one per account
a = Account("Asha")
b = Account("Ravi")
print(a.interest_rate, b.interest_rate, Account.interest_rate)
Account.interest_rate = 0.035 # change it on the class ...
print(a.interest_rate, b.interest_rate) # ... and every account sees it
a.interest_rate = 0.05 # assign through one instance
print(a.interest_rate, b.interest_rate, Account.interest_rate)
print(vars(a))
print(vars(b))
del a.interest_rate # remove the instance attribute
print(a.interest_rate) # the class attribute shows through again
print(sorted(k for k in vars(Account) if not k.startswith("__"))) Output
0.03 0.03 0.03
0.035 0.035
0.05 0.035 0.035
{'owner': 'Asha', 'interest_rate': 0.05}
{'owner': 'Ravi'}
0.035
['interest_rate', 'minimum_balance']
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 class_attributes.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
interest_rateandminimum_balancelive in the class. Every account reads them through the second step of the search, so changingAccount.interest_ratechanged the rate of both accounts at once.a.interest_rate = 0.05did not change the class. It created an instance attribute ona, which hides the class attribute foraalone:vars(a)now holds it andvars(b)does not.del a.interest_rateremoved the instance attribute, and the class attribute showed through again. The last line lists the class’s own names, leaving out the special ones that start with two underscores.
Step through the same run and watch which box answers each read of the rate:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- One rate for both. Neither account has an interest_rate of its own, so both reads go on to the class and give 0.03, the first line the program printed.
- Changed on the class. Account.interest_rate = 0.035 rebinds the class attribute. Both accounts still read through to the class, so both now give 0.035.
- Assigned through a. a.interest_rate = 0.05 leaves the class alone: like every assignment through an object, it stores the name in a's own dictionary.
- Two answers. Now a.interest_rate finds a's own 0.05 before it reaches the class, while b still reads 0.035 from the class. That is the third line, 0.05 0.035 0.035.
- del a.interest_rate. Deleting the attribute removes it from a's dictionary only. The class attribute shows through again, and a.interest_rate gives 0.035.
Then press Edit on the program, add Account.minimum_balance = 1000 and print(a.minimum_balance, b.minimum_balance)
at the end, and run it: the new line prints 1000 1000, because neither account has a minimum_balance of its own.
The table sums the two kinds up:
| Criterion | Class attribute | Instance attribute |
|---|---|---|
| Set in | the class body | a method, on self |
| Copies | one, shared by all | one per instance |
| obj.x = v | hides it for obj | replaces obj's value |
| For all | assign Account.x | not possible |
| When to choose | Shared constants and settings | Each object's own data, and anything mutable |
Change a shared value through the class
Assign Account.interest_rate = 0.035 to change the rate of every account that has no rate of its own. The same
assignment through one account only gives that account a value of its own, which later changes to the class never
reach.
Checkpoint
Check: Class and instance attributes
A list in the class body is one list for every instance
A class attribute that holds a list, a dictionary or a set is shared just like a number, with one difference that causes real bugs: methods usually change such a value in place instead of assigning to it, so the change goes to the class’s object, and every instance sees it.
class Basket:
items = [] # BUG: one list, created once, for every basket
def add(self, item):
self.items.append(item) # finds the class's list and changes it
first = Basket()
second = Basket()
first.add("rice")
second.add("soap")
print(first.items, second.items)
print(first.items is second.items is Basket.items)
print(vars(first))
class FixedBasket:
def __init__(self):
self.items = [] # a new list for every basket
def add(self, item):
self.items.append(item)
first = FixedBasket()
second = FixedBasket()
first.add("rice")
second.add("soap")
print(first.items, second.items)
print(vars(first)) Output
['rice', 'soap'] ['rice', 'soap']
True
{}
['rice'] ['soap']
{'items': ['rice']}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shared_list.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Step through the same program and watch where each append lands:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- One list in the class. items = [] in the class body ran once, when the class was made, so the class holds one list. Neither basket has an items of its own.
- first.add("rice"). self.items.append("rice") first reads self.items. first has no items, so the read finds the class's list, and append changes that list in place.
- second.add("soap"). second has no items either, so its append changes the same list. Both baskets now show ['rice', 'soap'], the first line the program printed.
- Nothing in the baskets. Nothing was ever assigned through self, so vars(first) is still {}. first.items, second.items and Basket.items are one list, which is why the second line is True.
- The fix. FixedBasket creates the list in __init__, which runs once for every new object. The program builds two new baskets with it, under the same names, and self.items = [] gives each a list of its own.
- A list each. Each add now changes its own basket's list: first holds ['rice'] and second holds ['soap'], and vars(first) is {'items': ['rice']}, the last line.
self.items.append(...) reads self.items, finds the class’s list in the second step and appends to it. Nothing is
ever assigned to the instance, so vars(first) is empty and both baskets show both items. The fix is to create the
list in __init__, which runs once for every new object. A mutable default value, as in
def __init__(self, items=[]), hides the same trap, which the lesson on parameters
explains: the default is created once, when the function is defined.
A list in the class body
items = [] in the class body makes one list, when the class is created, and every self.items.append(...)
changes that one list. Write self.items = [] in __init__, which runs for every new object and gives each one a
list of its own.
Numbers fail the other way round. self.issued += 1 looks as if it updates a shared counter, but
augmented assignment: An assignment that combines an operator with =, such as count += 5. A number or a string is rebound to the result, while += changes a list in place. More in “Operators, precedence and the walrus” reads first and assigns second, and the assignment lands
on the instance:
class Ticket:
issued = 0 # meant to count every ticket ever made
def __init__(self, seat):
self.seat = seat
self.issued += 1 # reads the class's 0, then writes an instance attribute
a = Ticket("A1")
b = Ticket("A2")
print(Ticket.issued, a.issued, b.issued)
class CountedTicket:
issued = 0
def __init__(self, seat):
self.seat = seat
type(self).issued += 1 # rebinds the class attribute itself
a = CountedTicket("A1")
b = CountedTicket("A2")
print(CountedTicket.issued, a.issued, vars(a)) Output
0 1 1
2 2 {'seat': 'A1'}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 count_tickets.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Step through both versions and watch where each read finds issued and where the new value goes:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A count in the class. issued = 0 in the class body is meant to count every ticket ever made. Each __init__ runs self.issued += 1.
- Read through self. In a's __init__, self.issued += 1 first reads self.issued. a has no issued yet, so the read finds the class's 0.
- Written to self. Then it assigns 0 + 1 to self.issued, and an assignment through an object stores the name in that object. a now holds an issued of its own, 1, and the class still holds 0.
- The same for b. b repeats both steps, reading the class's 0 and storing 1 in itself. So the program printed 0 1 1, the class's 0 and each ticket's own 1.
- The fix. CountedTicket writes type(self).issued += 1, which reads and assigns the class attribute itself. After two tickets the class holds 2 and each ticket holds only its seat, so the program printed 2 2 {'seat': 'A1'}.
Each ticket ended up with an issued of its own, 1, while the class kept 0. To change a class attribute from a
method, assign to the class: type(self).issued += 1 as here, or Ticket.issued += 1 when the subclasses that a
later lesson introduces should share one count.
Key idea
A mutable value in the class body is one object that every instance shares: create it in __init__, and change a
shared count through the class, never through self.
Checkpoint
Check: A mutable value in the class body
Practise: a bank account that keeps its own history
The exercise asks for a complete BankAccount class with deposits, withdrawals and interest. Its starter code has
the bug of the last section, which the sample tests catch: every account must keep its own list of transactions,
while the interest rate stays one class attribute for every account that has not set its own.
Exercise · Easy · Python
A bank account class whose accounts keep their own history
Complete the class BankAccount in bank.py. The starter code has a bug that the tests catch: find it as well.
BankAccount(owner, balance=0)storesownerandbalance, and gives the account its own empty list,transactions.deposit(amount)addsamountto the balance, appends("deposit", amount)totransactionsand returns the new balance.withdraw(amount)takesamountfrom the balance, appends("withdraw", amount)and returns the new balance.- Both raise
ValueErrorfor an amount of 0 or less, andwithdrawalso for an amount larger than the balance. A refused amount changes nothing: neither the balance nor the list. add_interest()computesround(balance * interest_rate, 2), adds it to the balance, appends("interest", interest)and returns the interest.interest_rateis a class attribute,0.04. SettingBankAccount.interest_ratechanges the rate of every account that has not set its own; setting it on one account changes only that account.
Two accounts must never share their transactions: after a.deposit(10), an account b created at the same time still has b.transactions == [].
Starter code · bank.py
class BankAccount:
interest_rate = 0.04 # one rate for every account
transactions = []
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
"""Add amount (more than 0), record ("deposit", amount), return the new balance."""
self.balance += amount
self.transactions.append(("deposit", amount))
return self.balance
def withdraw(self, amount):
"""Take amount (more than 0, at most the balance), record ("withdraw", amount), return the new balance."""
return self.balance
def add_interest(self):
"""Add round(balance * interest_rate, 2), record ("interest", interest), return the interest."""
return 0 The sample tests · test_bank.py
from bank import BankAccount
def raises_value_error(call, *args):
try:
call(*args)
except ValueError:
return True
return False
def test_deposit_and_withdraw():
"""deposits and withdrawals change the balance and return it"""
acc = BankAccount("Asha", 100)
assert acc.owner == "Asha"
assert acc.deposit(50) == 150
assert acc.withdraw(30) == 120
assert acc.withdraw(120) == 0
assert acc.balance == 0
assert acc.transactions == [("deposit", 50), ("withdraw", 30), ("withdraw", 120)]
def test_rejects_bad_amounts():
"""amounts of 0 or less, and withdrawals above the balance, raise ValueError and change nothing"""
acc = BankAccount("Ravi", 40)
for bad in (0, -5):
assert raises_value_error(acc.deposit, bad), f"deposit({bad}) should raise ValueError"
for bad in (0, -5, 41):
assert raises_value_error(acc.withdraw, bad), f"withdraw({bad}) should raise ValueError"
assert acc.balance == 40
assert acc.transactions == []
def test_own_transactions():
"""every account keeps its own list of transactions"""
a = BankAccount("Asha")
b = BankAccount("Ravi")
a.deposit(10)
b.deposit(20)
b.withdraw(5)
assert a.transactions == [("deposit", 10)]
assert b.transactions == [("deposit", 20), ("withdraw", 5)]
assert a.transactions is not b.transactions
def test_interest_rate_is_shared():
"""the class-level rate applies to every account that has not set its own"""
a = BankAccount("Asha", 1000)
b = BankAccount("Ravi", 500)
assert a.add_interest() == 40.0
old = BankAccount.interest_rate
try:
BankAccount.interest_rate = 0.05
assert b.add_interest() == 25.0
b.interest_rate = 0.1
assert b.add_interest() == 52.5
assert a.add_interest() == 52.0
finally:
BankAccount.interest_rate = old
assert a.balance == 1092.0
assert a.transactions == [("interest", 40.0), ("interest", 52.0)] A hint
The class body creates transactions = [] once, so every account appends to the same list. Create the list in __init__ instead, with self.transactions = []. In deposit and withdraw, check the amount before you change anything: if amount <= 0: raise ValueError("the amount must be more than 0"), and in withdraw also refuse an amount larger than self.balance. add_interest reads self.interest_rate, which finds the account's own value if it has one and the class's value otherwise.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Last, a run of questions on objects, self, attribute lookup and shared attributes. It opens with whichever of these
ideas your answers on this page found hardest, and explains every option you pick:
Practice that adapts to you
Practise this lesson
13 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is self in a Python method, and why does a method need it?
self is the first parameter of a method: the instance the method was called on. Python passes it automatically
when a method is called through an instance, so account.deposit(50) runs Account.deposit(account, 50), and inside
the method self.balance means that account’s balance. The name is a convention, not a keyword, but every Python
programmer expects it. A method needs the parameter because it is one function, stored once in the class and shared
by all instances: the instance passed in is how it knows whose attributes to read and change. Reading
account.deposit without calling it gives a bound method, which has already captured the instance. A method defined
without the parameter fails as soon as it is called through an instance, with “takes 0 positional arguments but 1 was
given”.
Key takeaways
class Name:creates a new type; calling the class creates an instance and runs__init__, which sets the instance’s attributes onself.obj.method(x)istype(obj).method(obj, x):selfis the instance, passed in as the first argument.- Reading
obj.namelooks in the instance’s own attributes first and then in the class; assigningobj.name = …writes to the instance. - A class attribute is shared by every instance; an instance attribute with the same name hides it for one object.
- Create lists, dictionaries and other mutable values in
__init__, or every instance shares one; change a class attribute through the class, never throughself. vars(obj)shows an object’s own attributes, which makes most attribute bugs visible.
Terms in this lesson
- attribute
- A name attached to an object and read with a dot, as in asha.name. A read looks in the object first and then in its class; an assignment stores the name in the object itself.
- bound method
- What reading a method through an instance gives: the function together with that instance, so calling it later passes the instance in as self. c.step is a bound method; c.step(5) calls it.
- class
- A type of object defined with a class statement. The class holds the methods and the class attributes, and calling it, as in Student("Asha", [78, 91, 85]), creates a new object of that type.
- class attribute
- An attribute assigned in the class body. Its one value is kept in the class and shared by every instance that has no attribute of its own with that name.
- instance
- An object made by calling a class. Each instance keeps attributes of its own in its own dictionary; type(obj) gives the class it was made from, and isinstance(obj, Student) checks it.
- instance attribute
- An attribute stored in one object's own dictionary, usually set on self in __init__. Each instance has its own value, and vars(obj) shows them.
- self
- The usual name of a method's first parameter: the instance the method was called on. A call such as asha.average() passes asha in as self, so the method works on that object's attributes.
References
- The Python Tutorial: a first look at classes (Python Software Foundation)
- The Python Tutorial: class and instance variables (Python Software Foundation)
- The Python Language Reference: data model, custom classes and class instances (Python Software Foundation)
- The Python Language Reference: object.__new__() (Python Software Foundation)
- The Python Language Reference: object.__init__() (Python Software Foundation)
- Descriptor Guide: functions and methods (Python Software Foundation)
- Built-in Functions: vars() (Python Software Foundation)
- What's New In Python 3.13: other language changes (__static_attributes__) (Python Software Foundation)
- PEP 8: class names (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress