Please use a larger screen

This presentation is designed for desktop or projector displays.

← Back to curriculum

Python Object Oriented Programming

Composition & Aggregation

Course 5

Owner Part A Part B Part C Objects composed of other objects

to navigate

Course Curriculum

Where we are in the journey

Quiz Time! (Lecture 02)

Which prefix makes an attribute private in Python?

A) No prefix

B) _single underscore

C) __double underscore

D) #hash

Double underscore triggers name mangling, making the attribute harder to access from outside

Recap: Lecture 02

Access Control

Public

Accessible from anywhere. No underscores.

self.engine_status = "Off"

Protected _single

Accessible within class and subclasses. Convention only.

self._fuel_level = 50

Private __double

Accessible within class only. Name mangling.

self.__owner = "John"

Quiz Time! (Lecture 02)

What is the main purpose of encapsulation?

A) Making code run faster

B) Hiding internal state and requiring interaction through methods

C) Allowing multiple inheritance

D) Creating abstract classes

Encapsulation bundles data with methods and restricts direct access — protecting the object's internal state from unintended changes

Recap: Lecture 02

Encapsulation in Practice

  • Bundle data and methods that operate on that data
  • Hide internal implementation details
  • Expose only what's needed via public interface
  • Use getters/setters or @property for controlled access
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance

    @property
    def balance(self):
        return self.__balance

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount

Quiz Time! (Lecture 03)

What does super().__init__() do in a child class?

A) Creates a new parent object

B) Calls the parent's constructor

C) Overrides the parent method

D) Deletes the parent class

super() gives access to the parent class — calling __init__() ensures the parent's setup runs before the child adds its own

Recap: Lecture 03

Inheritance: is-a Relationship

  • A child class inherits attributes and methods from a parent
  • Represents an is-a relationship: Dog is a Animal
  • Use super() to call parent methods
  • Child can override or extend parent behavior
class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

Quiz Time! (Lecture 03)

Which type of inheritance involves a child with multiple parents?

A) Single inheritance

B) Hierarchical inheritance

C) Multiple inheritance

D) Multilevel inheritance

Multiple inheritance = one child inherits from two or more parents. Python resolves method conflicts via MRO (Method Resolution Order)

Recap: Lecture 03

Types of Inheritance

Single

One parent → one child

class Dog(Animal):
    pass

Multiple

Multiple parents → one child

class C(A, B):
    pass

Multilevel

Grandparent → parent → child

class Puppy(Dog):
    pass

Quiz Time! (Lecture 04)

What happens when a child class defines a method with the same name as the parent?

A) Both methods run

B) The child's method overrides the parent's

C) A syntax error occurs

D) The parent's method always wins

This is method overriding — the child provides its own implementation, which takes precedence when called on the child instance

Recap: Lecture 04

Polymorphism: Many Forms

  • Same interface, different implementations
  • Method overriding lets each subclass behave differently
  • Enables writing code that works with any subclass
for shape in [Circle(5), Square(4)]:
    print(shape.area())
# Each shape calculates its own area
.area() πr² Same method, different behavior

Quiz Time! (Lecture 04)

What is the purpose of an abstract class?

A) To create objects directly

B) To store data in a database

C) To define a contract that subclasses must implement

D) To prevent inheritance

Abstract classes define methods that subclasses must implement — you cannot instantiate an abstract class directly

Recap: Lecture 04

Abstract Base Classes

  • Use ABC and @abstractmethod
  • Cannot be instantiated directly
  • Force subclasses to implement required methods
  • Perfect for defining a common interface
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def area(self):
        return 3.14 * self.r ** 2
05

OOP So Far

Four pillars we've covered

🔐 Encapsulation

Bundling data + methods, hiding internal state

👪 Inheritance

Child classes reuse and extend parent behavior

🎭 Polymorphism

Same interface, different implementations

👁 Abstraction

Exposing only essential features, hiding complexity

Today we explore how objects relate to each other — beyond inheritance

Object Relationships

Two fundamental types

is-a Inheritance

A subclass is a type of its parent

  • 🐶 Dog is a Animal
  • 🚗 Car is a Vehicle
  • 👤 Manager is a Employee

has-a Association

An object has a reference to another object

  • 🚗 Car has a Engine
  • 📚 Library has Books
  • ⚽ Team has Players
Today's focus: the has-a side — composition, aggregation, and dependency

Part 1

Composition

Strong "has-a" — parts cannot exist without the whole

What Is Composition?

  • A strong has-a relationship
  • The owner creates and manages the parts
  • Parts are exclusive — they belong to only one owner
  • When the owner is destroyed, all parts are destroyed too
  • Lifecycle is tightly coupled
