Composition
Build an object out of other objects, each responsible for its own part, and let method calls delegate instead of duplicating logic.
By the end of this lesson, you can
- Explain composition as building an object out of other objects, each responsible for its own part
- Trace how a method call on a composed object delegates work to the object it contains
- Explain why composition models a "has-a" relationship more naturally than duplicating logic would
Why it matters
A library member has an account for tracking fines — that’s a
relationship between two separate things, not one thing wearing two
hats. Reimplementing balance-tracking logic directly inside a Member
class would duplicate everything Account already does, including
every rule it enforces. Composition avoids that by having Member
hold an Account, and delegate to it.
Mental model
Composition means an object holds another object as one of its attributes, and calls that object’s methods to get work done — rather than reimplementing that behavior itself. It models a “has-a” relationship directly: a member has an account.
class Account:
def __init__(self, starting_balance):
self._balance = starting_balance
def withdraw(self, amount):
if amount > self._balance:
return False
self._balance = self._balance - amount
return True
def balance(self):
return self._balance
class Member:
def __init__(self, name, starting_balance):
self.name = name
self.account = Account(starting_balance)
def pay_fine(self, amount):
return self.account.withdraw(amount)
member = Member("Ada", 20)
print(member.pay_fine(5))
print(member.account.balance())True
15Member.__init__ creates and holds an Account as self.account.
pay_fine doesn’t touch any balance logic itself — it calls
self.account.withdraw(amount) and returns whatever that reports.
Member doesn’t need to know how Account enforces its rules, only
that it does.
Trace it
| Call | Delegates to | account.balance() afterward |
|---|---|---|
| Member("Ada", 20) | Account(20) is created and held as self.account | 20 |
| member.pay_fine(5) | self.account.withdraw(5) | 15 |
| member.pay_fine(100) | self.account.withdraw(100) — refused, exceeds balance | 15 (unchanged) |
Every rule Account.withdraw enforces — refusing to overdraw — applies
automatically to every Member, without Member ever mentioning that
rule itself.
Check your understanding
Or reveal the answer without checking
Answer:True, True, 4
Each pay_fine(3) delegates to account.withdraw(3), which succeeds both times (10 -> 7 -> 4), since 3 never exceeds the current balance at either point.
Or reveal the answer without checking
Answer:Every rule Account enforces applies automatically through delegation, and only needs to be maintained in one place
Delegating to a composed Account means Member automatically benefits from every rule Account enforces, now and in the future, without duplicating any of that logic itself.
Practice: warm-up
Trace this program — write down member.account.balance() after each
call.
member = Member("Linus", 50)
member.pay_fine(20)
member.pay_fine(20)
member.pay_fine(20)
Stuck? Reveal one hint at a time.
Hint 1
Each pay_fine call delegates to account.withdraw with the current balance at that point — not the original starting balance.
Hint 2
The third call needs to be checked against whatever the balance actually is by then, not against 50.
Reveal the trace
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
Member("Linus", 50) -> balance = 50
pay_fine(20) -> 20 <= 50, allowed -> balance = 30
pay_fine(20) -> 20 <= 30, allowed -> balance = 10
pay_fine(20) -> 20 <= 10? No -> refused, balance stays 10Practice: apply it
This version of Member doesn’t compose Account — it reimplements
balance tracking directly instead:
class Member:
def __init__(self, name, starting_balance):
self.name = name
self.balance = starting_balance
def pay_fine(self, amount):
if amount > self.balance:
return False
self.balance = self.balance - amount
return True
Or reveal the answer without checking
Answer:It duplicates Account's balance logic entirely — any future fix or rule added to Account (like a transaction log) never applies here, since this code never uses Account at all
This Member reimplements exactly what Account already does. Today it behaves the same, but the two implementations can now drift apart — a fix made to Account's withdraw() logic has no effect on this duplicated version.
Modification challenge: rewrite this version of Member to compose
an Account instead — store self.account = Account(starting_balance)
and have pay_fine delegate to self.account.withdraw(amount).
Summary
- Composition means an object holds another object as an attribute and delegates to it, rather than reimplementing its behavior.
- It models a “has-a” relationship directly: a member has an account, rather than being one.
- Every rule the composed object enforces applies automatically through delegation, without the containing object needing to know how.
- Copying another class’s logic instead of composing an instance of it duplicates every rule that logic enforces — and lets the two versions drift apart over time.