Skip to content

Abstract Base Classes (ABC)

Abstract Base Classes define interfaces that subclasses must implement. They provide a way to enforce contracts and enable polymorphism with explicit structure.

from abc import ABC, abstractmethod

Why Use ABCs?

The Problem: Duck Typing Limitations

Duck typing ("if it quacks like a duck...") is flexible but has issues:

# Duck typing - no enforcement
class Duck:
    def quack(self):
        print("Quack!")

class Person:
    def quack(self):
        print("I'm pretending to be a duck!")

# Both work, but Person isn't really a duck
def make_it_quack(thing):
    thing.quack()  # Works for both, but is Person valid?

The Solution: ABCs

ABCs explicitly define what methods a class must have:

from abc import ABC, abstractmethod

class Bird(ABC):
    @abstractmethod
    def fly(self):
        """All birds must implement fly()"""
        pass

    @abstractmethod
    def make_sound(self):
        """All birds must implement make_sound()"""
        pass

# Cannot instantiate abstract class
# bird = Bird()  # TypeError!

class Sparrow(Bird):
    def fly(self):
        return "Sparrow flies short distances"

    def make_sound(self):
        return "Chirp!"

# Can instantiate concrete class
sparrow = Sparrow()  # ✓ OK

Basic ABC Definition

Using ABC Base Class

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        """Calculate the area of the shape."""
        pass

    @abstractmethod
    def perimeter(self):
        """Calculate the perimeter of the shape."""
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        from math import pi
        return pi * self.radius ** 2

    def perimeter(self):
        from math import pi
        return 2 * pi * self.radius

Using ABCMeta Metaclass

Alternative syntax (equivalent to inheriting from ABC):

from abc import ABCMeta, abstractmethod

class Shape(metaclass=ABCMeta):
    @abstractmethod
    def area(self):
        pass

Abstract Methods

@abstractmethod

Must be implemented by all concrete subclasses:

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def connect(self):
        """Establish connection to database."""
        pass

    @abstractmethod
    def execute(self, query):
        """Execute a query."""
        pass

    @abstractmethod
    def close(self):
        """Close the connection."""
        pass

Abstract Properties

from abc import ABC, abstractmethod

class Vehicle(ABC):
    @property
    @abstractmethod
    def max_speed(self):
        """Maximum speed in km/h."""
        pass

    @property
    @abstractmethod
    def fuel_type(self):
        """Type of fuel used."""
        pass

class Car(Vehicle):
    @property
    def max_speed(self):
        return 200

    @property
    def fuel_type(self):
        return "gasoline"

Abstract Class Methods

from abc import ABC, abstractmethod

class Serializable(ABC):
    @classmethod
    @abstractmethod
    def from_json(cls, json_str):
        """Create instance from JSON string."""
        pass

    @abstractmethod
    def to_json(self):
        """Convert instance to JSON string."""
        pass

Abstract Static Methods

from abc import ABC, abstractmethod

class Validator(ABC):
    @staticmethod
    @abstractmethod
    def validate(value):
        """Validate the given value."""
        pass

Concrete Methods in ABCs

ABCs can have concrete (implemented) methods:

from abc import ABC, abstractmethod

