Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free: 20 minutes

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:

A Student class with marks and an average Python · student.py
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

  • The class statement runs its body once and creates a class object called Student. Class names use CapWords, as PEP 8 recommends, which tells a reader at a glance that Student(...) 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 name self is the new object. __init__ creates attributes by assigning to self, 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.marks inside average() 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 with list(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, and vars(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:

Interactive · message by message What Student("Asha", [78, 91, 85]) does Step through the call. Each arrow is a call with its arguments, or what a call returns; the column on the right shows what the new object holds.
What Student("Asha", [78, 91, 85]) does, step 7 of 7. Messages so far: Your code to class Student: Student("Asha", …); class Student to the new object: __init__(self, …); the new object to class Student: None; class Student to Your code: the new object.Your codeasha = Student(…)class Studentthe class objectthe new objectvars: name, marksStudent("Asha", …)__init__(self, …)Nonethe new object

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. __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.
  4. self.name = name. Assigning to self.name stores "Asha" in the object's own dictionary, the one that vars(asha) shows.
  5. 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.
  6. __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.
  7. 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

  1. One question Student("Ravi", [64, 70]) runs __init__. Inside that call, what is self?

    Choose one answer.

    A hint

    Think about what exists before __init__ starts, in the figure's second step.

    Show the answer: One question

    Answer: The new Student object that the call is building

    Calling a class makes an empty instance and then calls __init__(self, "Ravi", [64, 70]) with that instance as self. __init__ sets the attributes on it, and the call of the class returns the same object.

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:

Interactive · step by step One average() for two students Step through asha.average() and ravi.average(). The objects are on the left, the class and the running call on the right.
One average() for two students, step 6 of 6. Showing asha, ravi, class Student, the running call (self = ravi). In focus: the running call, ravi.finds averagecalls itashamarks [78, 91, 85, 96]ravimarks [64, 70]class Studentholds average(self)the running callself = raviaverage(ravi)returns 67.0

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. asha.average(). Python finds no average in asha's own dictionary, so it looks in the class and finds the function there.
  3. 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.
  4. 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.
  5. ravi.average(). The same function runs for ravi. Nothing is copied into ravi; only the object passed in as self changes.
  6. 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:

One method call, written three ways Python · binding.py
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

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:

A local variable where an attribute was meant Python · forgot_self.py
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

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

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    class Counter:
        def __init__(self, start=0):
            self.value = start
    
        def step(self, by=1):
            self.value += by
            return self.value
    
    c = Counter(10)
    Counter.step(c, 5)
    print(c.step())
    Show the answer: Question 1 of 2

    Answer: 16

    Calling a method through the class does exactly what the usual call does, with the instance written as the first argument: Counter.step(c, 5) raises c.value from 10 to 15. c.step() passes c in by itself and adds 1, giving 16.

  2. Question 2 of 2 A method is written def step(by):, without self. What happens when a program calls c.step(5)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It fails with TypeError: Counter.step() takes 1 positional argument but 2 were given

    A call through an instance passes the instance as the first argument whatever the parameter is called. With one parameter there is no room for 5, so the call fails before the body runs: Counter.step() takes 1 positional argument but 2 were given.

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:

Reading asha.average checks the instance attributes first, then the class attributes, and raises AttributeError when neither has it.asha.average1. asha's own attributesvars(asha)2. the class's attributesvars(Student)found: use it(a function comes backas a bound method)AttributeErrortherenot theretherenot there

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:

Interactive · step by step Three reads and a write on asha Step through asha.name, asha.average, asha.grade and an assignment. The search goes down the left column; a read that succeeds ends on the right.
Three reads and a write on asha, step 6 of 6. Showing the expression (asha.grade = "A"), 1. asha's attributes (vars: name, marks, grade), 2. Student's attributes, AttributeError, found. In focus: 1. asha's attributes.not therenot theretheretherethe expressionasha.grade = "A"1. asha's attributesvars: name, marks, grade2. Student's attributesaverage, add_markAttributeErrornot found anywherefounduse the valuestores grade

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. asha.name. Every read through an object starts with the object's own dictionary, the one vars(asha) shows. __init__ stored name there.
  2. 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.
  3. 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.
  4. 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.
  5. 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'.
  6. 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.

The names a class's methods assign Python · static_attributes.py
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

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

  1. Question 1 of 2 asha.name is read. Where does Python look first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In asha's own dictionary, the one vars(asha) shows

    Reading an attribute through an instance checks the instance's own dictionary first and then the class's. __init__ stored name in asha's dictionary, so the read stops at the first step.

  2. Question 2 of 2 Student has the class attribute school = "City High". After asha.school = "Hill View", what does Student.school give?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: "City High"

    Read through to the class, write to the instance. asha.school = "Hill View" creates an instance attribute on asha, which hides the class's value for asha alone; Student.school and every other student still give "City High".

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:

A shared interest rate, and one account that overrides it Python · class_attributes.py
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

  • interest_rate and minimum_balance live in the class. Every account reads them through the second step of the search, so changing Account.interest_rate changed the rate of both accounts at once.
  • a.interest_rate = 0.05 did not change the class. It created an instance attribute on a, which hides the class attribute for a alone: vars(a) now holds it and vars(b) does not.
  • del a.interest_rate removed 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:

Interactive · step by step One interest rate, two accounts Step through class_attributes.py. The accounts are on the left, the class on the right; each step says which box answers a read.
One interest rate, two accounts, step 5 of 5. Showing a (owner 'Asha'), b, class Account (interest_rate 0.035). In focus: a.reads throughreads throughaowner 'Asha'bowner 'Ravi'class Accountinterest_rate 0.035

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

Class attribute or instance attribute?
Criterion Class attributeInstance attribute
Set in the class bodya method, on self
Copies one, shared by allone per instance
obj.x = v hides it for objreplaces obj's value
For all assign Account.xnot possible
When to choose Shared constants and settingsEach 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

  1. Question 1 of 2 Three accounts share the class attribute interest_rate = 0.03. The program runs a.interest_rate = 0.05 for one of them, then Account.interest_rate = 0.04. How many of the three accounts now read 0.04?

    Type a number.

    A hint

    Which account answers the read from its own dictionary?

    Show the answer: Question 1 of 2

    Answer: 2 accounts

    a has an interest_rate of its own, 0.05, which it finds before it reaches the class. The other two have none, so they read through to the class and see the new 0.04.

  2. Question 2 of 2 Which statement changes the interest rate of every account that has not set a rate of its own?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Account.interest_rate = 0.035

    A class attribute is one value kept in the class. Assigning it through the class changes what every instance without an attribute of its own reads; an assignment through an instance only adds an attribute to that instance.

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.

One list for every basket, then one list each Python · shared_list.py
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

Step through the same program and watch where each append lands:

Interactive · step by step Two baskets and the class's one list Step through shared_list.py. The baskets are on the left with what vars() shows for each, the class on the right.
Two baskets and the class's one list, step 6 of 6. Showing first (vars: items ['rice']), second (vars: items ['soap']), class FixedBasket. In focus: first, second.firstvars: items ['rice']secondvars: items ['soap']class FixedBasketlist made in __init__

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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:

Counting instances: an assignment through self writes to the instance Python · count_tickets.py
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

Step through both versions and watch where each read finds issued and where the new value goes:

Interactive · step by step Counting tickets through self Step through the two tickets of count_tickets.py. The tickets are on the left, the class's count on the right.
Counting tickets through self, step 5 of 5. Showing ticket a (seat 'A1'), ticket b (seat 'A2'), the class (CountedTicket.issued 2). In focus: the class.reads issuedreads issuedticket aseat 'A1'ticket bseat 'A2'the classCountedTicket.issued 2

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Python Online Compiler Change the examples and print vars() of an instance and of its class after every step.

Checkpoint

Check: A mutable value in the class body

  1. Question 1 of 2 Inside __init__, which line makes every new Ticket add 1 to one count kept in the class?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: type(self).issued += 1

    Augmented assignment reads first and assigns second, and an assignment lands where its target says. type(self) is the class, so type(self).issued += 1 changes the class attribute; anything assigned through self lands on the instance.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    class Shop:
        stock = {}
    
        def add(self, item, n):
            self.stock[item] = self.stock.get(item, 0) + n
    
    a, b = Shop(), Shop()
    a.add("pens", 3)
    b.add("pens", 2)
    print(Shop.stock, vars(a))
    Show the answer: Question 2 of 2

    Answer: {'pens': 5} {}

    self.stock[item] = ... first reads self.stock, which finds the class's one dictionary, and then sets a key in it. Both shops therefore add to the same dictionary, which ends as {'pens': 5}, and neither shop has an attribute of its own.

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) stores owner and balance, and gives the account its own empty list, transactions.
  • deposit(amount) adds amount to the balance, appends ("deposit", amount) to transactions and returns the new balance.
  • withdraw(amount) takes amount from the balance, appends ("withdraw", amount) and returns the new balance.
  • Both raise ValueError for an amount of 0 or less, and withdraw also for an amount larger than the balance. A refused amount changes nothing: neither the balance nor the list.
  • add_interest() computes round(balance * interest_rate, 2), adds it to the balance, appends ("interest", interest) and returns the interest.
  • interest_rate is a class attribute, 0.04. Setting BankAccount.interest_rate changes 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.

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.

  1. Question 1 of 13 What does count_tickets.py print?

    What does this program print? Choose one answer.

    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))
    Show the answer: Question 1 of 13

    Answer: it prints

    0 1 1
    2 2 {'seat': 'A1'}

    self.issued += 1 reads self.issued, which finds the class's 0, and then assigns 1 to self.issued, which creates an attribute of the instance. Each ticket gets its own 1 and Ticket.issued stays 0. type(self).issued += 1 assigns to the class instead, so the count reaches 2 and the instances keep no copy of their own.

  2. Question 2 of 13 What does this print?

    Read the code, then choose one answer.

    class Box:
        size = 1
    
    a, b = Box(), Box()
    a.size = 5
    Box.size = 2
    print(a.size, b.size)
    Show the answer: Question 2 of 13

    Answer: 5 2

    a.size = 5 gives a an instance attribute that hides the class attribute. Changing Box.size later changes what b sees, because b has no size of its own, but a keeps finding its own 5 first.

  3. Question 3 of 13 report is an instance of the class Report. Which call does the same as report.render(True)?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: Report.render(report, True)

    A method call through an instance passes the instance as the first argument, self, and then the arguments you wrote. Through the class nothing is filled in, so you pass the instance yourself, first.

  4. Question 4 of 13 What does this print?

    Read the code, then choose one answer.

    class Team:
        members = []
    
        def join(self, name):
            self.members.append(name)
    
    x, y = Team(), Team()
    x.join("Asha")
    print(len(y.members), vars(y))
    Show the answer: Question 4 of 13

    Answer: 1 {}

    members is one list in the class body. self.members.append(...) reads the name, finds the class's list and changes it; nothing is assigned to the instance, so vars(y) is empty, and y sees the name that x added.

  5. Question 5 of 13 Which of these give every instance a list of its own? Choose every right answer.

    Choose every answer that is right.

    Show the answer: Question 5 of 13

    Answer:

    • self.items = [] inside __init__
    • self.items = list(start) inside __init__, where start is a parameter

    Code inside __init__ runs once for every new object, so each call creates a new list and assigns it to that object. A line in the class body runs once, when the class is created, and an annotation does not change that: items: list = [] is still one shared class attribute. list(start) also copies the caller's list, so the object does not share it with the caller either.

  6. Question 6 of 13 A class's __init__ ends with return self. What happens when a program calls the class?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: The call fails with TypeError: __init__() should return None

    The new object is made before __init__ runs and returned by the call of the class. __init__ must return None, and with return self the call fails with TypeError: __init__() should return None, not 'Student' (the class's name).

  7. Question 7 of 13 What does this print?

    Read the code, then choose one answer.

    class Student:
        def __init__(self, name, marks):
            self.name = name
            self.marks = list(marks)
    
    marks = [78]
    asha = Student("Asha", marks)
    marks.append(99)
    print(asha.marks)
    Show the answer: Question 7 of 13

    Answer: [78]

    __init__ ran once, when asha was made, and stored a new list with the same items. The caller's list and asha's list are two lists from then on, so appending 99 to one leaves the other as [78].

  8. Question 8 of 13 A program runs a = Student("Asha", [78]), then b = a, then c = Student("Ravi", [64]). How many Student objects exist?

    Type a number.

    Show the answer: Question 8 of 13

    Answer: 2 objects

    Each call of the class builds one new object, and there are two calls. b = a only gives the first object a second name, so a and b are one object.

  9. Question 9 of 13 What does this print?

    Read the code, then choose one answer.

    class Student:
        def __init__(self, name, marks):
            self.name = name
            self.marks = list(marks)
    
        def average(self):
            return sum(self.marks) / len(self.marks)
    
    asha = Student("Asha", [78, 91, 85])
    later = asha.average
    asha.marks.append(100)
    print(later())
    Show the answer: Question 9 of 13

    Answer: 88.5

    Reading asha.average gives a bound method: the function together with asha. Calling it runs the function now, with self = asha, so it sees the list as it is at the call: (78 + 91 + 85 + 100) / 4 = 88.5.

  10. Question 10 of 13 ravi = Student("Ravi", [64, 70]), with the Student of the first example. What does vars(ravi) show?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: {'name': 'Ravi', 'marks': [64, 70]}

    vars(obj) returns the instance's own dictionary, the first place a read looks. __init__ put name and marks there; the methods are found in the class, in the second step.

  11. Question 11 of 13 class Plan: has the class attribute price = 99, and p = Plan(). What does del p.price do?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: It fails with AttributeError: 'Plan' object has no attribute 'price'

    Reads go on to the class, but assignments and deletions work on the instance's own dictionary only. p has no price there, so del p.price raises AttributeError, and Plan.price stays 99. del Plan.price would delete the class attribute.

  12. Question 12 of 13 interest_rate = 0.03 is written in the body of class Account:, outside every method. Where is the value kept?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: In the class's own dictionary, vars(Account)

    The class body runs once and keeps its names in the class's dictionary. Every account then finds interest_rate in the second step of a read, until it has an attribute of its own with that name.

  13. Question 13 of 13 What does this print?

    Read the code, then choose one answer.

    class Config:
        retries = 3
    
    c1, c2 = Config(), Config()
    c1.retries += 1
    Config.retries = 10
    print(c1.retries, c2.retries)
    Show the answer: Question 13 of 13

    Answer: 4 10

    Augmented assignment through an object reads through to the class but assigns to the object, so c1 ends up with its own retries of 4. Config.retries = 10 changes the class, which c2 still reads and c1 no longer does.

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 on self.
  • obj.method(x) is type(obj).method(obj, x): self is the instance, passed in as the first argument.
  • Reading obj.name looks in the instance’s own attributes first and then in the class; assigning obj.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 through self.
  • 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.