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()) # FalseTypo rss → AttributeError: '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 0Key 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