Excellent.
DAY 20 is about real design decisions — the difference between code that works and code that scales, stays readable, and survives refactoring.

Most interviewers are not testing syntax here — they are testing engineering judgment.


🧠 DAY 20 — Inheritance, Composition & Mixins

(Pythonic Class Design & Tradeoffs)


🔑 CORE IDEA OF DAY 20

Inheritance is about “is-a”.
Composition is about “has-a”.
Python strongly prefers composition unless inheritance is unavoidable.


1️⃣ Inheritance — When It Makes Sense

Inheritance is appropriate when:

  • There is a true subtype relationship
  • Behavior is shared conceptually
  • Subclasses can safely replace base class (LSP)
class Animal:
    def speak(self):
        raise NotImplementedError

class Dog(Animal):
    def speak(self):
        return "woof"

This is good inheritance.


2️⃣ When Inheritance Becomes a Problem 🔥

class Car:
    def start(self): ...

class ElectricCar(Car):
    def refuel(self): ...   # ❌ conceptual mismatch

Smell:

  • Base class forces irrelevant behavior
  • Subclass violates expectations

This leads to:

  • Complex overrides
  • Fragile hierarchies
  • MRO confusion

3️⃣ Composition — Pythonic Default

class Engine:
    def start(self):
        print("engine start")

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

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

Why this is better:

  • Loosely coupled
  • Easier testing
  • Easier refactoring
  • No inheritance entanglement

🧠 Prefer composition unless inheritance is truly required.


4️⃣ Mixins — Controlled Multiple Inheritance

A mixin:

  • Provides a small, focused capability
  • Is not a standalone class
  • Is meant to be combined
class JSONSerializableMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

Usage:

class User(JSONSerializableMixin):
    def __init__(self, name):
        self.name = name

Mixins:

  • Have no state (ideally)
  • Do one thing well
  • Rely on MRO cooperation

5️⃣ Rules for Safe Mixins (INTERVIEW EXPECTED)

✔ Mixin should:

  • Be small
  • Avoid __init__
  • Use super() carefully
  • Assume nothing about base class except interface

❌ Mixin should NOT:

  • Be instantiated alone
  • Hold business state
  • Depend on concrete class details

6️⃣ Cooperative Inheritance with super() (RECAP)

class A:
    def f(self):
        print("A")

class Mixin:
    def f(self):
        print("Mixin")
        super().f()

class B(Mixin, A):
    pass

B().f()

Output:

Mixin
A

Works because:

  • All classes cooperate
  • MRO respected

7️⃣ Composition vs Inheritance — Side-by-Side

ScenarioChoose
Code reuseComposition
Subtype relationshipInheritance
Optional behaviorMixin
Avoid couplingComposition
Framework extensionInheritance / mixins

8️⃣ Multiple Inheritance (Use Carefully)

Python allows:

class C(A, B):
    pass

But danger zones:

  • Conflicting method names
  • Confusing MRO
  • Hard debugging

🧠 Use multiple inheritance only for mixins.


9️⃣ Real-World Python Examples

  • Django CBVs → heavy mixin usage
  • Flask → composition
  • Pandas → composition + delegation
  • Spark Python APIs → composition over inheritance

These libraries avoid deep hierarchies deliberately.


🔟 Composition Enables Dependency Injection

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

Benefits:

  • Easy mocking
  • Easy testing
  • Swappable implementations

This is professional-grade design.


🔥 INTERVIEW TRAPS (DAY 20)

Q1

Why is deep inheritance discouraged?

✔ Tight coupling
✔ Fragile base class
✔ MRO complexity


Q2

When is a mixin better than inheritance?

✔ Optional behavior
✔ Cross-cutting concerns
✔ No “is-a” relationship


Q3

Why mixins shouldn’t have __init__?

✔ Break MRO cooperation
✔ Cause double initialization


🧠 DAY 20 MENTAL CHECKLIST

Before choosing inheritance:

  1. Is this truly “is-a”?
  2. Will future subclasses break?
  3. Can composition solve this?
  4. Is behavior optional?
  5. Will MRO stay simple?

📝 DAY 20 ASSIGNMENT (MANDATORY)

1️⃣ Predict output (NO RUNNING):

class A:
    def f(self):
        print("A")

class M:
    def f(self):
        print("M")
        super().f()

class B(M, A):
    pass

B().f()

2️⃣ Explain clearly:

  • Difference between inheritance and composition
  • Why Python prefers composition

3️⃣ Design question:

How would you design logging behavior reusable across unrelated classes?

(Hint: mixin vs decorator)


🔜 DAY 21 PREVIEW

DAY 21 — Dunder Methods (Magic Methods & Operator Overloading)

You’ll learn:

  • How operators actually work
  • __str__ vs __repr__
  • Context managers
  • How Python protocols enable elegance

When ready, say 👉 “START DAY 21”