Think: a house and its rooms. Demolish the house → rooms are gone
🏠 House 🍳 Kitchen 🛌 Bedroom 🚽 Bathroom Owner destroyed → all parts destroyed

Real-World Composition Examples

🫀 Body & Heart

A heart cannot exist outside the body. The body owns the heart.

🚗 Car & Engine

Engine is built for this car. Car is destroyed → engine goes with it.

🏨 Hotel & Rooms

Rooms don't exist without the hotel. Exclusive ownership.

Key pattern: the owner creates parts internally — they are never shared with other objects

Composition in Python

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        return "Engine running"


class Car:
    def __init__(self, model):
        self.model = model
        self.engine = Engine(200)

    def drive(self):
        return self.engine.start()
  • Car creates the Engine inside __init__
  • Engine is not passed in — it's created internally
  • If the Car object is deleted, the Engine goes with it
  • No other object references this engine
# Usage
my_car = Car("Toyota")
print(my_car.drive())
# Engine running

Composition: Multiple Parts

class Wheel:
    def __init__(self, position):
        self.position = position

class Transmission:
    def __init__(self, type_):
        self.type = type_

class Car:
    def __init__(self, model):
        self.model = model
        self.engine = Engine(200)
        self.transmission = Transmission("automatic")
        self.wheels = [Wheel(pos) for pos in ["FL", "FR", "RL", "RR"]]
All parts are created inside the Car constructor — they live and die together

Part 2

Aggregation

Weak "has-a" — parts can exist independently

What Is Aggregation?

  • A weak has-a relationship
  • Objects are passed in from outside, not created internally
  • Parts can be shared across multiple owners
  • Parts survive after the owner is destroyed
  • Lifecycle is independent
Think: a team and its players. Dissolve the team → players still exist
Team 🧍 Player 1 🧍 Player 2 🧍 Player 3 Owner destroyed → parts survive

Real-World Aggregation Examples

✈️ Airline & Airplanes

Airline goes bankrupt → planes are sold to another airline. Planes survive.

📚 Library & Books

Close the library → books are donated. Books exist independently.

🎓 University & Students

Students can belong to multiple organizations. Not exclusive.

Aggregation in Python

class Book:
    def __init__(self, title):
        self.title = title


class Library:
    def __init__(self, name):
        self.name = name
        self.books = []

    def add_book(self, book):
        self.books.append(book)
  • Books are created outside the Library
  • They're passed in via add_book()
  • Deleting the library doesn't destroy the books
# Usage
b1 = Book("Python 101")
b2 = Book("Clean Code")

lib = Library("City Library")
lib.add_book(b1)
lib.add_book(b2)

del lib
print(b1.title)  # Python 101 still exists!

Composition vs Aggregation

Composition Aggregation
Relationship Strong "has-a" Weak "has-a"
Ownership Exclusive Shared
Lifecycle Parts die with owner Parts live independently
Creation Created inside owner Passed in from outside
Coupling Tight Loose
Example House & Rooms Team & Players

Side by Side in Code

Composition

class House:
    def __init__(self):
        # Created INSIDE
        self.rooms = [
            Room("kitchen"),
            Room("bedroom"),
            Room("bathroom"),
        ]

house = House()
del house
# rooms are gone too

Aggregation

class Library:
    def __init__(self):
        self.books = []

    def add_book(self, book):
        # Passed in from OUTSIDE
        self.books.append(book)

book = Book("Python 101")
lib = Library()
lib.add_book(book)
del lib
print(book.title)  # still exists!

Part 3

Coupling

How tightly objects depend on each other

Tight vs Loose Coupling

🔧 Tight Coupling

  • Classes depend directly on each other's internals
  • Changing one class forces changes in others
  • Hard to test in isolation
  • Difficult to reuse

🔌 Loose Coupling

  • Classes interact through interfaces
  • Changes are localized
  • Easy to test and mock
  • Highly reusable
Goal: aim for loose coupling wherever possible — it makes your code maintainable, flexible, and testable

Tight Coupling Example

class MySQLDatabase:
    def query(self, sql):
        return f"MySQL: {sql}"


class UserService:
    def __init__(self):
        self.db = MySQLDatabase()

    def get_user(self, id):
        return self.db.query(
            f"SELECT * FROM users WHERE id={id}"
        )
  • UserService creates MySQLDatabase internally
  • Want to switch to PostgreSQL? Must rewrite UserService
  • Can't test without a real MySQL instance

