Composition โ Objects Containing Objects
Real applications are built from many small, focused classes working together, not one giant class doing everything. We build a CountryExplorer that holds a list of Country objects.
One object holding โ and delegating to โ several others.
1. Learning Objective
By the end of this session you will be able to:
- Store a list of class instances as an attribute of another class
- Write a method on the outer class that loops over and delegates to the inner instances' own methods
- Explain the difference between composition (has-a) and the inheritance relationship we have not covered yet
- Add an instance to a composed collection after construction
- Trace a method call from the outer object down into an inner object's own method
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
Given class CountryExplorer:\n def __init__(self, countries):\n self.countries = countries where countries is a list of Country instances, what kind of relationship is this?
Given the CountryExplorer above with a method def summaries(self): return [c.summary() for c in self.countries], what does this method do?
summary() method (from Session 14), and just collects the results.How do you add a new Country to an existing CountryExplorer instance's collection after construction?
self.countries is just a regular list attribute โ Session 06's .append() works on it exactly the same way it worked on any other list.If explorer.countries contains 3 Country instances and you call explorer.summaries(), how many times does each individual Country's summary() method run?
summaries() iterates the 3 instances and calls .summary() on each one individually, exactly like the for-loop pattern from Session 06, just now calling a method instead of reading a dict key.Why is composition (CountryExplorer HAS a list of countries) generally preferred over cramming all country data as loose attributes directly onto one giant class?
3. The Concept โ Composition
CountryExplorer HAS a list of Country instances โ it does not become one, it holds references to several.
One class holding instances of another
Composition means an object's attribute is itself another object (or a collection of them) โ a "has-a" relationship, as opposed to the "is-a" relationship of inheritance, which we are deliberately not covering yet.
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
def summary(self):
return f"{self.name} ({self.region}): pop. {self.population:,}"
class CountryExplorer:
def __init__(self, countries):
self.countries = countries # a list of Country instances
Delegating to inner objects
The outer class does not need to know HOW to summarize a country โ it simply asks each Country instance to summarize itself, using the method that instance already has.
explorer = CountryExplorer(countries=[
Country(name="Kenya", region="Africa", population=54000000),
Country(name="Peru", region="Americas", population=33000000),
])
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
def summaries(self):
return [c.summary() for c in self.countries] # delegates to each Country
print(explorer.summaries())
# ['Kenya (Africa): pop. 54,000,000', 'Peru (Americas): pop. 33,000,000']
Growing the collection after construction
Since self.countries is just a list, everything from Session 06 still applies to it.
nigeria = Country(name="Nigeria", region="Africa", population=223000000)
explorer.countries.append(nigeria)
print(len(explorer.countries)) # one more than before
A method that operates across the whole collection
This is where composition starts to pay off โ the outer class can offer operations that make sense at the collection level, built from what each inner object already knows.
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
def summaries(self):
return [c.summary() for c in self.countries]
def total_population(self):
return sum(c.population for c in self.countries)
def find_by_region(self, region):
return [c for c in self.countries if c.region == region]
4. Lab
What you will build
A file called explorer_composed.py.
Step-by-step instructions
Create the file with Country and an empty CountryExplorer
# explorer_composed.py
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
def summary(self):
return f"{self.name} ({self.region}): pop. {self.population:,}"
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
Construct an explorer with 3 countries
explorer = CountryExplorer(countries=[
Country(name="Kenya", region="Africa", population=54000000),
Country(name="Ghana", region="Africa", population=31000000),
Country(name="Peru", region="Americas", population=33000000),
])
print(len(explorer.countries))
Add a summaries() method that delegates to each Country
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
def summaries(self):
return [c.summary() for c in self.countries]
explorer = CountryExplorer(countries=[
Country(name="Kenya", region="Africa", population=54000000),
Country(name="Peru", region="Americas", population=33000000),
])
for line in explorer.summaries():
print(line)
Add total_population() and find_by_region() methods
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
def summaries(self):
return [c.summary() for c in self.countries]
def total_population(self):
return sum(c.population for c in self.countries)
def find_by_region(self, region):
return [c for c in self.countries if c.region == region]
explorer = CountryExplorer(countries=[
Country(name="Kenya", region="Africa", population=54000000),
Country(name="Ghana", region="Africa", population=31000000),
Country(name="Peru", region="Americas", population=33000000),
])
print(explorer.total_population())
print([c.name for c in explorer.find_by_region("Africa")])
Add a country after construction and confirm total_population updates
explorer.countries.append(Country(name="Nigeria", region="Africa", population=223000000))
print(explorer.total_population()) # reflects the new total automatically
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
explorer_composed.py |
Created | CountryExplorer composes and delegates to a list of Country instances. |
docs/sessions/session-16/index.html |
Created | This session document. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add explorer_composed.py docs/sessions/session-16/index.html
git commit -m "session-16: compose CountryExplorer from a list of Country instances"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
Given class CountryExplorer:\n def __init__(self, countries):\n self.countries = countries where countries is a list of Country instances, what kind of relationship is this?
Given the CountryExplorer above with a method def summaries(self): return [c.summary() for c in self.countries], what does this method do?
summary() method (from Session 14), and just collects the results.How do you add a new Country to an existing CountryExplorer instance's collection after construction?
self.countries is just a regular list attribute โ Session 06's .append() works on it exactly the same way it worked on any other list.If explorer.countries contains 3 Country instances and you call explorer.summaries(), how many times does each individual Country's summary() method run?
summaries() iterates the 3 instances and calls .summary() on each one individually, exactly like the for-loop pattern from Session 06, just now calling a method instead of reading a dict key.Why is composition (CountryExplorer HAS a list of countries) generally preferred over cramming all country data as loose attributes directly onto one giant class?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why does total_population() automatically pick up the newly appended country without any code change to total_population itself? Trace through what self.countries actually contains at call time.
- What would go wrong if CountryExplorer tried to reimplement summary formatting itself instead of calling c.summary()?
- Can you think of a real application where "one object holding a collection of other objects" is the natural shape? (Hint: think of any list-based UI you have used.)
- How is CountryExplorer.countries similar to and different from the plain list of dictionaries we used in Session 06?
10. What Breaks If This Knowledge Is Missing?
- Duplicated logic: If CountryExplorer reimplemented formatting or validation instead of delegating to Country's own methods, a bug fix would need to happen in two places โ and they would inevitably drift out of sync over time.
- Conditional rendering / logic (Session 17): The next session adds conditional logic inside methods, such as handling an empty collection. Composition is what makes "no countries yet" a meaningful, testable state to handle.
- The whole rest of the project: Every remaining layer โ mock data, testing, architecture, real APIs โ operates on a composed structure just like CountryExplorer. This is the shape the entire application is built from.
11. What We Learned
Python concept mastered: Composition โ one class holding instances of another, and delegating collection-level work to each inner instance's own methods.
Unlocks: You can now build a real application out of small, focused, cooperating classes instead of one giant class doing everything.
Next session: Session 17 โ Conditional Logic in Methods. We handle the cases where things are missing or empty โ conditional logic inside methods, like an empty country list or a country with no known capital.