Classes & Objects

The Course
January 10, 2026
5 min read

You have been using objects since Day 1. "hello".upper() calls a method on a string object; [1, 2].append(3) calls one on a list object. Today you make your own.

The problem classes solve

Here is a program written with the tools you already have:

Data and behaviour, drifting apart Python
account_owner = "Ada"
account_balance = 100

def deposit(balance, amount):
    return balance + amount

account_balance = deposit(account_balance, 50)
print(account_owner, account_balance)

# Now add a second account, and a third...
other_owner = "Alan"
other_balance = 20

Every new account needs another pair of variables, and nothing links a balance to its owner. A class ties them together.

The same thing as a class Python
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        return self.balance

ada = Account("Ada", 100)
alan = Account("Alan", 20)

ada.deposit(50)

print(ada.owner, ada.balance)
print(alan.owner, alan.balance)

Two independent accounts, each carrying its own data and the behaviour that belongs to it.

The vocabulary

  • A class is the blueprint: Account.
  • An instance is one thing built from it: ada.
  • Attributes are the data on an instance: ada.balance.
  • Methods are functions defined in the class: ada.deposit(50).

__init__ and self

__init__ runs automatically when you create an instance. It is where attributes are set up.

Watching it happen Python
class Book:
    def __init__(self, title, pages):
        print(f"  building a Book: {title}")
        self.title = title
        self.pages = pages
        self.page_read = 0        # attributes not from arguments are fine too

    def read(self, pages):
        self.page_read = min(self.page_read + pages, self.pages)
        return self.progress()

    def progress(self):
        return f"{self.page_read}/{self.pages} pages"

dune = Book("Dune", 412)
print(dune.progress())
print(dune.read(100))
print(dune.read(400))

self is the instance the method was called on. Python passes it automatically: writing dune.read(100) calls read(dune, 100). Every method takes self first — forget it and you get TypeError: read() takes 1 positional argument but 2 were given.

self is a convention, not a keyword

You could call it anything. Nobody does. Call it self and every Python programmer will understand your code instantly.

Instance attributes versus class attributes

An attribute defined in the class body is shared by every instance. One defined on self belongs to that instance alone.

Shared or personal Python
class Dog:
    species = "Canis familiaris"        # class attribute - shared

    def __init__(self, name):
        self.name = name                # instance attribute - personal

rex = Dog("Rex")
fido = Dog("Fido")

print(rex.name, fido.name)
print(rex.species, fido.species)

Dog.species = "Canis lupus familiaris"  # change it once, everyone sees it
print(rex.species, fido.species)
Never share a mutable class attribute

tricks = [] in the class body gives every dog the same list, exactly like the mutable default argument trap from Day 4. Create mutable attributes inside __init__.

Methods that guard the data

The point of putting behaviour beside data is that the class can enforce its own rules.

A class with rules Python
class Account:
    def __init__(self, owner, balance=0):
        if balance < 0:
            raise ValueError("balance cannot start negative")
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError(f"insufficient funds: balance is {self.balance}")
        self.balance -= amount

account = Account("Ada", 100)
account.deposit(50)
account.withdraw(30)
print(account.balance)

account.withdraw(1000)

A leading underscore means “internal”

Python has no private attributes. It has a convention: a name starting with _ is an implementation detail, and other code should leave it alone.

Convention over enforcement Python
class Timer:
    def __init__(self):
        self._start = 0          # internal - do not touch from outside
        self.laps = []           # public

    def lap(self, seconds):
        self.laps.append(seconds)

    def best(self):
        return min(self.laps) if self.laps else None

timer = Timer()
timer.lap(12.4)
timer.lap(11.9)
print(timer.best())
print(timer._start)      # possible, but you are on your own
Exercise

A shopping cart

Write a Cart class with an empty list of items. Give it add(name, price), total() returning the sum of prices, and count() returning how many items. Adding a negative price should raise a ValueError.

class Cart:
    def __init__(self):
        pass          # your code here

    def add(self, name, price):
        pass          # your code here

    def total(self):
        return 0      # your code here

    def count(self):
        return 0      # your code here

cart = Cart()
cart.add("keyboard", 49.99)
cart.add("mouse", 25.50)
print(cart.count(), cart.total())
Show one solution
class Cart:
    def __init__(self):
        self.items = []

    def add(self, name, price):
        if price < 0:
            raise ValueError(f"price cannot be negative: {price}")
        self.items.append((name, price))

    def total(self):
        return sum(price for _, price in self.items)

    def count(self):
        return len(self.items)

cart = Cart()
cart.add("keyboard", 49.99)
cart.add("mouse", 25.50)
print(cart.count(), round(cart.total(), 2))

self.items = [] lives in __init__, so each cart gets its own list — the mutable-attribute rule in practice. Each item is a (name, price) tuple, which is Day 3’s “fixed record” advice applied.

What you learned

  • A class is a blueprint; an instance is one object built from it.
  • __init__ sets up attributes when an instance is created.
  • self is the instance, passed automatically as the first argument.
  • Class attributes are shared; instance attributes belong to one object.
  • A leading underscore marks something as internal by convention.
Last updated on January 10, 2026

Was this article helpful?

Your response is saved on this device.