Logo OnlineCBT
PYTHON Tutorial
PYTHON · LESSON 37

Composition, Abstract Classes and Protocols

Design flexible object relationships and explicit contracts.

Level: Beginner to Advanced
Duration: ~60 Mins Deep-Dive
Updated: 26 Jul 2026

Lesson 37: Composition, Abstract Classes and Protocols

1. Introduction to Enterprise Architecture & Low-Coupling Design Patterns

In previous lessons, we mastered Object-Oriented Programming (OOP) fundamentals—including constructors, methods, encapsulation via properties, inheritance hierarchies, and magic methods. However, as applications scale to enterprise production levels, relying excessively on deep, rigid class inheritance trees often leads to fragile architectures. A single change in a base parent class can break dozens of tightly-coupled child subclasses unexpectedly.

Modern software design principles favor Loose Coupling and Strong Cohesion. Software architects follow a fundamental design motto: "Favor Object Composition over Class Inheritance."

In this lesson, we will explore three advanced structural paradigms in Python:

  • Composition ("Has-A" Relationships): Building complex systems by combining smaller, independent component objects rather than extending deep class hierarchies.
  • Abstract Base Classes (ABCs): Enforcing formal interface contracts at runtime using Python's built-in abc module.
  • Protocols (`typing.Protocol`): Implementing modern **Structural Subtyping** (static duck typing) without requiring explicit inheritance.

2. Composition vs Inheritance: "Has-A" vs "Is-A"

1. The Inheritance Pitfall ("Is-A" Relationship)

Inheritance models an "Is-A" relationship. For instance, a SavingsAccount is a BankAccount. However, using inheritance to share utility behaviors (e.g., making Car inherit from Engine or Logger) creates rigid class coupling where child classes are forced to inherit unnecessary internal methods.

2. The Composition Solution ("Has-A" Relationship)

Composition models a "Has-A" relationship. A Car has an Engine and has a GPS. Instead of deriving from parent classes, a composite class accepts references to independent component objects and delegates specialized work to them.

  • Coupling Level
  • Tight Coupling (Child is tightly bound to Parent implementation details).
  • Loose Coupling (Class interacts with components via clear interfaces).
  • Runtime Flexibility
  • Static (Class hierarchy fixed at compile/definition time).
  • Dynamic (Components can be swapped at runtime via dependency injection).
  • Code Reusability
  • Reuses code via base class extension.
  • Reuses code by assembling independent, specialized modules.
  • Primary Risk
  • Fragile Base Class problem (modifying parent breaks sub-classes).
  • Slightly more initial setup code for delegating calls.
Architectural Dimension Object Inheritance ("Is-A") Object Composition ("Has-A")
COMPOSITION_DEMO.PY
# Component Class 1: Engine Behavior
class Engine:
    def __init__(self, horsepower: int):
        self.horsepower = horsepower

    def start(self) -> str:
        return f"V8 Engine ({self.horsepower} HP) roaring to life!"

# Component Class 2: GPS Navigation Behavior
class GPSNavigation:
    def route_to(self, destination: str) -> str:
        return f"GPS calculating optimal route to '{destination}'..."

# Composite Class: Assembles independent components ("Has-A" Engine & GPS)
class Vehicle:
    def __init__(self, make: str, model: str, engine_obj: Engine, gps_obj: GPSNavigation):
        self.make = make
        self.model = model
        # Storing component references in instance memory
        self.engine = engine_obj
        self.gps = gps_obj

    def drive_to(self, destination: str):
        print(f"=== Operating {self.make} {self.model} ===")
        print(self.engine.start())         # Delegating engine logic
        print(self.gps.route_to(destination)) # Delegating routing logic

# Testing Composition via Dependency Injection
v8_engine = Engine(450)
garmin_gps = GPSNavigation()

my_car = Vehicle("Ford", "Mustang", v8_engine, garmin_gps)
my_car.drive_to("San Francisco")
OUTPUT
=== Operating Ford Mustang ===
V8 Engine (450 HP) roaring to life!
GPS calculating optimal route to 'San Francisco'...

3. Formal Interfaces: Abstract Base Classes (`abc.ABC`)

When designing framework APIs or plugin systems, architects must guarantee that every concrete plugin or database driver implements specific required methods. If a developer forgets to implement a required method in a subclass, errors will occur at runtime when the method is invoked.

To enforce explicit interface contracts at instantiation time, Python provides the `abc` (Abstract Base Class) module.

1. Core Rules of Abstract Base Classes

  • An Abstract Base Class inherits from abc.ABC.
  • Methods that MUST be implemented by subclasses are decorated with @abstractmethod.
  • An Abstract Base Class **CANNOT be instantiated directly**! Attempting base = AbstractClass() raises an immediate TypeError.
  • Any concrete child subclass MUST override and implement ALL @abstractmethod methods. If even one abstract method remains un-implemented, Python prevents subclass instantiation!
