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
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.
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
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
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.
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())
self.name is 'Rex' for this instance.
speak returns name + ' says woof'.
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
In init, assign each: self.width = width.
area returns self.width * self.height.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
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
The bug is
side = side; it should beself.side = side.Once the attribute is stored, area can read self.side.
class Square:
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
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
Store self.count = 0 in init.
click adds one: self.count = self.count + 1. total just returns self.count.
class Counter:
def __init__(self):
self.count = 0
def click(self):
self.count = self.count + 1
def total(self):
return self.count
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
Store self.items = [] in init, and use self.items everywhere.
push appends; pop returns self.items.pop(); peek returns self.items[-1].
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[-1]
Recap
- A class defines a type; writing
ClassName(...)produces an instance of it. __init__runs at creation time and stores the object's attributes asself.something.- A method is a function on a class;
selfis 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.