Skip to content
aviral gupta

// I2.2 · ~30 min · Intermediate

Classes and instances

After this lesson you can define a class with __init__ and methods, create instances that each keep their own data, and choose between instance and class variables.

Lesson 2 of 6 in I2 Classes

You will be able to

  • Define a class with __init__ and methods, create instances and reach their data through self
  • Tell instance variables from class variables, and predict which one an attribute lookup finds
  • Explain how a method call passes the instance as self, and call methods correctly
  1. Warm-up · Activity 1 of 7

    Warm-up from module B4: you have called methods on objects since the first modules. What does this print?

    names = ["bob", "ada"]
    names.sort()
    print(names, "hi".upper())
  2. Predict · Activity 2 of 7

    Predict before you read on: two objects of the same class. What does this print?

    class Counter:
        def __init__(self):
            self.count = 0
    
        def increment(self):
            self.count += 1
    
    
    a = Counter()
    b = Counter()
    a.increment()
    a.increment()
    b.increment()
    print(a.count, b.count)
  3. Practice · Activity 3 of 7

    Fill in the name so that each Point stores its own x.

    class Point:
        def __init__(self, x, y):
            ____.x = x
            self.y = y
    .x = x
  4. Practice · Activity 4 of 7

    kind is a class variable. What does this print?

    class Dog:
        kind = "canine"
    
        def __init__(self, name):
            self.name = name
    
    
    d = Dog("Fido")
    e = Dog("Buddy")
    e.kind = "robot"
    print(d.kind, e.kind, Dog.kind)
  5. Practice · Activity 5 of 7

    Match each expression to what it is or does.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. Two dogs each learn one trick. What does this print?

    class Dog:
        tricks = []
    
        def __init__(self, name):
            self.name = name
    
        def add_trick(self, trick):
            self.tricks.append(trick)
    
    
    d = Dog("Fido")
    e = Dog("Buddy")
    d.add_trick("roll over")
    e.add_trick("play dead")
    print(d.tricks)
  7. Apply · Activity 7 of 7

    Mini-task. Write a class Rectangle whose __init__ takes width and height. Add a method area() that returns width times height, and a method scale(factor) that multiplies both sides by factor. Rectangle(3, 4) has area 12; after scale(2) it is 6 by 8 with area 48.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A shopping cart

Each Cart keeps its owner and its own list of lines, created in __init__. The methods call each other through self: summary uses total, which uses net. vat_rate is a class variable, so changing it on the class changes it for every cart. The third line shows that a method call is the class's function with the instance passed as self.

main.py

class Cart:
    """A shopping cart that holds (name, price, quantity) lines."""

    vat_rate = 0.19  # class variable: the same for every cart

    def __init__(self, owner: str) -> None:
        self.owner = owner
        self.lines: list[tuple[str, float, int]] = []  # a new list for each cart

    def add(self, name: str, price: float, quantity: int = 1) -> None:
        self.lines.append((name, price, quantity))

    def net(self) -> float:
        return sum(price * quantity for _, price, quantity in self.lines)

    def total(self) -> float:
        return round(self.net() * (1 + self.vat_rate), 2)

    def summary(self) -> str:
        count = sum(quantity for _, _, quantity in self.lines)
        return f"{self.owner}: {count} item(s), {self.total():.2f} EUR"


ada = Cart("ada")
ada.add("coffee", 8.50, 2)
ada.add("mug", 12.00)
bob = Cart("bob")
bob.add("tea", 4.20)

print(ada.summary())
print(bob.summary())
print(Cart.summary(bob) == bob.summary())
Cart.vat_rate = 0.07  # changes it for every cart
print(ada.summary())

Run it with

python main.py

Output

ada: 3 item(s), 34.51 EUR
bob: 1 item(s), 5.00 EUR
True
ada: 3 item(s), 31.03 EUR
  • ada and bob have separate lines lists, because each __init__ call creates a new one.
  • self.vat_rate finds the class variable, since no cart has a vat_rate of its own.
  • Cart.summary(bob) is the same call as bob.summary().
  • After Cart.vat_rate = 0.07, every cart uses the new rate.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

A book you are reading

Complete the class Book. Book(title, pages) stores both and starts with current_page at 0. read(n) moves current_page forward by n pages, but never past the last page, and returns the new current_page. is_finished() returns True once current_page equals pages. Each book keeps its own progress.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Set self.current_page = 0 in __init__, next to title and pages.

  2. Hint 2

    In read, add n to self.current_page, and use min(..., self.pages) so it cannot pass the last page.

  3. Hint 3

    is_finished returns the comparison itself: return self.current_page == self.pages.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

class Book:
    def __init__(self, title: str, pages: int) -> None:
        self.title = title
        self.pages = pages
        self.current_page = 0

    def read(self, n: int) -> int:
        """Read n pages, stopping at the last page; return the current page."""
        self.current_page = min(self.current_page + n, self.pages)
        return self.current_page

    def is_finished(self) -> bool:
        return self.current_page == self.pages
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

class Book:
    def __init__(self, title: str, pages: int) -> None:
        self.title = title
        self.pages = pages

    def read(self, n: int) -> int:
        """Read n pages, stopping at the last page; return the current page."""
        return 0

    def is_finished(self) -> bool:
        return False

test_main.py

from main import Book


def test_starts_at_zero():
    """A new book starts at page 0"""
    book = Book("Dune", 600)
    got = getattr(book, "current_page", None)
    assert got == 0, f"current_page of a new book is {got!r}, expected 0"


