šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Composition vs Inheritance in Python

"Favor composition over inheritance" is repeated everywhere — this lesson makes the trade-off concrete with real Python examples of when each genuinely is the right tool.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

What problem does needing "console AND file AND JSON" logging expose about the inheritance-based design?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Inheritance and composition both let one object reuse another's behavior, but they create very different kinds of coupling. This lesson works through a real refactor — from a fragile inheritance hierarchy to a flexible composed one — so the trade-off stops being an abstract slogan and becomes a concrete design skill.

1Why Inheritance Feels Right at First

class FileLogger(Logger): genuinely does save typing at the moment you write it — format_message is inherited for free, and FileLogger only needs to override the one method that actually differs. This immediate payoff is exactly why inheritance is usually the first tool reached for when one class needs 'most of what another class already does.'

The hidden cost doesn't show up in this first version; it shows up when requirements *grow* in a direction the original hierarchy didn't anticipate. The moment you need logging to go to both the console *and* a file simultaneously, FileLogger and a hypothetical ConsoleLogger don't compose — you're stuck either writing a new ConsoleAndFileLogger subclass that duplicates logic from both, or reaching for multiple inheritance, which brings its own complexity (method resolution order, diamond problems) for what should be a simple 'do both things' requirement.

Add a requirement for JSON-formatted output on top of that, and the combinatorial nature of the problem becomes undeniable: every independent axis of variation (where output goes, how it's formatted) that gets modeled as a subclass multiplies against every other axis, producing a subclass explosion that grows multiplicatively, not additively, with each new feature.

āœ•
—
+
class Logger:
    def format_message(self, msg: str) -> str:
        return f"[LOG] {msg}"
    def log(self, msg: str) -> None:
        print(self.format_message(msg))

class FileLogger(Logger):
    def log(self, msg: str) -> None:
        with open("app.log", "a") as f:
            f.write(self.format_message(msg) + "\n")
localhost:3000
Inheritance Growth
2 outputs Ɨ 2 formats
= 4 subclasses needed, growing multiplicatively

2Composition: Independent Axes, Independently Swappable

The composed Logger fixes the combinatorial problem by recognizing that 'where output goes' and 'how it's formatted' are genuinely *independent* concerns, and modeling each as its own small, swappable object rather than baking both into a single class hierarchy. Logger no longer inherits formatting or output behavior — it *holds references* to a formatter object and a list of output objects, delegating to them rather than implementing their behavior itself.

Adding a new output destination (say, RemoteOutput, shipping logs to a remote aggregation service) now requires writing exactly one new class implementing the shared write() interface — Logger itself doesn't change, JsonFormatter doesn't change, no existing Output implementation changes. The same is true in reverse: adding a new formatter never requires touching any output class. Each axis of variation grows independently, additively, instead of multiplying against every other axis.

This is the concrete mechanism behind 'favor composition over inheritance': composition decomposes a problem along its *actual* independent dimensions (what varies together stays together; what varies independently gets separated), while a naive inheritance hierarchy often forces every combination of independent concerns into a single, linear chain of is-a relationships that can't represent 'and' cleanly.

āœ•
—
+
class ConsoleAndFileLogger(Logger):
    def log(self, msg):
        print(self.format_message(msg))
        with open("app.log", "a") as f:
            f.write(self.format_message(msg) + "\n")

# Need JSON formatting too? Now it's ConsoleAndFileJsonLogger...
# combinatorial explosion of subclasses
localhost:3000
Composed Flexibility
New RemoteOutput class
Zero existing classes touched — pure addition

3When Inheritance Is Still the Right Choice

None of this means inheritance is a mistake to avoid categorically — it remains the right tool specifically when there's a genuine, stable is-a relationship *and* the subclass fully honors the parent's behavioral contract (the Liskov Substitution Principle from the previous lesson). A ValidationError subclassing Exception is a clean, appropriate use of inheritance: every ValidationError genuinely *is* an Exception in every meaningful sense, that relationship is stable and unlikely to need 'and' composition later, and Python's exception-handling machinery is itself built around exactly this kind of type hierarchy.

A useful diagnostic question when choosing between the two: 'will I ever need an object that has *both* of these behaviors simultaneously, or need to swap one of these behaviors independently at runtime?' If yes, that's a strong signal toward composition — inheritance struggles to represent 'has both' or 'swappable at runtime' cleanly, since a class's base classes are fixed at definition time, not reconfigurable per-instance the way composed collaborator objects are.

Many of Python's own standard-library and popular third-party designs lean composition-first for exactly this flexibility: logging.Logger itself (the real standard-library one) is composed of Handler, Formatter, and Filter objects, not a deep class hierarchy — a strong real-world validation of the pattern this lesson builds by hand.

āœ•
—
+
class Logger:
    def __init__(self, formatter, outputs: list):
        self.formatter = formatter
        self.outputs = outputs

    def log(self, msg: str) -> None:
        formatted = self.formatter.format(msg)
        for output in self.outputs:
            output.write(formatted)

logger = Logger(JsonFormatter(), [ConsoleOutput(), FileOutput("app.log")])
localhost:3000
Choosing the Right Tool
ValidationError(Exception)
Genuine, stable is-a — inheritance is the right call here

4Step-by-Step Breakdown

"Favor composition over inheritance" is easy to say and hard to apply. Let's refactor a real, fragile inheritance hierarchy and feel exactly why the advice exists.

Inheritance looks convenient at first: a FileLogger reusing a Logger's format_message method for free.

The fragility shows up when requirements grow: needing BOTH file and console output means either multiple inheritance chaos or a new subclass per COMBINATION.

Checkpoint: What problem does needing "console AND file AND JSON" logging expose about the inheritance-based design?

  • →Every new combination of features requires a brand new subclass — a combinatorial explosion
  • →Inheritance is inherently slower at runtime than composition

Composition fixes this: Logger HAS a formatter and HAS a list of outputs, both swappable independently, with no combinatorial subclassing.

Checkpoint: In the composed Logger, how many classes need to change to add a new output destination (e.g. sending logs to a remote server)?

  • →Zero existing classes change — just add one new output class implementing the same interface
  • →Every existing formatter and output class needs to be updated

With composition as your default reach, design patterns are the next layer — many of the classic patterns exist specifically to structure composition well.

Compose Real Behavior via Injection. Finish Logger.log(): injecting the output object avoids a subclass explosion.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Ask "will this need to combine with something else?" before choosing inheritance

If a class might need to mix and match independent behaviors (output destinations, formats, strategies) rather than fitting one fixed slot in a hierarchy, composition avoids the combinatorial subclass explosion inheritance would produce.

Reserve inheritance for genuine, stable is-a relationships that satisfy LSP

Exception hierarchies, and cases where a subclass is truly a specialized version of its parent with no behavioral surprises, are exactly where inheritance remains simpler and more appropriate than composition.

Frequent Bugs

THE BUG

Extending an inheritance hierarchy to handle a new combination of features (e.g. ConsoleAndFileJsonLogger) instead of recognizing the combinatorial growth as a signal to refactor toward composition.

THE FIX

When a class hierarchy starts needing a new subclass for every combination of independent features, refactor the independent, combinable behaviors into separate composed objects instead of continuing to grow the hierarchy.

Real-World Examples

Refactoring a Payment Processor Hierarchy Into Composed Strategies

A PaymentProcessor class hierarchy has grown CreditCardProcessorWithRetry, PayPalProcessorWithRetry, and CreditCardProcessorWithLogging subclasses — every new cross-cutting concern (retry, logging) multiplies against every payment method.

class PaymentProcessor:
    def __init__(self, gateway, retry_policy, logger):
        self.gateway = gateway
        self.retry_policy = retry_policy
        self.logger = logger

    def process(self, amount: float) -> bool:
        self.logger.log(f"Processing {amount}")
        return self.retry_policy.execute(lambda: self.gateway.charge(amount))

processor = PaymentProcessor(CreditCardGateway(), ExponentialBackoffRetry(), ConsoleLogger())

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Continuing to add subclasses for every new combination of features (ConsoleAndFileJsonLogger, ConsoleAndFileXmlLogger, ...) instead of recognizing the pattern and refactoring to composition.

# Wrong: hierarchy grows multiplicatively class ConsoleAndFileJsonLogger(Logger): ... class ConsoleAndFileXmlLogger(Logger): ... # Correct: independent, composable pieces logger = Logger(JsonFormatter(), [ConsoleOutput(), FileOutput("app.log")]) logger2 = Logger(XmlFormatter(), [ConsoleOutput()])

The Solution //

When you notice yourself naming a class after a conjunction of features ("AndFile", "AndJson"), that's the signal to extract each independent feature into its own composed object instead of continuing to grow the hierarchy.

Lesson Glossary

[01]Composition

A design approach where a class holds references to other objects (collaborators) and delegates behavior to them, rather than inheriting that behavior.

Code Preview
// Composition context

[02]is-a relationship

The relationship inheritance models: a subclass is a more specific kind of its parent class, and should be substitutable for it (see Liskov Substitution).

Code Preview
// is-a relationship context

[03]has-a relationship

The relationship composition models: an object holds (has) a reference to another object it delegates to, without being a specialized version of it.

Code Preview
// has-a relationship context

[04]Combinatorial explosion

The problem where modeling multiple independent behavioral axes as subclasses requires a new class for every combination, growing multiplicatively.

Code Preview
// Combinatorial explosion context

Continue Learning