class Animal(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def speak(self):
        """Must be implemented by subclass."""
        pass

    # Concrete method - inherited as-is
    def introduce(self):
        return f"I am {self.name} and I say: {self.speak()}"

class Dog(Animal):
    def speak(self):
        return "Woof!"

dog = Dog("Buddy")
print(dog.introduce())  # "I am Buddy and I say: Woof!"

Default Implementation

Abstract methods can have default implementations:

from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def log(self, message):
        """Log a message. Subclasses should call super()."""
        print(f"[{self.__class__.__name__}] {message}")

class FileLogger(Logger):
    def log(self, message):
        super().log(message)  # Call default implementation
        # Additional file-specific logging
        with open("log.txt", "a") as f:
            f.write(message + "\n")

Checking Implementation

isinstance() and issubclass()

from abc import ABC, abstractmethod

class Drawable(ABC):
    @abstractmethod
    def draw(self):
        pass

class Circle(Drawable):
    def draw(self):
        return "Drawing circle"

circle = Circle()

print(isinstance(circle, Drawable))  # True
print(issubclass(Circle, Drawable))  # True

subclasshook

Customize isinstance/issubclass behavior:

from abc import ABC, abstractmethod

class Iterable(ABC):
    @abstractmethod
    def __iter__(self):
        pass

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Iterable:
            if hasattr(C, '__iter__'):
                return True
        return NotImplemented

# Now any class with __iter__ is considered Iterable
class MyContainer:
    def __iter__(self):
        return iter([1, 2, 3])

print(isinstance(MyContainer(), Iterable))  # True

Built-in ABCs

Python's collections.abc module provides many useful ABCs:

from collections.abc import (
    Iterable,      # Has __iter__
    Iterator,      # Has __iter__ and __next__
    Sequence,      # Has __getitem__ and __len__
    MutableSequence,  # Sequence + __setitem__, __delitem__, insert
    Mapping,       # Has __getitem__, __iter__, __len__
    MutableMapping,   # Mapping + __setitem__, __delitem__
    Set,           # Has __contains__, __iter__, __len__
    Callable,      # Has __call__
    Hashable,      # Has __hash__
)

# Check if something is iterable
from collections.abc import Iterable
print(isinstance([1, 2, 3], Iterable))  # True
print(isinstance(42, Iterable))          # False

Implementing Collection ABCs

from collections.abc import Sequence

class MyList(Sequence):
    def __init__(self, data):
        self._data = list(data)

    def __getitem__(self, index):
        return self._data[index]

    def __len__(self):
        return len(self._data)

    # Sequence provides: __contains__, __iter__, __reversed__,
    # index(), count() for free!

ml = MyList([1, 2, 3, 4, 5])
print(3 in ml)      # True (uses inherited __contains__)
print(ml.count(3))  # 1 (uses inherited count())

Practical Examples

Plugin System

from abc import ABC, abstractmethod

class Plugin(ABC):
    @property
    @abstractmethod
    def name(self):
        """Plugin name."""
        pass

    @abstractmethod
    def execute(self, data):
        """Process data."""
        pass

    def __repr__(self):
        return f"<Plugin: {self.name}>"

class UppercasePlugin(Plugin):
    @property
    def name(self):
        return "uppercase"

    def execute(self, data):
        return data.upper()

class ReversePlugin(Plugin):
    @property
    def name(self):
        return "reverse"

    def execute(self, data):
        return data[::-1]

# Plugin manager
class PluginManager:
    def __init__(self):
        self.plugins = {}

    def register(self, plugin: Plugin):
        self.plugins[plugin.name] = plugin

    def process(self, name, data):
        return self.plugins[name].execute(data)

Repository Pattern

from abc import ABC, abstractmethod
from typing import List, Optional

class Repository(ABC):
    @abstractmethod
    def get(self, id: int) -> Optional[dict]:
        pass

    @abstractmethod
    def get_all(self) -> List[dict]:
        pass

    @abstractmethod
    def add(self, entity: dict) -> int:
        pass

    @abstractmethod
    def update(self, id: int, entity: dict) -> bool:
        pass

    @abstractmethod
    def delete(self, id: int) -> bool:
        pass

class InMemoryRepository(Repository):
    def __init__(self):
        self._data = {}
        self._next_id = 1

    def get(self, id: int) -> Optional[dict]:
        return self._data.get(id)

    def get_all(self) -> List[dict]:
        return list(self._data.values())

    def add(self, entity: dict) -> int:
        id = self._next_id
        self._data[id] = {**entity, 'id': id}
        self._next_id += 1
        return id

    def update(self, id: int, entity: dict) -> bool:
        if id in self._data:
            self._data[id] = {**entity, 'id': id}
            return True
        return False

    def delete(self, id: int) -> bool:
        if id in self._data:
            del self._data[id]
            return True
        return False

Strategy Pattern

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount: float) -> str:
        pass

class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number: str):
        self.card_number = card_number

    def pay(self, amount: float) -> str:
        return f"Paid ${amount} with credit card {self.card_number[-4:]}"

class PayPalPayment(PaymentStrategy):
    def __init__(self, email: str):
        self.email = email

    def pay(self, amount: float) -> str:
        return f"Paid ${amount} via PayPal ({self.email})"

class ShoppingCart:
    def __init__(self):
        self.items = []
        self.payment_strategy: PaymentStrategy = None

    def set_payment(self, strategy: PaymentStrategy):
        self.payment_strategy = strategy

    def checkout(self):
        total = sum(item['price'] for item in self.items)
        return self.payment_strategy.pay(total)

ABC vs Protocol

