# Class Relationships

Course: [Python](https://www.devtutorials.ai/learn/python) · Section: Advanced Python
Source: https://www.devtutorials.ai/learn/python/advanced-python/class-relationships

> Has-a instead of is-a: aggregation where the part is passed in, composition where the whole builds it, and how to tell them apart.

Inheritance answers one question: *is this a kind of that?* A `ScienceTeacher` **is a** `Teacher`.

Most of the time the answer is no. A classroom is not a kind of teacher, and a school is not a kind of classroom. But a classroom **has a** teacher, and a school **has** classrooms. That second relationship is the one you will use far more often, and it comes in two strengths.

Here is that has-a relationship in code — a classroom holding a teacher object it was handed:
```python runnable title="A classroom has a teacher"
class Teacher:
    def __init__(self, name):
        self.name = name

class Classroom:
    def __init__(self, label, teacher):
        self.label = label
        self.teacher = teacher

mrs_rao = Teacher("Mrs Rao")
room = Classroom("7A", mrs_rao)

print(room.label, "is taught by", room.teacher.name)
print(room.teacher is mrs_rao)
```

Inheritance is **is-a**. Aggregation and composition are both **has-a**. The difference between the two is whether the part can live without the whole.

## What You Will Learn

In this lesson, you will learn:

- The difference between is-a and has-a, and why has-a is usually the right answer
- What aggregation is, and how to spot it: the part was made outside and handed in
- What composition is: the whole makes its own parts, and they go when it goes
- How to tell them apart in two seconds by asking one question
- Why "favour composition over inheritance" is advice worth following

By the end of this lesson, you will be able to tell aggregation and composition apart at a glance, choose between them with one question, and build objects that hold their parts together without pretending to be something they are not.

## Aggregation

### What Is Aggregation?

**Aggregation** is a has-a relationship where the part exists on its own. It was made elsewhere, it is handed to the whole, and it carries on existing after the whole is gone.

A classroom has a teacher. The teacher existed before that classroom and will still be employed after it is closed. The classroom borrows them.

### The Has-A Relationship

Here is the has-a relationship in full — the whole keeps a reference to the part and hands work to it:

```python runnable title="Reading the relationship out loud"
class Teacher:
    def __init__(self, name, subject):
        self.name = name
        self.subject = subject

    def describe(self):
        return f"{self.name} teaches {self.subject}"

class Classroom:
    def __init__(self, label, teacher):
        self.label = label
        self.teacher = teacher

    def describe(self):
        return f"Class {self.label}: {self.teacher.describe()}"

mrs_rao = Teacher("Mrs Rao", "Maths")
room = Classroom("7A", mrs_rao)

print(room.describe())
```

`Classroom` has a `Teacher`. It does not try to be one, and it does not inherit anything from one.

### Creating Aggregated Objects

The sign of aggregation is in the `__init__` line: the part arrives as a **parameter**.

```python runnable title="The part is made outside and passed in"
class Teacher:
    def __init__(self, name):
        self.name = name

class Classroom:
    def __init__(self, label, teacher):
        self.label = label
        self.teacher = teacher

mrs_rao = Teacher("Mrs Rao")

morning = Classroom("7A", mrs_rao)
afternoon = Classroom("8B", mrs_rao)

print(morning.teacher.name, afternoon.teacher.name)
print(morning.teacher is afternoon.teacher)
```

One teacher, two classrooms, and both hold the *same* object. That sharing is only possible because the teacher is not owned by either room.

### Accessing Aggregated Objects

You reach the part through the whole, with a dot for each step.

```python runnable title="One dot per step"
class Teacher:
    def __init__(self, name, subject):
        self.name = name
        self.subject = subject

class Classroom:
    def __init__(self, label, teacher):
        self.label = label
        self.teacher = teacher

room = Classroom("7A", Teacher("Mrs Rao", "Maths"))

print(room.teacher)
print(room.teacher.name)
print(room.teacher.subject.upper())
```

### The Part Outlives the Whole

This is the test that settles it. Delete the classroom and ask whether the teacher is still there.

```python runnable title="The teacher survives the classroom"
class Teacher:
    def __init__(self, name):
        self.name = name

class Classroom:
    def __init__(self, label, teacher):
        self.label = label
        self.teacher = teacher

mrs_rao = Teacher("Mrs Rao")
room = Classroom("7A", mrs_rao)

print("Before:", room.teacher.name)

del room

print("After the classroom is gone:", mrs_rao.name)
```

### An Aggregation Example

Here is aggregation at a larger size — a school holding classrooms, while the staff list stays outside where it belongs:

```python runnable title="A school of classrooms, and a staff list of its own"
class Teacher:
    def __init__(self, name):
        self.name = name

class Classroom:
    def __init__(self, label, teacher):
        self.label = label
        self.teacher = teacher

class School:
    def __init__(self, name):
        self.name = name
        self.classrooms = []

    def add(self, classroom):
        self.classrooms.append(classroom)

staff = [Teacher("Mrs Rao"), Teacher("Mr Khan")]

school = School("Greenfield")
school.add(Classroom("7A", staff[0]))
school.add(Classroom("8B", staff[1]))

for room in school.classrooms:
    print(room.label, "—", room.teacher.name)

print("Staff still listed:", [teacher.name for teacher in staff])
```

### Benefits of Aggregation

- **Sharing.** One teacher object can be used by several classrooms, and a change to their name shows everywhere.
- **Independent lifetimes.** Closing a classroom does not sack the teacher.
- **Easier testing.** You can hand in a simple stand-in object instead of the real one.
- **Looser coupling.** The classroom only needs the teacher to have a `name`; it does not care which class it came from.

Sharing cuts both ways. Because both classrooms hold the same teacher object, changing `mrs_rao.name` changes what both rooms report. That is usually what you want — but only because there is genuinely one teacher.

## Composition

### What Is Composition?

**Composition** is a has-a relationship where the whole **owns** the part. The whole creates it, nothing else holds it, and when the whole is gone the part has no reason to exist.

A classroom has desks. Those desks belong to that room. Close the room and the desks are not reassigned to a teacher — they go with it.

### A Strong Has-A Relationship

Here is composition in code — the room builds its own desks:

```python runnable title="The room makes its own desks"
class Desk:
    def __init__(self, number):
        self.number = number

class Classroom:
    def __init__(self, label, desk_count):
        self.label = label
        self.desks = [Desk(number) for number in range(1, desk_count + 1)]

room = Classroom("7A", 4)

print(room.label, "has", len(room.desks), "desks")
print([desk.number for desk in room.desks])
```

Compare this `__init__` with the aggregation one. No `Desk` is passed in. The room is handed a *number* and builds the desks itself.

### Creating Composed Objects

Here are the two relationships side by side in one class, so you can see the difference in a single glance:

```python runnable title="Aggregation and composition, side by side"
class Teacher:
    def __init__(self, name):
        self.name = name

class Desk:
    def __init__(self, number):
        self.number = number

class Classroom:
    def __init__(self, label, teacher, desk_count):
        self.label = label
        self.teacher = teacher
        self.desks = [Desk(number) for number in range(1, desk_count + 1)]

room = Classroom("7A", Teacher("Mrs Rao"), 3)

print("Teacher passed in :", room.teacher.name)
print("Desks built inside:", len(room.desks))
```

One class, both relationships, and the difference is visible in a single line each.

### Object Lifecycle Dependency

Nothing outside the room holds those desks, so when the room goes, they go too. Python collects an object when nothing refers to it any more.

```python runnable title="Nothing else is holding the desks"
import sys

class Desk:
    def __init__(self, number):
        self.number = number

class Classroom:
    def __init__(self, label, desk_count):
        self.label = label
        self.desks = [Desk(number) for number in range(1, desk_count + 1)]

room = Classroom("7A", 2)
first_desk = room.desks[0]

print("References to the first desk:", sys.getrefcount(first_desk) - 1)

outside = Desk(99)
print("References to a desk nobody owns:", sys.getrefcount(outside) - 1)
```

The desk inside the room is held by the room's list *and* by the name `first_desk`. The point is that without that extra name, the room's list is the only thing holding it — so the room's lifetime is the desk's lifetime.

### A Composition Example

Here is composition at work — a report card that makes its own rows:

```python runnable title="A report card made of its own rows"
class Row:
    def __init__(self, subject, mark):
        self.subject = subject
        self.mark = mark

    def line(self):
        status = "pass" if self.mark >= 35 else "fail"
        return f"  {self.subject:<10}{self.mark:>4}  {status}"

class ReportCard:
    def __init__(self, student, results):
        self.student = student
        self.rows = [Row(subject, mark) for subject, mark in results.items()]

    def show(self):
        print(self.student)
        for row in self.rows:
            print(row.line())
        print(f"  {'Average':<10}{self.average():>4.0f}")

    def average(self):
        return sum(row.mark for row in self.rows) / len(self.rows)

card = ReportCard("Asha", {"Maths": 78, "Science": 85, "English": 30})
card.show()
```

Nobody outside ever holds a `Row`. They are made from the marks, they live in the card, and they go with it.

### Benefits of Composition

- **Clear ownership.** One object is responsible for the part, so there is no question who may change it.
- **No surprises from sharing.** Nothing else holds the part, so nothing else can change it under you.
- **Tidy cleanup.** The part goes when the whole goes, with nothing left behind.
- **Small classes.** A big class becomes a whole made of small, testable parts.

Composition is only real if nothing leaks. The moment a method returns `self.desks`, the caller holds the list and can add to or empty it. Return a copy — `list(self.desks)` — or a tuple, if the ownership is meant to mean anything.

```python runnable title="A leak, and the fix"
class Classroom:
    def __init__(self, label, desk_count):
        self.label = label
        self._desks = list(range(1, desk_count + 1))

    def leaky_desks(self):
        return self._desks

    def safe_desks(self):
        return list(self._desks)

room = Classroom("7A", 3)

room.leaky_desks().clear()
print("After clearing what leaky_desks returned:", room._desks)

room = Classroom("7A", 3)
room.safe_desks().clear()
print("After clearing what safe_desks returned:", room._desks)
```

## Aggregation against Composition

### Key Differences

Here are the differences side by side:

| | Aggregation | Composition |
|---|---|---|
| Strength | Weak has-a | Strong has-a |
| Who makes the part | Somebody else | The whole itself |
| How it arrives | Passed into `__init__` | Built inside `__init__` |
| Can be shared | Yes | No |
| When the whole is deleted | The part lives on | The part goes too |
| Read it as | "uses a" | "is made of" |
| Example | A classroom and its teacher | A classroom and its desks |

### The One Question

**Can the part exist on its own, and be used by something else?**

- Yes → aggregation. Make it outside, pass it in.
- No → composition. Build it inside.

```python runnable title="The question answered in code"
class Book:
    def __init__(self, title):
        self.title = title

class Page:
    def __init__(self, number):
        self.number = number

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

class Novel:
    def __init__(self, title, page_count):
        self.title = title
        self.pages = [Page(n) for n in range(1, page_count + 1)]

shared = Book("Python for Schools")
library = Library([shared])
print("Aggregation — the book exists without the library:", shared.title)

novel = Novel("A Short Story", 3)
print("Composition — the pages exist only inside:", len(novel.pages))
```

### Real-World Examples

Here is how the choice looks in familiar places:

| Whole | Part | Which | Why |
|---|---|---|---|
| School | Teachers | Aggregation | A teacher can move to another school |
| School | Buildings | Composition | A building is not reassigned when the school closes |
| Classroom | Teacher | Aggregation | Shared across several classes |
| Classroom | Desks | Composition | Bought for that room |
| Playlist | Songs | Aggregation | The same song is in many playlists |
| Report card | Rows | Composition | A row has no meaning on its own |
| Order | Customer | Aggregation | The customer existed before the order |
| Order | Order lines | Composition | A line means nothing without its order |

### When to Use Each

Use **aggregation** when the part is a real thing in its own right — a person, a product, an account — that other parts of the program also know about.

Use **composition** when the part is detail: a piece you invented to keep a big class small, that nobody outside needs to name.

And use **inheritance** only when the child genuinely is a kind of the parent, and can be used anywhere the parent can.

```python runnable title="Composition where inheritance was tempting"
class Engine:
    def start(self):
        return "engine running"

class CarByInheritance(Engine):
    pass

class CarByComposition:
    def __init__(self):
        self.engine = Engine()

    def start(self):
        return self.engine.start()

print(CarByInheritance().start())
print(CarByComposition().start())
print(isinstance(CarByInheritance(), Engine))
```

Both print the same thing, and the first one is wrong. A car **is not** an engine — and that claim is not just untidy: `isinstance(car, Engine)` now answers `True`, so any code that checks "is this an engine?" will accept a car. The second version says what is actually true, and swapping in a different engine later costs one line.

"Favour composition over inheritance" is the usual version of this advice. The reason is plain: a child class is tied to its parent's every change, while a part is only tied to the few methods you actually call on it.

## Common mistakes

Before moving on, here are some of the mistakes beginners make with these two relationships.

Each row is one mistake. The middle column is what you will actually see on screen.

<div className="tscroll">

| Mistake | What you see | Why it happens | Fix |
|---|---|---|---|
| Inheriting for a has-a | `isinstance(car, Engine)` is `True` | Inheritance claims is-a | Hold the part as an attribute |
| Building the part inside when it should be shared | Two wholes get two different parts | That is composition, not aggregation | Pass the part into `__init__` |
| Passing the part in when nobody else should have it | An outsider changes it later | That is aggregation, not composition | Build it inside |
| Returning the internal list | `AttributeError`-free, but the caller empties it | The caller got the real list | Return `list(self.desks)` |
| A mutable default, `def __init__(self, desks=[])` | Every object shares one list | The default is made once | `desks=None`, then `self.desks = desks or []` |
| Expecting `del room` to delete the teacher | The teacher is still there | Other names still hold it | That is the point of aggregation |
| Reaching through several objects, `a.b.c.d` | Fragile code, breaks on any change | The caller knows too much | Give the whole a method that does it |
| Thinking `self.desks = desks` copies | Changing the caller's list changes yours | A name is bound, not copied | `self.desks = list(desks)` |

</div>

## What to Remember

- Inheritance is **is-a**. Aggregation and composition are both **has-a**.
- **Aggregation**: the part is made elsewhere and passed in. It can be shared, and it outlives the whole.
- **Composition**: the whole builds the part itself. Nothing else holds it, and it goes when the whole goes.
- The giveaway is in `__init__` — a part that arrives as a parameter is aggregated; one built inside is composed.
- Ask one question: can the part exist on its own and be used by something else?
- Shared parts mean shared changes. That is the point, and the risk.
- Composition only holds if nothing leaks: return a copy, not the internal list.
- A mutable default like `desks=[]` is made once and shared by every object. Use `None` and build inside.
- Reaching through `a.b.c.d` means the caller knows too much; give the whole a method instead.
- Favour composition over inheritance — a part ties you to a few methods, a parent ties you to everything it does.

Next: **Multithreading**
