What is it? #
Object-oriented programming is a way of organising code so that data and the operations on that data live together.
Without it, you end up with a dictionary of order fields in one place and a dozen loose functions that all take that dictionary as their first argument. It works, but nothing stops someone building an invalid order, and the rules are scattered.
A class bundles the data and the functions that operate on it into one unit. An object is one concrete instance of that class — one specific order, one specific user.
OOP is a tool, not a requirement. A script that reads a CSV and prints a total does not need a class. You reach for one when the same group of data keeps travelling together, when there are rules about what makes it valid, or when several variations share most of their behaviour.
Think of it like this #
Think of a coffee machine. Inside there is water, beans and a temperature setting — that is its data. Outside there are buttons: brew, clean, descale — that is its behaviour. You do not reach inside to adjust the boiler by hand; you press a button, and the machine keeps itself in a sensible state.
A class is the design of the machine. An object is one machine sitting on the counter.
Simple example #
Compare two versions of the same shopping basket: one built from loose dictionaries and free functions, and one built as a class. The second version makes it impossible to have a basket whose total does not match its items.
Code #
# Without a class — the data and rules drift apart
basket = {"items": [], "total": 0}
def add_item(basket, name, price):
basket["items"].append({"name": name, "price": price})
basket["total"] += price # nothing enforces this stays in sync
add_item(basket, "Book", 499)
basket["total"] = 0 # nothing stops this either
# With a class — the rules live with the data
class Basket:
def __init__(self, owner):
self.owner = owner
self._items = [] # leading underscore: internal
def add(self, name, price):
if price < 0:
raise ValueError("Price cannot be negative")
self._items.append({"name": name, "price": price})
@property
def total(self):
return sum(item["price"] for item in self._items)
def __len__(self):
return len(self._items)
def __repr__(self):
return f"Basket(owner={self.owner!r}, items={len(self._items)})"
b = Basket("Ravi")
b.add("Book", 499)
b.add("Pen", 60)
print(b.total, len(b)) # 559 2
print(b) # Basket(owner='Ravi', items=2)
How it works #
class Basket: defines the design. Nothing exists yet until you create an instance with Basket("Ravi").
__init__ runs automatically when an object is created. It sets up the starting state. self is the object being created, and every attribute you attach to self belongs to that specific object.
self as the first parameter of every method is Python being explicit about something other languages hide. When you call b.add("Book", 499), Python passes b in as self.
_items with a leading underscore is a convention meaning "internal, do not touch from outside". Python does not enforce it, but every Python developer reads it the same way.
@property makes total behave like an attribute while actually running code. You write b.total, not b.total(). Because it is calculated from the items each time, it can never fall out of sync — which was exactly the bug in the dictionary version.
__len__ and __repr__ are dunder (double underscore) methods that hook into built-in behaviour. Defining __len__ makes len(b) work. Defining __repr__ makes printing the object useful instead of showing a memory address — a small habit that pays off every time you debug.
Real-world use #
Classes show up wherever a concept has both state and rules: a user session, a payment, a database connection pool, a parsed configuration, an HTTP client with a base URL and retry policy.
Frameworks lean on them heavily. In a web framework, a model class describes a table and its behaviour; a serializer class validates input; a view class handles a request. Understanding classes is what makes framework code stop looking like magic.
The counterweight is real: plenty of experienced Python developers write mostly functions and only introduce classes when state genuinely needs protecting. A class with no state and one method is just a function wearing a costume.
Common mistakes #
- Creating classes for everything. If it has no state, a function is simpler.
- Forgetting
selfin a method signature, then getting a confusing argument-count error. - Storing a value that could be computed. A stale
totalattribute is a bug waiting to happen — use a property. - Skipping
__repr__, then debugging objects that print as memory addresses. - Treating a class as a namespace for unrelated functions. That is what modules are for.
Practice #
Write a BankAccount class with an owner and a starting balance. Give it deposit and withdraw methods that reject invalid amounts, a balance property that cannot be set directly from outside, and a __repr__ that shows the owner and current balance.