ABSTRACT_CLASS_DEMO.PY
from abc import ABC, abstractmethod

# Abstract Base Class Interface Blueprint
class BaseRepository(ABC):

    @abstractmethod
    def save(self, record_id: str, data: dict) -> bool:
        """Abstract method: Must be implemented by concrete subclasses."""
        pass

    @abstractmethod
    def fetch(self, record_id: str) -> dict:
        """Abstract method: Must be implemented by concrete subclasses."""
        pass


# Concrete Subclass 1: In-Memory Implementation
class MemoryRepository(BaseRepository):
    def __init__(self):
        self._store = {}

    def save(self, record_id: str, data: dict) -> bool:
        self._store[record_id] = data
        print(f"[MEMORY REPO] Saved record #{record_id}")
        return True

    def fetch(self, record_id: str) -> dict:
        return self._store.get(record_id, {})


# Verifying ABC Instantiation Rules
try:
    # 1. Attempting to instantiate ABC directly fails!
    base_repo = BaseRepository()
except TypeError as err:
    print("Caught Direct ABC Instantiation Error:", err)

# 2. Instantiating valid concrete subclass succeeds cleanly
repo = MemoryRepository()
repo.save("REC_101", {"name": "Alice", "role": "Admin"})
print("Fetched Record:", repo.fetch("REC_101"))
OUTPUT
Caught Direct ABC Instantiation Error: Can't instantiate abstract class BaseRepository with abstract methods fetch, save
[MEMORY REPO] Saved record #REC_101
Fetched Record: {'name': 'Alice', 'role': 'Admin'}

4. Modern Structural Subtyping: Protocols (`typing.Protocol`)

Abstract Base Classes rely on Nominal Subtyping: a class MUST explicitly inherit from ABC (e.g., class MyRepo(BaseRepository):). This requires classes to share an explicit inheritance tree.

Python 3.8+ introduced Protocols (`typing.Protocol`), which bring Structural Subtyping (known as static duck typing) to Python's type system!

How Protocols Differ from ABCs: With Protocols, a class does NOT need to inherit from a base Protocol class! If a class defines matching method names and type signatures defined in a Protocol, Python's type checkers and runtime matchers treat it as a valid compliant implementation automatically!
PROTOCOL_DEMO.PY
from typing import Protocol, runtime_checkable

# Defining a Structural Protocol Interface
@runtime_checkable  # Allows isinstance() checks on the Protocol at runtime!
class PrintablePayload(Protocol):
    def format_payload(self) -> str:
        """Any class implementing format_payload() satisfies this Protocol implicitly!"""
        ...

# Class A: Unrelated standalone class (No inheritance!)
class InvoiceDocument:
    def __init__(self, invoice_id: str, amount: float):
        self.invoice_id = invoice_id
        self.amount = amount

    def format_payload(self) -> str:
        return f"INVOICE #{self.invoice_id}: Total Amount ${self.amount:,.2f}"

# Class B: Unrelated standalone class
class AuditLogEntry:
    def __init__(self, action: str, timestamp: str):
        self.action = action
        self.timestamp = timestamp

    def format_payload(self) -> str:
        return f"LOG [{self.timestamp}]: Action '{self.action}' executed."

# Polymorphic pipeline function accepting any PrintablePayload Protocol object
def render_printable_stream(items: list[PrintablePayload]):
    print("=== Streaming Protocol-Compliant Payloads ===")
    for item in items:
        # Runtime protocol compatibility check enabled by @runtime_checkable
        if isinstance(item, PrintablePayload):
            print("Rendered Output:", item.format_payload())

inv = InvoiceDocument("INV_9901", 1250.00)
log = AuditLogEntry("USER_LOGIN", "2026-07-26 19:30:00")

render_printable_stream([inv, log])
OUTPUT
=== Streaming Protocol-Compliant Payloads ===
Rendered Output: INVOICE #INV_9901: Total Amount $1,250.00
Rendered Output: LOG [2026-07-26 19:30:00]: Action 'USER_LOGIN' executed.

5. Combining Abstract Interfaces & Protocols with `match-case`

Python 3.10+ Class Pattern Matching integrates seamlessly with Abstract Classes and Runtime-Checkable Protocols. Structural dispatchers inspect instance compatibility against abstract interface boundaries dynamically.

MATCH_CASE_INTERFACE_DISPATCHER.PY
from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable

@runtime_checkable
class Encryptable(Protocol):
    def encrypt(self) -> str: ...

class BaseMessage(ABC):
    def __init__(self, sender: str):
        self.sender = sender

