Chapter 6 of 13 · Free, no signup

Support Us

Python Mastery · Chapter 6 · ~22 min · Intermediate

Classes & Objects Visualised

Move from writing loose functions to designing full software systems. See how classes act as factories stamping out independent objects, why self binds instance state, and how magic dunder methods make your objects feel like native Python types.

  • See how classes stamp out independent objects
  • Demystify what 'self' actually does behind the scenes
  • Harness dunder methods (__str__, __len__, __eq__)
  • Avoid the infamous mutable class attribute trap

1The Blueprint & Cookie Cutter mental model

Think of a Class as an architectural blueprint or a cookie cutter. The blueprint describes what an account has (data) and what it can do (methods), but the blueprint itself does not hold money. An Instance (Object) is the actual house built from that blueprint.

Class (The Blueprint)

Written once with class BankAccount:. Specifies attributes and methods.

Instance (The Live Object)

Created as many times as you need: asha = BankAccount("Asha", 500).

When you write asha.deposit(100), Python secretly translates it to BankAccount.deposit(asha, 100). That's all self is: the specific instance passed into the function!

2Play with the blueprint factory

Instantiate accounts and deposit or withdraw funds. Watch how each instance protects its own private self.balance without interfering with others:

Class Blueprint ➔ Independent Instances

The Blueprint (Class Definition)

class BankAccount:
    bank_name = "Apex Bank"    # Class variable (shared)

    def __init__(self, owner, balance=0):
        self.owner = owner     # Instance variable (unique)
        self.balance = balance

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

    def withdraw(self, amount):
        if self.balance >= amount:
            self.balance -= amount
            return True
        return False

Live Instances in Memory (2 active objects)

Ashaid: acc_1
$500

self.owner = "Asha"

TARGET INSTANCE ("self")
Raviid: acc_2
$1200

self.owner = "Ravi"

Call Method on Asha (this instance is "self"):

Click an action below to call an instance method.

What does this code do?

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

d1 = Dog('Buddy')
d2 = Dog('Rocky')

3Class variables vs Instance variables

Where you define a variable matters tremendously. A variable declared directly inside the class is shared by all instances. A variable attached to self inside __init__ belongs solely to that instance:

Modify Class Variable:·

Class Level: BankAccount

BankAccount.interest_rate = 5%

Shared across ALL instances unless an instance sets its own copy.

asha = BankAccount("Asha")asha.interest_rate = 5%

No instance override: falls back to BankAccount.interest_rate.

ravi = BankAccount("Ravi")ravi.interest_rate = 5%

Reads directly from class variable: updated automatically when BankAccount changes.

What is the output?

class Item:
    tax = 0.05

i1 = Item()
i2 = Item()
Item.tax = 0.10
print(i1.tax)

4Dunder (Magic) methods

Methods that start and end with double underscores (dunder) let your custom objects integrate seamlessly with Python's built-in operators and functions:

What triggers it:

print(item) or str(item)

Purpose:

Human-friendly readable summary (for customers / UI).

Sample Return Output:

Order #101 for Asha ($150.00)

Implementation in your class

def __str__(self):
    return f"Order #{self.id} for {self.customer} ($ {self.total})"

What is the difference between __str__ and __repr__?

5The 4 classic OOP traps

Trap 1 — The Mutable Class Attribute Bug. If you define a list or dict directly in the class body, every single instance shares the exact same collection!

# ❌ THE TRAP: items is a CLASS variable!
class Store:
    items = []  # Shared across EVERY Store instance!

delhi = Store()
mumbai = Store()
Delhi Store
inventory: ["Shirt"]
Mumbai Store
inventory: ["Shoes"]

Trap 2 — Forgetting 'self' in method definitions. If you write def bark(): without self, calling dog.bark() crashes with TypeError: bark() takes 0 positional arguments but 1 was given.

Trap 3 — Missing parentheses on method calls. Writing print(account.get_balance) prints the method reference rather than calling it. Always add parentheses: account.get_balance().

Trap 4 — Fragile deep inheritance. Don't force everything into a subclass hierarchy. Look at composition below:

Composition: 'Has-A'

A Car HAS AN Engine. An Order HAS Items. Instead of inheriting from everything, build objects out of smaller parts.

class Order:
def __init__(self, customer, items):
self.customer = customer # Has customer
self.items = items # Has items

Software Engineering Principle

"Favor Composition over Inheritance": Deep inheritance trees (A extends B extends C extends D) become brittle and hard to refactor. Combining clean components gives you maximum flexibility with zero surprises.

6Modern Python: Clean Dataclasses

Writing def __init__(self, a, b, c): self.a = a ... repeatedly is tedious. Python's built-in @dataclass decorator generates __init__, __repr__, and __eq__ for you automatically:

from dataclasses import dataclass

@dataclass
class Customer:
    id: int
    name: str
    email: str
    active: bool = True

c = Customer(1, "Asha", "asha@example.com")
print(c)  # Customer(id=1, name='Asha', email='asha@example.com', active=True)

Explore and run full classes, dataclasses, and method pipelines in the Python Playground.

7Challenge: Object-Oriented Architect

Demonstrate your understanding of classes, self, dunder methods, and attribute scopes. Get all 5 correct to finish Chapter 6!

Classes & Objects Visualised · code lab

Edit the example, run it with Python, or step through the lines.

Python
Line-by-line view
1class Counter:
2 def __init__(self, start=0):
3 self.value = start
4 def add(self, amount):
5 self.value += amount
6
7first = Counter(2)
8first.add(3)
9print(first.value)

Names

Run the example to inspect local names.

Objects in memory

Memory snapshots appear as Python runs.

Python output

Run the code to see its output.

1. Why is 'self' passed as the first parameter to instance methods in Python?

2. Which dunder method is invoked when you pass an object into print(obj)?

3. Where should mutable collections (like lists or dicts) be initialized to avoid shared state bugs?

4. How does a child class call its parent's constructor method?

5. What does Python output when you type 'account.deposit' (without parentheses)?

🎯 0 / 5 correct

Run the full e-commerce OOP pipeline with @dataclass and custom dunders in the Python Playground (code preloaded).

8Recap

  • A class is the blueprint; an instance (object) is the live entity.
  • self is how an instance refers to its own private state inside methods.
  • Initialize lists and dicts inside __init__ on self to avoid the shared class attribute trap.
  • Use dunder methods (__str__, __len__, __eq__) to make your classes feel native.
  • Prefer Composition ("has-a") over deep Inheritance ("is-a") for resilient design.
  • Use @dataclass to eliminate boilerplate when building data containers.

Finish the challenge above to complete the chapter, or

Enjoying this free lesson? Your support helps us build more free animated chapters.

Support Us