def test_read_moves_on():
    """read(n) adds n pages and returns the new page"""
    book = Book("Dune", 600)
    got = book.read(40), book.read(10)
    assert got == (40, 50), f"read(40), read(10) returned {got!r}, expected (40, 50)"


def test_stops_at_last_page():
    """read never goes past the last page"""
    book = Book("Emma", 100)
    got = book.read(250)
    assert got == 100, f"read(250) on a 100-page book returned {got!r}, expected 100"


def test_finished():
    """is_finished is True only at the last page"""
    book = Book("Emma", 100)
    book.read(99)
    assert book.is_finished() is False, "is_finished() is True at page 99 of 100"
    book.read(1)
    assert book.is_finished() is True, "is_finished() is False at page 100 of 100"


def test_separate_books():
    """Two books keep separate progress"""
    a = Book("A", 50)
    b = Book("B", 50)
    a.read(20)
    assert b.read(5) == 5, "reading book a changed the progress of book b"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 2

Numbered tickets

Every Ticket should get the next number: 1, 2, 3 and so on, counted in the class variable issued. Each ticket also has its own list of seats. The starter has two bugs: every ticket gets number 1, and all tickets share one seats list. Fix both, and keep issued on the class.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    self.issued += 1 reads the class variable but then creates an instance attribute. Change the class variable through the class: Ticket.issued += 1.

  2. Hint 2

    A list in the class body is shared by every ticket. Create self.seats = [] in __init__ instead.

  3. Hint 3

    Remove the seats line from the class body, and add self.seats: list[str] = [] to __init__.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

class Ticket:
    issued = 0

    def __init__(self, event: str) -> None:
        self.event = event
        Ticket.issued += 1
        self.number = Ticket.issued
        self.seats: list[str] = []

    def add_seat(self, seat: str) -> None:
        self.seats.append(seat)
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

class Ticket:
    issued = 0
    seats: list[str] = []

    def __init__(self, event: str) -> None:
        self.event = event
        self.issued += 1
        self.number = self.issued

    def add_seat(self, seat: str) -> None:
        self.seats.append(seat)

test_main.py

from main import Ticket


def test_numbers_count_up():
    """Tickets are numbered 1, 2, 3"""
    Ticket.issued = 0
    got = [Ticket("concert").number for _ in range(3)]
    assert got == [1, 2, 3], f"three tickets got the numbers {got!r}, expected [1, 2, 3]"


def test_class_counter():
    """issued on the class counts every ticket"""
    Ticket.issued = 0
    Ticket("play")
    Ticket("play")
    assert Ticket.issued == 2, f"Ticket.issued is {Ticket.issued!r} after two tickets, expected 2"


def test_own_seats():
    """Each ticket has its own list of seats"""
    a = Ticket("film")
    b = Ticket("film")
    a.add_seat("A1")
    b.add_seat("B7")
    assert a.seats == ["A1"], f"a.seats is {a.seats!r}, expected ['A1']"
    assert b.seats == ["B7"], f"b.seats is {b.seats!r}, expected ['B7']"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

Forgetting self in a method

class Greeter:
    def hello():
        return "hello"


Greeter().hello()

What Python prints

TypeError: Greeter.hello() takes 0 positional arguments but 1 was given

Why, and the fix

A method call always passes the instance as the first argument, so every method needs a parameter to receive it. Write def hello(self):. The error counts that hidden argument: the "1 was given" is the instance itself.

Using an attribute without self.

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * radius ** 2


print(Circle(2).area())

What Python prints

NameError: name 'radius' is not defined

Why, and the fix

radius was a parameter of __init__, and it disappeared when __init__ returned. What stays is the attribute self.radius. Inside a method, always reach the object's data through self: 3.14159 * self.radius ** 2.

Leaving out an argument of __init__

class Circle:
    def __init__(self, radius):
        self.radius = radius


c = Circle()

What Python prints

TypeError: Circle.__init__() missing 1 required positional argument: 'radius'

Why, and the fix

The arguments of Circle(...) are passed on to __init__, after self. __init__ needs a radius, so pass one: Circle(2). If a sensible default exists, give the parameter one instead: def __init__(self, radius=1.0):.

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

A class makes objects; __init__ sets them up

class Account: defines a new type. Calling it, Account("ada", 50), creates a new instance, calls __init__ on it with those arguments, and returns the instance. __init__ gives each object its starting state: self.owner = owner creates an attribute on this one object. Attributes need no declaration; like variables, they exist once they are assigned. Every instance has its own attributes, so changing a.balance leaves b.balance alone. __init__ returns nothing: it only sets up the object that the call of the class returns.

Methods and self

A function defined in the class body is a method. When you call a.deposit(10), Python finds deposit in the class and passes the instance as the first argument, so the call is the same as Account.deposit(a, 10). That first parameter is called self by convention; the name means nothing special to Python, but every Python programmer expects it. Inside a method, reach the object's data and other methods through self: self.balance, self.deposit(5). A bare balance would be a local variable, not the attribute.

Class variables are shared

A name assigned in the class body, outside any method, is a class variable: one value shared by every instance, such as vat_rate = 0.19. Reading obj.name looks on the instance first, then on the class. Assigning obj.name = value always sets an instance attribute, which then hides the class variable for that one object; to change the value for everyone, assign to the class: Cart.vat_rate = 0.07. A mutable class variable, such as a list, is one list for all instances, so create per-object lists in __init__.

Sources

Last reviewed September 29, 2026