class SecureTextMessage(BaseMessage):
    def __init__(self, sender: str, raw_text: str):
        super().__init__(sender)
        self.raw_text = raw_text

    def encrypt(self) -> str:
        return f"SECURE_HASH({self.raw_text[::-1]})"

def dispatch_interface_payload(payload_obj: object):
    """
    Routes objects based on Abstract Classes and Protocols using Structural Pattern Matching.
    Demonstrates evaluating interface compliance dynamically.
    """
    match payload_obj:
        case Encryptable() as enc_obj if isinstance(enc_obj, BaseMessage):
            return f"MATCH_BOTH: BaseMessage from '{enc_obj.sender}' encrypted -> {enc_obj.encrypt()}"
            
        case Encryptable() as enc_obj:
            return f"MATCH_PROTOCOL_ONLY: Encrypted string -> {enc_obj.encrypt()}"
            
        case BaseMessage(sender=src):
            return f"MATCH_BASE_ONLY: Unencrypted BaseMessage from '{src}'"
            
        case _:
            return "ERROR: Unrecognized payload schema."

msg1 = SecureTextMessage("Alice", "TopSecret123")

print(dispatch_interface_payload(msg1))
OUTPUT
MATCH_BOTH: BaseMessage from 'Alice' encrypted -> SECURE_HASH(321terceSTpoT)

6. Frequently Asked Interview Questions with Answers

Q1: What is the core difference between Composition ("Has-A") and Inheritance ("Is-A")?
Answer: Inheritance ("Is-A") extends a parent class hierarchy directly, creating tight coupling between base and child classes. Composition ("Has-A") combines independent component objects as member attributes inside a composite class, promoting loose coupling, high cohesion, and dynamic runtime flexibility.
Q2: What happens if a concrete subclass fails to implement an `@abstractmethod` defined in its parent `ABC`?
Answer: Python prevents instantiation of the child subclass! Attempting to create an instance raises an immediate TypeError stating that the class cannot be instantiated with abstract methods un-implemented.
Q3: How does a `typing.Protocol` differ from an Abstract Base Class (`abc.ABC`) in Python?
Answer: ABCs rely on **Nominal Subtyping** (classes must explicitly inherit from ABC). Protocols rely on **Structural Subtyping** (static duck typing); any class implementing the methods and signatures specified in a Protocol satisfies the Protocol interface automatically without needing explicit inheritance.
Q4: Why is `@runtime_checkable` required when using `typing.Protocol` with `isinstance()`?
Answer: By default, Protocols exist purely for static type checkers (like MyPy) and strip type information at runtime. Adding the @runtime_checkable decorator allows Python's runtime engine to perform isinstance(obj, Protocol) checks by evaluating if the object contains required methods.
Q5: Can an Abstract Base Class (`ABC`) define concrete methods alongside abstract methods?
Answer: Yes! ABCs frequently contain concrete helper methods that provide shared default behavior for all child subclasses, alongside `@abstractmethod` definitions that enforce subclass specialization.

7. Homework & Practical Assignments

Task 1: Composition-Based Computer System Builder

Create a script named composition_computer_task.py inside your lesson_37 folder:

  • Define component class Processor(brand: str, cores: int) with method get_specs().
  • Define component class StorageDrive(capacity_gb: int) with method get_specs().
  • Define composite class Computer(model: str, cpu: Processor, disk: StorageDrive).
  • Add method display_system_summary() on Computer delegating calls to component objects.
  • Instantiate components and composite objects, then print formatted summaries using f-strings.

Task 2: Abstract Payment Repository with `match-case` Dispatcher

Create a script named abstract_repo_task.py:

  • Import ABC and abstractmethod from abc.
  • Define ABC BasePaymentProcessor(ABC) with abstract method execute_transaction(amount: float) -> bool.
  • Create concrete subclasses StripeProcessor and PayPalProcessor implementing transaction logic.
  • Write a dispatcher function using match-case Class Pattern Matching to inspect concrete processor objects and execute payments safely.
  • Test function with instances of both concrete processors and catch direct ABC instantiation errors.

Task 3: Master Capstone Review Project — Modular Enterprise Architecture OS (`enterprise_arch_os.py`)

Create a script named enterprise_arch_os.py inside your lesson_37 folder. This assignment tests and integrates **ALL concepts learned across Lessons 1 through 37**.