Feature ABC Protocol (typing)
Type Nominal (explicit inheritance) Structural (implicit)
Runtime check isinstance() works Requires @runtime_checkable
Inheritance Required Not required
Use case Enforce implementation Type hints, duck typing
from abc import ABC, abstractmethod
from typing import Protocol

# ABC - must inherit
class DrawableABC(ABC):
    @abstractmethod
    def draw(self): pass

class Circle(DrawableABC):  # Must inherit
    def draw(self): return "circle"

# Protocol - just implement method
class DrawableProtocol(Protocol):
    def draw(self) -> str: ...

class Square:  # No inheritance needed
    def draw(self): return "square"

# Square is compatible with DrawableProtocol
def render(shape: DrawableProtocol):
    print(shape.draw())

render(Square())  # Works!

Summary

Feature Syntax
Define ABC class MyABC(ABC):
Abstract method @abstractmethod
Abstract property @property + @abstractmethod
Abstract classmethod @classmethod + @abstractmethod
Concrete method Regular method in ABC
Check instance isinstance(obj, MyABC)

Key Takeaways:

  • ABCs define interfaces that subclasses must implement
  • Cannot instantiate a class with unimplemented abstract methods
  • Use @abstractmethod decorator for required methods
  • ABCs can have concrete methods with shared implementation
  • collections.abc provides useful built-in ABCs
  • Choose ABC for strict contracts, Protocol for structural typing

Runnable Example: abstract_base_class_example.py

"""
TUTORIAL: Abstract Base Classes (ABC) - Defining Contracts for Subclasses
==========================================================================

In this tutorial, you'll learn how to use Python's abc module to create
Abstract Base Classes. An ABC is a blueprint that enforces what methods
subclasses MUST implement.

Key concepts:
  1. ABC: A class that cannot be instantiated directly
  2. @abstractmethod: Marks methods that subclasses MUST override
  3. Concrete methods: Can provide default implementation in the ABC
  4. Inheritance: Subclasses must implement all abstract methods

Why use ABC?
  - Enforce a consistent interface across subclasses
  - Prevent accidental incomplete implementations
  - Document what subclasses must do
  - Catch errors at class definition time, not runtime

In this example, we define a Tombola (lottery machine) ABC with:
  - Abstract methods: load() and pick() that subclasses must implement
  - Concrete methods: loaded() and inspect() that use the abstract methods

This demonstrates how abstract methods serve as hooks that concrete methods
depend on, creating a reusable pattern for subclasses.
"""

import abc


# ============ Example 1: Defining an Abstract Base Class ============

