Module 5 · Object-Oriented Programming ⏱ 18 min

Classes & Objects

By the end of this lesson you will be able to:
  • Define a class and set up its state in __init__
  • Create instances (objects) and read their attributes
  • Write methods that operate on the instance's own data
  • Explain how self binds a method call to the object it runs on

So far your data has lived in loose variables and dicts. That works for small scripts, but real programs describe things — a customer, a bank account, a button on a screen — each carrying its own data and its own behaviour. A class is the blueprint for such a thing, and an object is one built from it.

class Dog:
    def __init__(self, name):
        self.name = name

rex = Dog('Rex')
print(rex.name)   # Rex

You write the blueprint once with class, then stamp out as many objects (also called instances) as you need. Each one remembers its own data.

flowchart LR
  B["class Dog (blueprint)"] --> I1["Dog('Rex')"]
  B --> I2["Dog('Bo')"]
  style B fill:#3776ab,color:#fff
One blueprint (class) produces many objects (instances), each holding its own data.

Why bundle data with behaviour

Without classes, the facts about one thing tend to scatter. A customer's name sits in a string, their balance in a number, and the code that withdraws from them lives in a function somewhere else. Add a second customer and you need a second name, a second balance, and a way to keep each name paired with the right balance.

The moment data and the code that acts on it drift apart, bugs creep in — a withdrawal applied to the wrong balance, a name updated in one place but not the other. A class solves this by gluing the two together. The data lives on the object as attributes, and the behaviour lives on the object as methods. Pull one customer out of a list and you get its name, its balance, and the functions that know how to handle it, all in a single package.

The anatomy of a class

The __init__ method — read aloud as dunder init — runs automatically the moment you create an object. Its job is to set up that object's attributes, the named data stored on it. Inside it, self.name = name takes the value handed in at construction and pins it onto this particular object as an attribute called name.

After that you read the attribute back with a dot: rex.name. The dot means "belonging to this object." Think of __init__ as the form you fill in once when an object is born — after that, the object carries those values everywhere it goes.

Construction, step by step

Calling Dog('Rex', 'labrador') sets three things in motion. Python first builds a fresh, empty object. It then calls __init__, handing that new object in as self along with the arguments you supplied. Finally the finished object is given back to you, already loaded with its attributes.

Notice you never call __init__ yourself — writing Dog(...) triggers it. That is why the name is marked with double underscores: Python reserves a few such names for behaviour it runs on your behalf at key moments, and this one guards the moment of creation.

A class with two attributes and a method. Each instance keeps its own values.
class Dog:
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

    def speak(self):
        return self.name + ' says woof'

rex = Dog('Rex', 'labrador')
bo = Dog('Bo', 'poodle')

print(rex.name, rex.breed)
print(bo.speak())

Methods: functions that live on a class

A method is a function defined inside a class. Its first parameter is always self — a reference to the object the method was called on — which is how it reads and changes that object's attributes:

class Dog:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return self.name + ' says woof'

print(Dog('Rex').speak())   # Rex says woof

When you write rex.speak(), Python quietly passes rex in as self for you. You never write self at the call site; it appears only inside the definition, where it stands for "whichever object this method is running on right now."

flowchart LR
  Call["you write: rex.speak()"] --> Pass["Python passes rex as self"]
  Pass --> Run["speak(self) runs on rex"]
  Run --> Out["uses self.name"]
  style Call fill:#3776ab,color:#fff
  style Pass fill:#1e293b,color:#fff
You write rex.speak(); Python hands rex in as the self argument so the method runs against the right object.

Each object holds its own state

The payoff of self is that every instance keeps a private copy of its attributes. Build two accounts from the same class and they do not share a balance — depositing into one leaves the other untouched. That isolation is what makes objects safe to use in large numbers: you can create thousands of them without any quietly corrupting its neighbours.

This is also why methods take self instead of reading global variables. Everything an object knows about itself travels with it, on its own attributes, so the same method behaves correctly no matter which object calls it.

flowchart TD
  C["class Account"] --> A["alice = Account(100)"]
  C --> B["bob = Account(50)"]
  A --> PA["alice.balance is 100"]
  B --> PB["bob.balance is 50"]
  style A fill:#3776ab,color:#fff
  style B fill:#3776ab,color:#fff
Two instances built from the same class keep separate attribute values.
deposit changes one account's balance. The other account is unaffected.
class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

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

alice = Account("Alice", 100)
bob = Account("Bob", 50)

alice.deposit(25)
bob.deposit(500)

print(alice.owner, "has", alice.balance)
print(bob.owner, "has", bob.balance)

Methods that use other methods

Because every method receives self, a method can ask the same object to do something else by calling another method through self. That lets an object build complex behaviour out of its own smaller pieces, without any outside code getting involved.

Suppose an account has a deposit method and a withdraw method. Moving money then becomes those two working together inside a third method called transfer_to — the object orchestrates itself. This is how large classes stay readable: each method does one small job, and the others call it by name through self.

Exercise

What does this print? Predict the output of calling .speak() on the instance.

class Dog:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return self.name + ' says woof'

d = Dog('Rex')
print(d.speak())
Exercise

Define a Rectangle class. Its __init__(self, width, height) should store both values as attributes, and an .area(self) method should return width * height.

class Rectangle:
    def __init__(self, width, height):
        # store width and height as attributes
        pass

    # define area(self) below
Exercise

This Square is meant to remember its side length, but __init__ never stores it on the object — so .area() crashes with AttributeError. Store the attribute correctly using self.

class Square:
    def __init__(self, side):
        side = side          # BUG: never stored on the object

    def area(self):
        return self.side * self.side
Exercise

Build a Counter class that starts at zero. .click() adds one to its internal count, and .total() returns the current count. Two counters must be independent — clicking one must not affect the other.

class Counter:
    def __init__(self):
        # start the count at zero
        pass

    def click(self):
        # add one to the count
        pass

    def total(self):
        # return the current count
        pass
Exercise

Build a Stack (last-in, first-out) backed by a list. __init__ starts it empty, push(item) adds to the top, pop() removes and returns the top item, and peek() returns the top item without removing it. (A list's .append and .pop do the heavy lifting; peek reads the last element with [-1].)

class Stack:
    def __init__(self):
        # start with an empty list
        pass

    def push(self, item):
        pass

    def pop(self):
        pass

    def peek(self):
        pass

Recap

  • A class defines a type; writing ClassName(...) produces an instance of it.
  • __init__ runs at creation time and stores the object's attributes as self.something.
  • A method is a function on a class; self is the object it was called on, passed in automatically.
  • Instances do not share attributes — each one's data is its own, so they are safe to create in large numbers.
  • A method can call another method through self, letting an object build big behaviour from small pieces.

Next you will give your classes richer behaviour and let new kinds inherit from existing ones.

Checkpoint quiz

Which method runs automatically when you create a new instance of a class?

Inside a method, what does self refer to?

Why does writing width = width inside init cause self.width to fail later?

Go deeper — technical resources