Project Architectural Requirements Specification:

  1. Composition, ABCs & Protocols Architecture (Lesson 37):
    • Define Abstract Base Class BaseStorageEngine(ABC) with abstract methods save_payload() and load_payload().
    • Define a runtime-checkable Protocol AuditLoggerProtocol(Protocol) specifying log_audit_event().
    • Build concrete composite class EnterpriseServiceOS that composes a storage engine component and a protocol-compliant audit logger ("Has-A" relationship).
  2. Data Classes, Magic Methods & Inheritance (Lessons 34-36):
    • Define a Data Class ServiceRecord(record_id, title, price, status) with overloaded __lt__ comparison for sorting.
    • Protect internal properties using @property getters/setters with validation logic.
  3. Advanced OOP Methods & Constructors (Lessons 32-33):
    • Implement class method factory from_json_string(cls, json_str) to construct instances.
    • Implement static utility methods for record ID validation. Track total active instances via class attributes.
  4. Observability & Diagnostics (Lesson 31):
    • Configure a logging FileHandler writing diagnostic logs to enterprise_arch.log.
    • Incorporate developer assertions (assert) verifying component invariant non-nullability.
  5. Context Managers & Fault Tolerance (Lessons 29-30):
    • Build a class-based context manager SystemVaultLock(db_path) managing persistent storage locks.
    • Define custom exception classes: ArchitectureError(Exception), StorageFailureError(ArchitectureError). Wrap executions in 4-block try-except-else-finally suites.
  6. CSV & JSON Persistence Layer (Lessons 27-28):
    • Maintain storage directory arch_vault/ using pathlib.Path.
    • Persist JSON database maps to records_db.json and export audit logs to system_audit.csv.
  7. Nested Collections, Comprehensions & Data Structures (Lessons 21-26):
    • Maintain an in-memory database mapping record IDs to Data Class Instances.
    • Use List and Dictionary Comprehensions to clean, filter, and map record streams dynamically.
  8. Advanced Function Parameters & Scope (Lessons 15-17):
    • Structure logic into pure modular functions with type hints, docstrings, early return Guard Clauses, and *args / **kwargs logging.
  9. Indefinite & Definite Loops (Lessons 13-14):
    • Run the interactive CLI interface inside a continuous while True menu loop.
    • Iterate through output reports using for loops with enumerate() and .items().
  10. Pattern Matching CLI Dispatcher (Lesson 12):
    • Process user CLI command tokens inside a match-case block:
      • case ["POST", rid, title, price_str] → Instantiate ServiceRecord, validate components, and store using composite storage engine.
      • case ["FETCH", rid] → Load record safely and output formatted presentation.
      • case ["AUDIT", "COMPONENTS"] → Route storage engine objects using Class Pattern Matching and Protocol type checks.
      • case ["EXPORT", "CSV"] → Export active records to CSV safely.
      • case ["EXIT" | "QUIT"] → Terminate session safely using a sentinel flag.
      • case _ → Output command error message.
  11. Conditionals, Formatting & Foundations (Lessons 1-11):
    • Sanitize all inputs using .strip() and .upper().
    • Format tabular reports using f-strings with field width alignment specifiers (:<15, :>10) and currency formatting (:,.2f).

8. File & Workspace Directory Structure

Standard Course Workspace Layout:

Ensure all exercise files are stored inside their corresponding lesson directories:

python_mastery_course/
│
├── lesson_01/ ... lesson_36/
│
└── lesson_37/
    ├── composition_demo.py
    ├── abstract_class_demo.py
    ├── protocol_demo.py
    ├── match_case_interface_dispatcher.py
    ├── composition_computer_task.py   <-- (Task 1)
    ├── abstract_repo_task.py          <-- (Task 2)
    └── enterprise_arch_os.py          <-- (Task 3: Master Review Capstone)
        

9. What We Will Learn Next

Next Up: Lesson 38 — Project: Object-Oriented Inventory System

Congratulations on completing the entire core Object-Oriented Programming (OOP) module of the course! It is time to consolidate everything into our second major portfolio capstone project.

In the next lesson, we will cover:

  • Architecting a production-grade Object-Oriented Inventory Management System.
  • Integrating Composition, Abstract Classes, Data Classes, and Encapsulated Properties into a unified application.
  • Implementing persistent file backing (JSON/CSV) with custom Context Managers.
  • Applying match-case pattern dispatchers to handle CLI user commands.
  • Writing diagnostic logging and custom exception handlers for robust fault tolerance.

📝 Live Lesson Practice

HTML/CSS JavaScript Python C++ C PHP
⌨️ Practice Inputs (लाइव इनपुट भरें) (खाली होने पर RED, भरने पर GREEN underline)
💻 Code Editor (Monaco VS Code Engine)
👀 Live Preview
Address Contact

+91 7877547686

E-mail

onlinecbtportal@gmail.com

Helpline Number

+91 7877547686


Click To Download
Get it on Google Play

ऐप डाउनलोड करने
के लिए Google Play पर
उपलब्ध है

WhatsApp Chat