Loose Coupling Example

class UserService:
    def __init__(self, db):
        self.db = db

    def get_user(self, id):
        return self.db.query(
            f"SELECT * FROM users WHERE id={id}"
        )

# Easy to swap implementations
svc = UserService(MySQLDatabase())
svc = UserService(PostgresDatabase())
svc = UserService(MockDatabase())
  • Database is passed in, not created inside
  • Easy to swap implementations
  • Easy to test with mocks
  • This is aggregation at work!

has-a Relationship Spectrum

From strong ownership to temporary use

Composition

Strong ownership
Parts die with owner

Aggregation

Weak ownership
Parts live independently

Dependency

Temporary use
No ownership at all

← Strongest coupling Weakest coupling →

Part 4

Composition over Inheritance

Prefer composing objects over deep class hierarchies

Composition over Inheritance

A principle suggesting classes should achieve polymorphism through composition rather than inheritance

  • Instead of inheriting properties from a parent class...
  • ...a class should be composed of other classes that provide desired behavior
  • More flexible — can change behavior at runtime
  • Avoids fragile base class problem
Inheritance: Manager is-a Employee
Composition: Employee has-a Role

Misuse of Inheritance

class Employee:
    def __init__(self, name):
        self.name = name

class Manager(Employee):
    def generate_report(self):
        return "Manager report"

class Technician(Employee):
    def repair_equipment(self):
        return "Equipment repaired"
  • Adding new roles = new subclasses → bloated hierarchy
  • Changing an employee's role means creating a new instance
  • Shared behaviors across roles = duplication or more complex inheritance

Correcting with Composition

Employee has a Role — roles are pluggable objects

class ManagerRole:
    def perform_duty(self):
        return "Manager report"

class TechnicianRole:
    def perform_duty(self):
        return "Equipment repaired"

class Employee:
    def __init__(self, name, role=None):
        self.name = name
        self.role = role

    def perform_duty(self):
        if self.role:
            return self.role.perform_duty()
        return "No specific duty"
# Create roles independently
mgr_role = ManagerRole()
tech_role = TechnicianRole()

# Assign to employees
alice = Employee("Alice", mgr_role)
bob = Employee("Bob", tech_role)

print(alice.perform_duty())
# Manager report

print(bob.perform_duty())
# Equipment repaired

# Change role at runtime!
alice.role = tech_role
print(alice.perform_duty())
# Equipment repaired

Benefits of Composition over Inheritance

⚡ Flexibility

Change behavior at runtime by swapping components

📦 No Hierarchy Bloat

New behaviors = new components, not new subclasses

🧪 Testability

Test each component independently

🔁 Reusability

Components can be shared across different classes

Rule of thumb: Use inheritance for genuine "is-a" relationships. Use composition for "has-a" or when you need flexibility.

Part 5

Dependency

When one class uses methods of another temporarily

What Is a Dependency?

  • Class A uses methods of Class B
  • A temporary relationship — no ownership
  • B is not stored as an attribute of A
  • Typically passed as a method parameter
  • The weakest form of coupling
Class A Class B uses A uses B's methods, but doesn't own B

Dependency in Python

class Printer:
    def print_document(self, text):
        print(f"Printing: {text}")


class Report:
    def __init__(self, content):
        self.content = content

    def send_to_printer(self, printer):
        printer.print_document(self.content)
  • Report doesn't own the Printer
  • Printer is only used temporarily in the method call
  • After the method returns, the relationship ends
# Usage
printer = Printer()
report = Report("Q1 Results")
report.send_to_printer(printer)
# Report has no reference to printer

Part 6

Dependency Injection

Providing dependencies from outside instead of creating them inside

Think: Amazon Delivery

You order from Amazon. How does it get to you?

📦
🛒

Order

📦
🏢

Warehouse

📦
✈️ / 🚢 / 🚚

Transport

📦
🏠

You

Amazon doesn't build its own planes and trucks — it injects different delivery services depending on distance, cost, speed

What Is Dependency Injection?

  • A technique where dependencies are provided from outside
  • Instead of a class creating its own dependencies...
  • ...they are injected via constructor or method parameters
  • Promotes loose coupling
  • Makes code testable and swappable
Service Dep A Dep B Dep C Dependencies injected from outside

Without DI (Tight Coupling)

class OrderService:
    def __init__(self):
        self.shipper = Omniva()  # hardcoded!

    def ship_order(self, order):
        self.shipper.deliver(order)

Problems

  • Locked to Omniva — can't easily switch
  • Must modify class to use DHL or LP Express
  • Hard to test without real Omniva API

