← Home

081. Classes

Bundle data and behavior together with classes

081. Classes

He has been passing process dicts everywhere: {"name": "chrome", "pid": 1234, "rss_mb": 420, "cpu_percent": 12.5}. Every function takes the same four keys. He keeps typo-ing "rss_mb" as "rss" and getting None.

He creates a Process class instead:

class Process:
    def __init__(self, name, pid, rss_mb, cpu_percent):
        self.name = name
        self.pid  = pid
        self.rss_mb = rss_mb
        self.cpu_percent = cpu_percent

    def is_heavy(self, threshold_mb=500):
        return self.rss_mb > threshold_mb

    def summary(self):
        return f"{self.name}[{self.pid}]: {self.rss_mb} MB"

chrome = Process("chrome", 1234, 420, 12.5)
print(chrome.summary())       # chrome[1234]: 420 MB
print(chrome.is_heavy())      # False

Typo rssAttributeError: 'Process' object has no attribute 'rss' — caught immediately, not silently returning None.


💡 Fun fact: Object-oriented programming with classes was popularized by Smalltalk in the 1970s and heavily influenced Python’s design. Guido van Rossum has said that Python’s class model is deliberately simpler than C++ or Java — everything is public by default, there’s no compiler-enforced private, and self is explicit rather than implicit, making it clear when you’re accessing instance state.

⚠️ Watch out: The most universal beginner mistake with Python classes is forgetting self as the first parameter of a method — writing def __init__(owner, balance=0) instead of def __init__(self, owner, balance=0). Python raises a confusing TypeError: __init__() takes 2 positional arguments but 3 were given that doesn’t mention self at all.

🤔 Think about it: Python doesn’t enforce private attributes — you can access account._balance from outside the class just fine. What are the practical consequences of this design choice compared to languages like Java where private is compiler-enforced?

Learning objectives

  • Define a class with init and instance methods
  • Use self to access and modify instance attributes
  • Implement str for readable object representation

Key concepts

  • class
  • instance
  • self
  • init
  • instance methods

Try it

Concept detail

class defines a blueprint. An object is one instance of that blueprint.

class BankAccount:
    def __init__(self, owner, balance=0):  # constructor
        self.owner = owner      # instance attribute
        self.balance = balance

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

# Create instances
alice = BankAccount("Alice", 100)
bob   = BankAccount("Bob", 0)

# Each instance has its own state
alice.deposit(50)   # alice.balance = 150
bob.balance         # still 0

Key rules:

  • self is always the first parameter of every method
  • self.attribute creates/accesses an attribute on this specific instance
  • “balance += amount” without self refers to a local variable (doesn’t exist)

WHY classes instead of dicts:

  • Typo “rss” → AttributeError (caught); dict typo → None (silent)
  • Methods live with data — no passing dicts around
  • isinstance() checks, inheritance, str / repr for readable output

str vs repr:

  • str → user-facing: print(acc) calls this
  • repr → developer-facing: repr(acc), REPL display
  • If only repr defined, Python uses it for str too

Solution

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

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

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError(f"Insufficient funds: {amount} > {self.balance}")
        self.balance -= amount
        return self.balance

    def __str__(self):
        return f"BankAccount({self.owner}, balance={self.balance})"

Tests

def test_initial_balance():
    acc = BankAccount("Alice", 100)
    assert acc.balance == 100
    assert acc.owner == "Alice"

def test_deposit():
    acc = BankAccount("Bob", 50)
    result = acc.deposit(25)
    assert result == 75
    assert acc.balance == 75

def test_deposit_accumulates():
    acc = BankAccount("Carol", 0)
    acc.deposit(100)
    acc.deposit(50)
    assert acc.balance == 150

def test_withdraw_valid():
    acc = BankAccount("Dave", 200)
    result = acc.withdraw(50)
    assert result == 150
    assert acc.balance == 150

def test_withdraw_insufficient():
    acc = BankAccount("Eve", 10)
    with pytest.raises(ValueError):
        acc.withdraw(100)

def test_str():
    acc = BankAccount("Frank", 500)
    s = str(acc)
    assert "Frank" in s
    assert "500" in s

Resources