Layer 2 Session 16 Object-Oriented Basics

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.

AppNavMenuExplorera value threaded through layers that do not use it

One object holding โ€” and delegating to โ€” several others.

Estimated time: 35โ€“40 minutes  ยท  Pre-quiz โ†’ Concept โ†’ Lab โ†’ Commit โ†’ Post-quiz

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.

Question 1 of 5

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?

This is composition: one object holds references to other objects as part of its own data. CountryExplorer is not a kind of Country, and does not inherit from it โ€” it simply has some.
Question 2 of 5

Given the CountryExplorer above with a method def summaries(self): return [c.summary() for c in self.countries], what does this method do?

This is delegation: the outer object does not know how to summarize a country itself โ€” it delegates that job to each Country instance's own summary() method (from Session 14), and just collects the results.
Question 3 of 5

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.
Question 4 of 5

If explorer.countries contains 3 Country instances and you call explorer.summaries(), how many times does each individual Country's summary() method run?

The list comprehension inside 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.
Question 5 of 5

Why is composition (CountryExplorer HAS a list of countries) generally preferred over cramming all country data as loose attributes directly onto one giant class?

Composition lets each class stay small and focused โ€” Country knows how to be a country, CountryExplorer knows how to manage a collection of them. This mirrors exactly why we split code into modules in Session 10.

3. The Concept โ€” Composition

COUNTRYEXPLORER.countries โ†’ [ ]HOLDSCountry, Country,Country

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

Lab objective: Build a CountryExplorer class that composes a list of Country instances and offers collection-level operations by delegating to each one.

What you will build

A file called explorer_composed.py.

Step-by-step instructions

1

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
2

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))
3

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)
4

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")])
5

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

FileActionWhy
explorer_composed.py Created CountryExplorer composes and delegates to a list of Country instances.
docs/sessions/session-16/index.html Created This session document.
If you find yourself editing any other file, stop. This session touches exactly 2 files.

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"
Do not commit until you can answer out loud: "Why does total_population() automatically reflect a country added after construction, without any changes to total_population itself?"

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.

Question 1 of 5

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?

This is composition: one object holds references to other objects as part of its own data. CountryExplorer is not a kind of Country, and does not inherit from it โ€” it simply has some.
Question 2 of 5

Given the CountryExplorer above with a method def summaries(self): return [c.summary() for c in self.countries], what does this method do?

This is delegation: the outer object does not know how to summarize a country itself โ€” it delegates that job to each Country instance's own summary() method (from Session 14), and just collects the results.
Question 3 of 5

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.
Question 4 of 5

If explorer.countries contains 3 Country instances and you call explorer.summaries(), how many times does each individual Country's summary() method run?

The list comprehension inside 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.
Question 5 of 5

Why is composition (CountryExplorer HAS a list of countries) generally preferred over cramming all country data as loose attributes directly onto one giant class?

Composition lets each class stay small and focused โ€” Country knows how to be a country, CountryExplorer knows how to manage a collection of them. This mirrors exactly why we split code into modules in Session 10.

9. Reflection Questions

Think through these after the post-quiz. No right answer โ€” they are for discussion.

  1. 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.
  2. What would go wrong if CountryExplorer tried to reimplement summary formatting itself instead of calling c.summary()?
  3. 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.)
  4. 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.