if __name__ == "__main__":
    print("=" * 70)
    print("EXAMPLE 1: Creating the Tombola ABC")
    print("=" * 70)

    class Tombola(abc.ABC):
        """Abstract Base Class for a lottery machine (tombola).

        A Tombola must be able to:
        - load() items from an iterable
        - pick() random items and remove them
        - report loaded() status
        - inspect() current contents

        Subclasses MUST implement load() and pick().
        The load() and inspect() methods depend on these abstract methods
        to provide complete functionality.
        """

        @abc.abstractmethod
        def load(self, iterable):
            """Add items from an iterable to the tombola.

            This is an abstract method. Every subclass MUST override it
            with its own implementation. Without it, you cannot instantiate
            the subclass.

            Args:
                iterable: A collection of items to add to the tombola.
            """

        @abc.abstractmethod
        def pick(self):
            """Remove item at random from the tombola, returning it.

            This is an abstract method. Every subclass MUST override it
            with its own implementation.

            Returns:
                A randomly selected item from the tombola.

            Raises:
                LookupError: When the tombola is empty.
            """

        def loaded(self):
            """Return True if there's at least 1 item, False otherwise.

            This is a concrete method. It provides a default implementation
            that uses the abstract method pick() as a hook.

            Note: This method works with ANY subclass that implements
            load() and pick(), making it reusable across all subclasses.
            """
            return bool(self.inspect())

        def inspect(self):
            """Return a sorted tuple with the items currently inside.

            This is a concrete method that uses the abstract methods:
            1. Calls pick() repeatedly to get all items
            2. Calls load() to restore the items after inspection

            This demonstrates how concrete methods can depend on
            abstract methods to provide sophisticated behavior.

            Returns:
                tuple: Sorted tuple of all items in the tombola.
            """
            items = []
            while True:
                try:
                    # Keep picking items until empty
                    items.append(self.pick())
                except LookupError:
                    # Empty - break the loop
                    break

            # Restore the items we removed
            self.load(items)

            # Return sorted tuple of contents
            return tuple(items)


    print(f"\nTombola ABC defined with:")
    print(f"  - Abstract methods: load(), pick()")
    print(f"  - Concrete methods: loaded(), inspect()")
    print(f"\nTrying to instantiate Tombola directly...")

    try:
        tombola = Tombola()
        print(f"  ERROR: This should have failed!")
    except TypeError as e:
        print(f"  Result: TypeError")
        print(f"  Message: {e}")
        print(f"  WHY? ABC classes cannot be instantiated directly")


    # ============ Example 2: Creating a Concrete Subclass ============
    print("\n" + "=" * 70)
    print("EXAMPLE 2: Implementing a concrete subclass (BingoTombola)")
    print("=" * 70)

    import random

    class BingoTombola(Tombola):
        """A Tombola implementation using a list to store items.

        This concrete subclass implements the two abstract methods
        that Tombola requires.
        """

        def __init__(self):
            """Initialize an empty bingo tombola."""
            self._items = []

        def load(self, iterable):
            """Load items from an iterable into the list.

            Args:
                iterable: Items to add to the tombola.
            """
            self._items.extend(iterable)

        def pick(self):
            """Remove and return a random item.

            Returns:
                A random item from _items.

            Raises:
                LookupError: When the list is empty.
            """
            try:
                position = random.randrange(len(self._items))
            except ValueError:
                raise LookupError('pick from empty Tombola') from None
            return self._items.pop(position)

        def __call__(self):
            """Return self for compatibility."""
            return self


    print(f"\nBingoTombola created - a concrete subclass of Tombola")
    print(f"It implements both abstract methods:")
    print(f"  - load(iterable): Stores items in self._items")
    print(f"  - pick(): Removes and returns a random item")

    # Create an instance
    bingo = BingoTombola()
    print(f"\nbingo = BingoTombola()")
    print(f"  Instance created successfully (it's a proper subclass)")


    # ============ Example 3: Using the Concrete Subclass ============
    print("\n" + "=" * 70)
    print("EXAMPLE 3: Using BingoTombola - load and inspect")
    print("=" * 70)

    # Load some items
    numbers = range(1, 7)
    bingo.load(numbers)

    print(f"\nbingo.load(range(1, 7))  # Load numbers 1-6")
    print(f"  Items loaded into tombola")

    # Check if loaded
    print(f"\nbingo.loaded()  # Check if anything is loaded")
    print(f"  Result: {bingo.loaded()}")
    print(f"  WHY? inspect() called pick() and found items")

    # Inspect contents
    contents = bingo.inspect()
    print(f"\nbingo.inspect()  # Get all items without modifying")
    print(f"  Result: {contents}")
    print(f"  Note: Still loaded after inspect (load() restored items)")


    # ============ Example 4: Picking Items and Emptying ============
    print("\n" + "=" * 70)
    print("EXAMPLE 4: Picking items one at a time")
    print("=" * 70)

    bingo2 = BingoTombola()
    bingo2.load(['a', 'b', 'c', 'd', 'e'])

    print(f"\nbingo2.load(['a', 'b', 'c', 'd', 'e'])")
    print(f"bingo2.loaded() = {bingo2.loaded()}")

    print(f"\nPicking 3 items:")
    for i in range(3):
        item = bingo2.pick()
        print(f"  Pick {i+1}: {item}")

    print(f"\nRemaining items via inspect():")
    print(f"  bingo2.inspect() = {bingo2.inspect()}")

    print(f"\nbingo2.loaded() = {bingo2.loaded()}")
    print(f"  Still has items")

    # Pick remaining items
    print(f"\nPicking remaining items until empty:")
    try:
        while True:
            item = bingo2.pick()
            print(f"  Picked: {item}")
    except LookupError as e:
        print(f"  LookupError: {e}")
        print(f"  WHY? No more items in tombola")

    print(f"\nbingo2.loaded() = {bingo2.loaded()}")
    print(f"  Now empty after all picks")


    # ============ Example 5: Why Abstract Methods Matter ============
    print("\n" + "=" * 70)
    print("EXAMPLE 5: Attempted incomplete subclass - this would fail")
    print("=" * 70)

    print(f"\nAttempting to create incomplete subclass:")
    print(f"  class IncompleteTombola(Tombola):")
    print(f"      def load(self, iterable): pass")
    print(f"      # Missing pick() implementation\n")

    try:
        class IncompleteTombola(Tombola):
            def load(self, iterable):
                pass
            # Forgot to implement pick()

        incomplete = IncompleteTombola()
        print(f"  ERROR: This should have failed!")
    except TypeError as e:
        print(f"  Result: TypeError")
        print(f"  Message: {e}")
        print(f"  WHY? Class definition fails if not all abstract methods")
        print(f"       are implemented. This catches errors early!")


    # ============ Example 6: How Concrete Methods Depend on Abstract Methods ============
    print("\n" + "=" * 70)
    print("EXAMPLE 6: Concrete methods using abstract method hooks")
    print("=" * 70)

    print(f"\nThe inspect() method is elegant because:")
    print(f"  1. It's defined once in the ABC")
    print(f"  2. It works for ANY concrete subclass")
    print(f"  3. It 'hooks' into pick() and load()")

    print(f"\nFlow of inspect() for BingoTombola:")
    print(f"  1. Loop: bingo.pick() [abstract hook]")
    print(f"     - Removes items from _items list")
    print(f"     - Raises LookupError when empty")
    print(f"  2. Collect all items into a list")
    print(f"  3. Restore: bingo.load(items) [abstract hook]")
    print(f"     - Puts items back without modification")
    print(f"  4. Return tuple(items)")

    print(f"\nBecause load() and pick() are abstract hooks,")
    print(f"each subclass can have different storage mechanisms")
    print(f"but inspect() works the same way for all of them!")


    # ============ Example 7: Another Subclass - Different Implementation ============
    print("\n" + "=" * 70)
    print("EXAMPLE 7: Alternative subclass with different storage (LotteryTombola)")
    print("=" * 70)

    class LotteryTombola(Tombola):
        """A Tombola implementation using a set instead of a list.

        This shows that different implementations can use different
        internal data structures while satisfying the same interface.
        """

        def __init__(self):
            """Initialize an empty lottery tombola."""
            self._numbers = set()

        def load(self, iterable):
            """Add numbers to the set.

            Args:
                iterable: Numbers to add.
            """
            self._numbers.update(iterable)

        def pick(self):
            """Remove and return a random number.

            Returns:
                A random number from the set.

            Raises:
                LookupError: When the set is empty.
            """
            if not self._numbers:
                raise LookupError('pick from empty LotteryTombola')
            # Convert to list, pick random, remove from set
            value = random.choice(list(self._numbers))
            self._numbers.discard(value)
            return value


    lottery = LotteryTombola()
    lottery.load([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

    print(f"\nlottery = LotteryTombola()")
    print(f"lottery.load(range(1, 11))")

    print(f"\nlottery.loaded() = {lottery.loaded()}")
    print(f"lottery.inspect() = {lottery.inspect()}")

    print(f"\nLotteryTombola uses a different storage mechanism (set)")
    print(f"but provides the SAME interface as BingoTombola!")
    print(f"This is the power of ABC - enforcing a contract.")


    # ============ Example 8: Summary - Why Use ABC? ============
    print("\n" + "=" * 70)
    print("EXAMPLE 8: Summary - Benefits of Abstract Base Classes")
    print("=" * 70)

    print(f"\nBenefits of ABC:")
    print(f"  1. CONTRACT: Forces subclasses to implement required methods")
    print(f"     - TypeError raised at class definition if incomplete")
    print(f"     - Errors caught early, not at runtime")

    print(f"  2. REUSABILITY: Concrete methods work for all subclasses")
    print(f"     - inspect() works the same for BingoTombola and LotteryTombola")
    print(f"     - Hooks (load, pick) allow customization")

    print(f"  3. DOCUMENTATION: Clear what subclasses must do")
    print(f"     - Docstrings on abstract methods guide implementers")
    print(f"     - Self-documenting code")

    print(f"  4. CONSISTENCY: All subclasses have same interface")
    print(f"     - Users know what methods exist")
    print(f"     - Polymorphism works correctly")

    print(f"\nWhen to use ABC:")
    print(f"  - Designing a framework or library")
    print(f"  - Multiple related classes should follow same interface")
    print(f"  - Want to prevent incomplete implementations")
    print(f"  - Need polymorphic behavior")

    print(f"\n" + "=" * 70)