What if...

...we could inject any shipping provider? The OrderService shouldn't care which carrier delivers.

With DI (Loose Coupling)

class Omniva:
    def deliver(self, order):
        return f"Omniva ships {order}"

class LPExpress:
    def deliver(self, order):
        return f"LP Express ships {order}"

class OrderService:
    def __init__(self, shipper):
        self.shipper = shipper

    def ship_order(self, order):
        return self.shipper.deliver(order)
# Inject Omniva
svc = OrderService(Omniva())
print(svc.ship_order("Laptop"))
# Omniva ships Laptop

# Inject LP Express instead
svc = OrderService(LPExpress())
print(svc.ship_order("Laptop"))
# LP Express ships Laptop

# Inject a mock for testing
svc = OrderService(MockShipper())
print(svc.ship_order("Laptop"))
# MockShipper ships Laptop

Three Types of Injection

🚧 Constructor Injection

Passed via __init__. Most common.

class App:
    def __init__(self, db):
        self.db = db

🔨 Setter Injection

Assigned after creation via a method.

class App:
    def set_db(self, db):
        self.db = db

📨 Method Injection

Passed per method call. Temporary.

class App:
    def query(self, db):
        db.execute()

DI with Abstract Base Class

Combine DI with ABC for a proper contract

from abc import ABC, abstractmethod

class Shipper(ABC):
    @abstractmethod
    def deliver(self, order):
        pass

class Omniva(Shipper):
    def deliver(self, order):
        return f"Omniva: {order}"

class DHL(Shipper):
    def deliver(self, order):
        return f"DHL: {order}"
class OrderService:
    def __init__(self, shipper: Shipper):
        self.shipper = shipper

    def ship(self, order):
        return self.shipper.deliver(order)
  • Shipper ABC defines the contract
  • Any class implementing deliver() can be injected
  • Combines polymorphism + DI for maximum flexibility

All Relationship Types Summary

Type Strength Lifecycle Example
Inheritance is-a Bound to parent type Dog is-a Animal
Composition Strong has-a Parts die with owner House has Rooms
Aggregation Weak has-a Parts survive Team has Players
Dependency Uses Temporary Report uses Printer
Choose the weakest relationship that satisfies your requirements — this maximizes flexibility

Quiz Time! (Lecture 05)

A House creates its Room objects inside __init__. When the house is deleted, the rooms are gone. What is this?

A) Composition

B) Aggregation

C) Dependency

D) Inheritance

Composition — parts are created inside the owner and share its lifecycle. Filled diamond ♦️ in UML.

Quiz Time! (Lecture 05)

What relationship does this code show?

class Team:
    def __init__(self, name):
        self.name = name
        self.members = []

    def add_member(self, person):
        self.members.append(person)

A) Composition

B) Aggregation

C) Dependency

D) Inheritance

Aggregationperson is created outside and passed in. Members survive if the team is deleted.

Quiz Time! (Lecture 05)

Which snippet shows tight coupling?

A)

class App:
    def __init__(self):
        self.db = MySQL()

B)

class App:
    def __init__(self, db):
        self.db = db

A) is tight coupling — the class creates its own dependency. B) injects it from outside, making it easy to swap and test.

Quiz Time! (Lecture 05)

What relationship does this code show?

class Report:
    def export(self, formatter):
        return formatter.format(self.data)

A) Composition

B) Aggregation

C) Dependency

D) Inheritance

Dependencyformatter is passed as a method parameter, used temporarily, and not stored. The weakest form of coupling.

⚠️ Common Mistake: It Depends on Context

Is Car → Engine composition or aggregation?

♦️ Composition

Car factory builds the engine inside the car. Engine is exclusive to this car.

class Car:
    def __init__(self):
        self.engine = Engine(200)

◊ Aggregation

Engine is sourced from a supplier and installed. Can be swapped out.

class Car:
    def __init__(self, engine):
        self.engine = engine
The relationship type depends on who creates and owns the part — not the domain objects themselves

🧮 When to Use What?

Does the owner create and destroy the part?

Composition — House creates Rooms

Does the part exist independently and get passed in?

Aggregation — Library receives Books

Is it only used temporarily in a method call?

Dependency — Report uses Printer

When in doubt, choose the weakest relationship that works — it keeps your code flexible

Live Coding

Football World

Composition + Aggregation side by side

♦️ Composition

Stadium creates its Pitch & Seats

Delete stadium → pitch is gone

◊ Aggregation

Teams have Players passed in

Messi plays for club + country

Key Takeaways

Next up: Exception Handling & Logging