Understanding Composition and Aggregation in Python
Course: Python Training
Topic: Understanding Composition and Aggregation
Course Link: Python Training Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction to Composition and Aggregation
Composition and aggregation are important concepts in object-oriented programming (OOP). They describe how objects of different classes are connected to build larger and more organized applications. Both represent a has-a relationship, in which one object contains a reference to another object.
For example, a car has an engine, a department has employees, a library has books, and a playlist has songs. These relationships help developers divide a complex system into smaller, reusable components.
The primary difference is ownership and lifecycle dependency. In composition, a component is treated as a strongly owned part of a whole. In aggregation, the related object can exist independently of the object that references it.
Learning Objectives
- Understand the meaning of composition and aggregation.
- Learn how to implement both relationships using Python classes and objects.
- Understand object ownership and lifecycle dependency.
- Differentiate composition, aggregation, inheritance, and association.
- Learn how constructors and object references establish relationships.
- Apply these concepts to real-world programming scenarios.
- Identify common mistakes and follow object-oriented design best practices.
2. What Is Composition in Python?
Composition is an object-oriented design relationship in which one object owns or manages other objects as its components. The component is conceptually part of the larger object, and its lifecycle is closely connected to that object's lifecycle.
For example, a car contains an engine as one of its components. In a simple software model, the car can create and manage its own engine object.
Key Characteristics of Composition
- Strong ownership: The containing object is responsible for managing its component.
- Lifecycle dependency: The component is conceptually tied to the whole object.
- Object containment: A component is commonly stored as an instance attribute.
- Code organization: Each class handles a specific responsibility.
- Reusability: Components can be designed as separate classes with their own methods.
In Python, composition is implemented by creating an object of one class inside another class or storing a component object as an attribute. Python does not automatically destroy an object simply because its containing object is deleted; the actual lifecycle depends on references and program design.
3. Basic Example of Composition
Let's create an Engine class and a Car class. The car creates its own engine and uses it to start the vehicle.
Python Program
class Engine:
def start(self):
return "Engine started successfully."
class Car:
def __init__(self):
self.engine = Engine()
def start_car(self):
return self.engine.start()
car = Car()
print(car.start_car())
Output
Engine started successfully.
Explanation
- The
Engine class defines a start() method.
- The
Car class creates an engine object inside its constructor.
- The engine object is stored in the
self.engine attribute.
- The
start_car() method delegates the operation to the engine object.
- When the car starts, the engine performs its own operation.
This example demonstrates composition because the car is designed to create and manage its engine component. It is a simplified model; in a real application, an engine could also be independently created and injected into a car.
4. What Is Aggregation in Python?
Aggregation is a relationship in which one object contains a reference to another object that can exist independently. The containing object uses the referenced object but does not necessarily own its entire lifecycle.
For example, a department can have several employees. Employees can exist before joining a department and can continue to exist after leaving it.
Key Characteristics of Aggregation
- Independent lifecycle: The related object can exist without the containing object.
- Shared references: The same object may be referenced by multiple containers when the application allows it.
- Flexible relationships: Objects can be connected or disconnected as requirements change.
- Reduced ownership dependency: Removing one reference does not necessarily mean the referenced object should be destroyed.
- Reusability: The same independently managed object can be used in different contexts.
In Python, aggregation is commonly implemented by passing an already-created object into another object's constructor and storing that reference as an attribute.
5. Basic Example of Aggregation
Consider a department that contains references to employee objects. The employees are created independently and then associated with the department.
Python Program
class Employee:
def __init__(self, name, role):
self.name = name
self.role = role
def display(self):
return f"{self.name} - {self.role}"
class Department:
def __init__(self, name, employees):
self.name = name
self.employees = employees
def display_employees(self):
print(f"Department: {self.name}")
for employee in self.employees:
print(employee.display())
employee1 = Employee("Amit", "Developer")
employee2 = Employee("Priya", "Tester")
department = Department(
"Technology",
[employee1, employee2]
)
department.display_employees()
Output
Department: Technology
Amit - Developer
Priya - Tester
Explanation
- The
Employee objects are created independently of the department.
- The
Department constructor receives references to those objects.
- The department stores the references in its
employees list.
- The department accesses employee methods to display their information.
- The employee objects can continue to exist even if the department object is no longer used, provided other references remain.
This is an example of aggregation because employees are independently managed objects that the department references.
6. Difference Between Composition and Aggregation
Although both relationships connect objects, their main distinction is the intended ownership and lifecycle of the component.
| Feature |
Composition |
Aggregation |
| Relationship |
Strong has-a relationship |
Weaker has-a relationship |
| Ownership |
The whole conceptually owns or manages the component. |
The whole references an independently managed object. |
| Lifecycle |
The component's lifecycle is conceptually tied to the whole. |
The component can exist independently of the whole. |
| Object creation |
Often created or managed by the containing object. |
Often created externally and passed into the container. |
| Independent reuse |
Usually not independently owned within the intended model. |
Can be shared or reused by different objects. |
| Python implementation |
Often creates a component inside the constructor. |
Often receives an existing object through a constructor parameter. |
| Example |
A document containing its own sections. |
A team referencing independently managed employees. |
| UML notation |
Filled diamond at the whole object end. |
Hollow diamond at the whole object end. |
Important: Python does not enforce composition or aggregation as special language features. These terms describe the design intention. The way objects are created, referenced, shared, and managed determines the relationship in the application.
7. Understanding the Has-A Relationship
A has-a relationship means that one object contains, uses, or references another object. Composition and aggregation are two forms of this relationship.
Examples of Has-A Relationships
| Containing Object |
Related Object |
Possible Relationship |
| Car |
Engine |
Composition when the engine is treated as an owned component. |
| Department |
Employee |
Aggregation when employees are independently managed. |
| Playlist |
Song |
Aggregation when songs exist independently of playlists. |
| Order |
OrderItem |
Composition when order items belong exclusively to their order. |
| Library |
Book |
Aggregation when books are independently managed and reusable. |
| House |
Room |
Composition when rooms are modeled as parts of a particular house. |
These are illustrative design choices, not universal rules. The correct relationship depends on the real-world meaning and lifecycle requirements of the system being modeled.
8. Creating Objects Inside a Class vs. Passing Objects into a Class
One practical way to understand composition and aggregation is to compare how component objects are supplied to a containing class.
Composition: Creating a Component Internally
class Battery:
def supply_power(self):
return "Battery supplying power"
class Phone:
def __init__(self):
self.battery = Battery()
def turn_on(self):
return self.battery.supply_power()
phone = Phone()
print(phone.turn_on())
Output
Battery supplying power
The Phone class creates its own Battery object. This design suggests that the phone manages its battery as an internal component.
Aggregation: Passing an Existing Object
class Battery:
def supply_power(self):
return "Battery supplying power"
class Phone:
def __init__(self, battery):
self.battery = battery
def turn_on(self):
return self.battery.supply_power()
battery = Battery()
phone = Phone(battery)
print(phone.turn_on())
Output
Battery supplying power
Here, the battery is created outside the phone and then passed into its constructor. This can represent aggregation when the battery is independently managed.
However, constructor placement alone does not determine the relationship. An object passed into a constructor can still become an exclusively owned component, and an internally created object can be exposed or shared. Ownership and lifecycle intent are the deciding factors.
9. Composition with Multiple Components
A single object can be composed of multiple components. Each component can handle a different responsibility, making the overall application easier to maintain.
Example: Computer System
class Processor:
def process(self):
return "Processor is running"
class Memory:
def load_data(self):
return "Memory is loading data"
class Computer:
def __init__(self):
self.processor = Processor()
self.memory = Memory()
def start(self):
print(self.processor.process())
print(self.memory.load_data())
print("Computer started")
computer = Computer()
computer.start()
Output
Processor is running
Memory is loading data
Computer started
Explanation
- The
Processor class manages processor-related behavior.
- The
Memory class manages memory-related behavior.
- The
Computer class combines these objects into a larger system.
- The
start() method delegates work to each component.
This structure separates responsibilities and avoids placing every operation into one large class. It models composition when the computer is intended to manage its components as part of the whole.
10. Aggregation with Shared Objects
Aggregation allows an object to be referenced by more than one container. This is useful when the same resource participates in multiple relationships.
Example: Employees Working on Multiple Projects
class Employee:
def __init__(self, name):
self.name = name
def display(self):
return self.name
class Project:
def __init__(self, name, employees):
self.name = name
self.employees = employees
def show_team(self):
names = [employee.display()
for employee in self.employees]
print(f"{self.name}: {', '.join(names)}")
employee1 = Employee("Amit")
employee2 = Employee("Priya")
project1 = Project("Website Development", [employee1])
project2 = Project("Mobile Application", [employee1, employee2])
project1.show_team()
project2.show_team()
Output
Website Development: Amit
Mobile Application: Amit, Priya
Explanation
The same employee1 object is referenced by two project objects. Both projects use the employee's information without owning the employee's entire lifecycle.
This design can be useful in project management systems, task assignment applications, educational platforms, and other systems where resources participate in multiple groups.
11. Composition and Delegation
Delegation occurs when one object relies on another object to perform a particular operation. Composition frequently uses delegation to distribute responsibilities across multiple classes.
Example: Music Player
class AudioPlayer:
def play_audio(self, file_name):
return f"Playing audio: {file_name}"
class MusicPlayer:
def __init__(self):
self.audio_player = AudioPlayer()
def play(self, file_name):
return self.audio_player.play_audio(file_name)
player = MusicPlayer()
print(player.play("song.mp3"))
Output
Playing audio: song.mp3
The MusicPlayer delegates the audio playback operation to its AudioPlayer component. This means the music player does not need to implement every low-level playback detail itself.
Benefits of Delegation
- Separates responsibilities between classes.
- Improves code reuse.
- Makes it easier to replace an implementation.
- Reduces the need for complicated inheritance hierarchies.
- Makes individual components easier to test.
12. Composition vs. Inheritance
Composition and inheritance are different ways to organize behavior in object-oriented programming.
Inheritance represents an is-a relationship. Composition represents a has-a relationship.
| Feature |
Composition |
Inheritance |
| Relationship |
Has-a |
Is-a |
| Implementation |
Combines objects as attributes. |
Creates a subclass from a parent class. |
| Code reuse |
Reuses behavior through delegation. |
Reuses inherited methods and attributes. |
| Flexibility |
Components can often be replaced at runtime. |
Behavior is tied more closely to the class hierarchy. |
| Coupling |
Can reduce coupling when components have clear interfaces. |
Can create stronger coupling between subclasses and base classes. |
| Example |
A car has an engine. |
A dog is an animal. |
Example of Inheritance
class Animal:
def eat(self):
return "Animal is eating"
class Dog(Animal):
def bark(self):
return "Dog is barking"
dog = Dog()
print(dog.eat())
print(dog.bark())
Example of Composition
class Engine:
def start(self):
return "Engine started"
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
return self.engine.start()
car = Car()
print(car.start())
Use inheritance when one class is genuinely a specialized form of another. Use composition when an object needs to collaborate with other objects to perform its responsibilities. Composition is often preferred when behavior needs to be combined or changed flexibly.
13. Composition and Aggregation in UML
Unified Modeling Language (UML) class diagrams help visualize relationships between classes.
Composition Symbol
Composition is represented by a filled diamond on the side of the containing or whole class.
Car ◆──────── Engine
This simplified representation indicates that a car is modeled as owning an engine component. In a formal UML diagram, the filled diamond is placed at the whole end of the relationship.
Aggregation Symbol
Aggregation is represented by a hollow diamond on the side of the containing or aggregate class.
Department ◇──────── Employee
This represents a department that references employees who can exist independently.
Relationship Summary
- Filled diamond: composition.
- Hollow diamond: aggregation.
- Simple line: association.
- Arrow with a hollow triangle: inheritance or generalization.
These diagrams help communicate the intended ownership, structure, and responsibilities of a system before implementation begins.
14. Real-World Example: Online Shopping Cart
An online shopping cart can demonstrate both composition and aggregation. A cart may own its individual cart-item entries, while referencing product objects managed by a product catalog.
Python Implementation
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
class CartItem:
def __init__(self, product, quantity):
self.product = product
self.quantity = quantity
def subtotal(self):
return self.product.price * self.quantity
class ShoppingCart:
def __init__(self):
self.items = []
def add_product(self, product, quantity=1):
item = CartItem(product, quantity)
self.items.append(item)
def display_cart(self):
total = 0
for item in self.items:
subtotal = item.subtotal()
total += subtotal
print(
f"{item.product.name} x {item.quantity} "
f"= ₹{subtotal}"
)
print(f"Total: ₹{total}")
product1 = Product("Keyboard", 1200)
product2 = Product("Mouse", 600)
cart = ShoppingCart()
cart.add_product(product1, 2)
cart.add_product(product2, 1)
cart.display_cart()
Output
Keyboard x 2 = ₹2400
Mouse x 1 = ₹600
Total: ₹3000
Relationship Explanation
- ShoppingCart and CartItem: The cart creates and manages cart-item entries, making this a composition-style relationship.
- CartItem and Product: Each cart item references a product that can exist independently in the product catalog. This can be modeled as aggregation.
- Subtotal calculation: Each cart item delegates the price lookup to its product reference and calculates its own subtotal.
This is a simplified example. Real shopping systems may store product identifiers and price snapshots instead of holding direct product references, especially when prices and product details can change.
15. Advantages of Composition and Aggregation
Advantages of Composition
- Modularity: Large systems can be divided into smaller components.
- Encapsulation: Each component manages its own state and behavior.
- Code reuse: Components can be reused in other suitable contexts.
- Maintainability: Changes to a component can be isolated from unrelated code.
- Clear ownership: The relationship between a whole and its parts is explicit in the design.
Advantages of Aggregation
- Flexibility: Objects can be connected to different containers over time.
- Independent lifecycle: Referenced objects can continue to exist independently.
- Shared resources: Multiple objects can use the same resource.
- Testability: Existing objects can be passed into a class during testing.
- Loose coupling: A class can work with objects supplied from outside rather than constructing every dependency itself.
16. Common Mistakes and Troubleshooting
| Mistake |
Why It Happens |
Solution |
| Confusing composition with aggregation |
Both relationships use object references. |
Consider ownership, sharing, and intended lifecycle rather than syntax alone. |
| Creating all objects inside one class |
The developer assumes every component must be internally created. |
Pass dependencies into constructors when external ownership or flexibility is required. |
| Using inheritance for every relationship |
The developer confuses has-a and is-a relationships. |
Use composition or aggregation when one object contains or references another. |
| Deleting a container and expecting all components to disappear |
The developer assumes Python automatically enforces composition lifecycles. |
Remember that objects remain alive while references to them exist; manage resources explicitly when needed. |
| Sharing mutable objects unintentionally |
Multiple containers reference the same object. |
Document ownership and decide whether shared state is intended. |
| Making a class responsible for too many tasks |
Components are not separated into clear responsibilities. |
Split the functionality into focused classes and delegate operations appropriately. |
| Assuming constructor placement proves ownership |
The implementation detail is mistaken for the design relationship. |
Determine whether the object is independently managed or exclusively owned in the application model. |
17. Best Practices for Composition and Aggregation
- Model real-world relationships carefully: Decide whether a component is an owned part or an independently managed object.
- Follow single responsibility: Give each class a clear, focused purpose.
- Prefer composition over unnecessary inheritance: Combine objects when a has-a relationship better represents the design.
- Use dependency injection where appropriate: Pass externally managed dependencies into constructors to improve flexibility and testability.
- Avoid unintended shared state: If multiple objects reference the same mutable object, understand how changes will affect all users.
- Keep interfaces simple: Components should expose only the operations needed by the containing class.
- Manage resources explicitly: Files, database connections, and network resources may require context managers or explicit cleanup.
- Document ownership: Make it clear which class creates, shares, replaces, or releases a component.
- Write unit tests: Test component behavior separately and verify how containing objects interact with those components.
- Do not overcomplicate simple programs: Introduce additional classes when they improve clarity, reuse, or maintainability.
18. Practical Exercises
Exercise 1: School and Classroom
Create a Classroom class and a School class. Let the school create and manage its classrooms as components. Add methods to display classroom information.
Exercise 2: Department and Employees
Create an Employee class and a Department class. Create employees independently and pass them into the department. Display the department name and employee details.
Exercise 3: Library and Books
Create a Book class and a Library class. Allow the library to store references to existing book objects and display all available books.
Exercise 4: Computer and Components
Create Processor, Memory, and Computer classes. The computer should create or manage its components and delegate startup operations to them.
Exercise 5: Team and Projects
Create an Employee class and a Project class. Allow the same employee object to participate in more than one project, demonstrating aggregation and shared references.
Exercise 6: Shopping Cart
Create Product, CartItem, and ShoppingCart classes. Add multiple products to the cart, calculate subtotals, and display the grand total.
19. Mini Project: Library Management System
Project Goal: Build a small library management system that demonstrates aggregation by allowing a library to reference independently created book objects.
Project Requirements
- Create a
Book class with title, author, and publication year.
- Create a
Library class that stores references to book objects.
- Add a method to register an existing book.
- Add a method to display all books.
- Add a method to search for a book by title.
- Keep book creation separate from library creation.
Sample Solution
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def display(self):
return f"{self.title} by {self.author} ({self.year})"
class Library:
def __init__(self, name):
self.name = name
self.books = []
def add_book(self, book):
self.books.append(book)
def display_books(self):
print(f"Library: {self.name}")
for book in self.books:
print(book.display())
def search_book(self, title):
for book in self.books:
if book.title.lower() == title.lower():
return book
return None
book1 = Book("Python Basics", "A. Sharma", 2024)
book2 = Book("Object-Oriented Design", "R. Mehta", 2023)
library = Library("Central Library")
library.add_book(book1)
library.add_book(book2)
library.display_books()
result = library.search_book("Python Basics")
if result:
print("Found:", result.display())
Output
Library: Central Library
Python Basics by A. Sharma (2024)
Object-Oriented Design by R. Mehta (2023)
Found: Python Basics by A. Sharma (2024)
Project Learning Outcomes
- Understand how independently created objects can be aggregated.
- Practice class design and object references.
- Implement searching and displaying operations.
- Learn how to separate object creation from object management.
- Build a foundation for larger applications involving multiple interacting classes.
20. Interview Questions and Answers
Q1. What is composition in Python?
Composition is a design relationship in which an object owns or manages other objects as components. The component's lifecycle is conceptually tied to the containing object.
Q2. What is aggregation in Python?
Aggregation is a has-a relationship in which one object references another object that can exist independently of the containing object.
Q3. What is the main difference between composition and aggregation?
The main difference is ownership and lifecycle dependency. Composition represents stronger ownership, while aggregation represents a looser relationship with independently managed objects.
Q4. What is a has-a relationship?
A has-a relationship occurs when an object contains or references another object. A car having an engine is a common example.
Q5. How do you implement composition in Python?
Composition can be implemented by creating an object of one class inside another class and storing it as an instance attribute, when that design reflects the intended ownership relationship.
Q6. How do you implement aggregation in Python?
Aggregation is commonly implemented by creating an object independently and passing it into another class, which stores a reference to that object.
Q7. Does Python enforce composition and aggregation?
No. Python provides object references but does not have special syntax that automatically enforces these relationships. They are design concepts expressed through object creation, references, ownership, and lifecycle management.
Q8. Can one object be shared by multiple objects?
Yes. Multiple objects can hold references to the same object. This is often useful for aggregation but requires care when the shared object is mutable.
Q9. What is the difference between composition and inheritance?
Composition represents a has-a relationship by combining objects. Inheritance represents an is-a relationship in which a subclass derives behavior or characteristics from a parent class.
Q10. Can composition improve code reusability?
Yes. Components can encapsulate separate responsibilities and be reused by multiple containing classes.
Q11. What is delegation?
Delegation occurs when one object calls another object's method to perform a task. It is frequently used in composition to distribute responsibilities.
Q12. What happens when a Python object is deleted?
Deleting a variable or reference does not necessarily destroy the object. The object can remain alive while other references exist. Python's memory management is separate from the conceptual ownership relationship.
Q13. What is the difference between association and aggregation?
Association is a general relationship between objects. Aggregation is a more specific whole-part relationship in which the part can exist independently of the whole.
Q14. When should aggregation be preferred?
Aggregation is suitable when the related object is independently managed, can be shared, or needs to continue existing outside the container.
Q15. Why is composition often preferred over inheritance?
Composition can make behavior easier to replace, combine, and test without creating a rigid class hierarchy. However, the choice depends on the actual relationship and design requirements.
21. Quick Revision Notes
- Composition and aggregation are object-oriented has-a relationships.
- Composition represents strong ownership and a conceptually dependent component lifecycle.
- Aggregation represents a relationship with an independently managed object.
- Composition commonly creates or manages components within a containing class.
- Aggregation commonly stores references to externally created objects.
- Python does not automatically enforce either relationship.
- Composition often uses delegation to distribute responsibilities.
- Inheritance represents an is-a relationship, not a has-a relationship.
- UML represents composition with a filled diamond and aggregation with a hollow diamond.
- Good object-oriented design makes ownership, responsibilities, and dependencies clear.
Key Syntax to Remember
Composition Example
class Component:
def execute(self):
return "Component executed"
class Container:
def __init__(self):
self.component = Component()
def run(self):
return self.component.execute()
container = Container()
print(container.run())
Aggregation Example
class Component:
def execute(self):
return "Component executed"
class Container:
def __init__(self, component):
self.component = component
def run(self):
return self.component.execute()
component = Component()
container = Container(component)
print(container.run())
22. Conclusion
Composition and aggregation help developers design Python applications by organizing objects into meaningful relationships. Composition is appropriate when a containing object owns and manages components as parts of a whole. Aggregation is suitable when an object references independently managed components that may be shared or used elsewhere.
Understanding these relationships improves modularity, code reuse, maintainability, and flexibility. Combined with encapsulation, delegation, and appropriate use of inheritance, they provide a strong foundation for designing real-world Python applications.
Continue learning object-oriented programming through the Python Training Course at JustAcademy. You can also visit the JustAcademy website or register for a course demo.