PythonBeginner 12 min Lesson 15 of 30

Day 15 — OOP Basics

Understand what problem classes solve before learning the syntax: grouping data with the behaviour that belongs to it, and when functions are enough.

Python · Lesson 15 of 30
0/30 done(0%)

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 #

PYTHON
# 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 self in a method signature, then getting a confusing argument-count error.
  • Storing a value that could be computed. A stale total attribute 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.

Quick quiz

  1. 1. What problem does a class mainly solve?

  2. 2. What does `__init__` do?

  3. 3. What is `self`?

  4. 4. Why use `@property` for a computed total?

  5. 5. When is a class probably the wrong choice?

Summary

  • A class groups data with the behaviour and rules that belong to it.
  • `__init__` sets up a new object and `self` refers to that object.
  • Computed values belong in properties so they cannot go stale.
  • Dunder methods like `__len__` and `__repr__` make your objects behave like built-ins.
  • Use classes when state needs protecting — not for every group of functions.