Loading episodes…
0:00 0:00

Beyond Factory Method: A Visual Guide to the Abstract Factory Pattern in Python

00:00
BACK TO HOME

Beyond Factory Method: A Visual Guide to the Abstract Factory Pattern in Python

10xTeam December 15, 2025 13 min read

The Abstract Factory pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. Think of it as a “factory of factories,” where a master factory delegates object creation to other, more specialized factories.

This guide will walk you through the limitations of simpler patterns like the Factory Method and show how the Abstract Factory pattern solves a more complex class of problems, especially when consistency among created objects is critical.

The Problem: When Factory Method Isn’t Enough

Imagine our logistics application, which initially used a Factory Method to create transport objects. It worked perfectly: a RoadLogistics factory created Trucks, and a SeaLogistics factory created Ships.

But now, the business requirements have expanded:

  1. Every transport needs a corresponding delivery receipt.
  2. Every transport must be assigned a specific tracking device.

Crucially, these objects are related. A lightweight bike delivery should generate a DigitalReceipt and use a GPSTracker, while a heavy-duty truck requires an OfficialReceipt and a SatelliteTracker.

If we try to cram this logic into our existing Factory Method, it quickly becomes a mess. The creation method would be bloated with conditional logic to handle not just the transport, but also the receipt and the tracker.

# A messy creator class that violates the Single Responsibility Principle
class LogisticsManager:
    def create_delivery_bundle(self, package_weight: int):
        if package_weight <= 3:
            # Bike delivery family
            transport = Bike()
            receipt = DigitalReceipt()
            tracker = GPSTracker()
            return transport, receipt, tracker
        elif 3 < package_weight <= 50:
            # Motor delivery family
            transport = Motor()
            receipt = PaperReceipt()
            tracker = CameraTracker()
            return transport, receipt, tracker
        else:
            # Truck delivery family
            transport = Truck()
            receipt = OfficialReceipt()
            tracker = SatelliteTracker()
            return transport, receipt, tracker

# The client is tightly coupled to this complex conditional logic.

This approach has several flaws:

  • Violates Single Responsibility Principle: The create_delivery_bundle method is doing too much.
  • Violates Open/Closed Principle: Adding a new delivery type (e.g., “Drone”) requires modifying this giant if/elif/else block.
  • Tight Coupling: The client is aware of all concrete product classes, and the creation logic is centralized and rigid.

This is where the Abstract Factory pattern shines. It lets us group object creation into families and ensures that the objects created by a specific factory are compatible.

The Solution: The Abstract Factory Pattern

The Abstract Factory pattern introduces a new layer of abstraction: an interface for creating the entire family of products. The client interacts with this interface, completely decoupled from the concrete classes.

Here are the key components:

  1. Abstract Factory: An interface (in Python, an Abstract Base Class) that declares a set of methods for creating abstract products. E.g., ILogisticsFactory with create_transport(), create_receipt(), and create_tracker().
  2. Concrete Factories: Classes that implement the Abstract Factory interface to create a specific family of products. E.g., BikeLogisticsFactory creates Bike, DigitalReceipt, and GPSTracker.
  3. Abstract Products: Interfaces for a type of product. E.g., ITransport, IReceipt, ITracker.
  4. Concrete Products: The actual objects being created, which implement the Abstract Product interfaces. E.g., Bike implements ITransport.
  5. Client: Uses only the Abstract Factory and Abstract Product interfaces.

Let’s visualize the entire structure with a class diagram.

classDiagram
    direction LR

    class Client {
        - factory: ILogisticsFactory
        + create_shipment()
    }

    class ILogisticsFactory {
        <<interface>>
        +create_transport(): ITransport
        +create_receipt(): IReceipt
        +create_tracker(): ITracker
    }

    class BikeLogisticsFactory {
        +create_transport(): ITransport
        +create_receipt(): IReceipt
        +create_tracker(): ITracker
    }
    class TruckLogisticsFactory {
        +create_transport(): ITransport
        +create_receipt(): IReceipt
        +create_tracker(): ITracker
    }

    class ITransport {<<interface>>}
    class IReceipt {<<interface>>}
    class ITracker {<<interface>>}

    class Bike {+deliver()}
    class Truck {+deliver()}
    class DigitalReceipt {+print()}
    class OfficialReceipt {+print()}
    class GPSTracker {+track()}
    class SatelliteTracker {+track()}

    Client ..> ILogisticsFactory
    BikeLogisticsFactory --|> ILogisticsFactory
    TruckLogisticsFactory --|> ILogisticsFactory

    BikeLogisticsFactory ..> Bike
    BikeLogisticsFactory ..> DigitalReceipt
    BikeLogisticsFactory ..> GPSTracker

    TruckLogisticsFactory ..> Truck
    TruckLogisticsFactory ..> OfficialReceipt
    TruckLogisticsFactory ..> SatelliteTracker

    Bike --|> ITransport
    Truck --|> ITransport
    DigitalReceipt --|> IReceipt
    OfficialReceipt --|> IReceipt
    GPSTracker --|> ITracker
    SatelliteTracker --|> ITracker

Step-by-Step Implementation in Python

Let’s build this system from the ground up. Here’s our project structure:

logistics_project/
├── products/
│   ├── __init__.py
│   ├── transports.py
│   ├── receipts.py
│   └── trackers.py
├── factories/
│   ├── __init__.py
│   ├── factory_interface.py
│   └── concrete_factories.py
└── main.py

Step 1: Define the Abstract Products

These are the interfaces for each object in our product family. We’ll use Python’s abc module.

products/transports.py

from abc import ABC, abstractmethod

class ITransport(ABC):
    @abstractmethod
    def deliver(self) -> str:
        pass

class Bike(ITransport):
    def deliver(self) -> str:
        return "Delivering by Bike."

class Motor(ITransport):
    def deliver(self) -> str:
        return "Delivering by Motor."

class Truck(ITransport):
    def deliver(self) -> str:
        return "Delivering by Truck."

products/receipts.py

from abc import ABC, abstractmethod

class IReceipt(ABC):
    @abstractmethod
    def print(self) -> str:
        pass

class DigitalReceipt(IReceipt):
    def print(self) -> str:
        return "Printing Digital Receipt."

class PaperReceipt(IReceipt):
    def print(self) -> str:
        return "Printing Paper Receipt."

class OfficialReceipt(IReceipt):
    def print(self) -> str:
        return "Printing Official Receipt."

products/trackers.py

from abc import ABC, abstractmethod

class ITracker(ABC):
    @abstractmethod
    def track(self) -> str:
        pass

class GPSTracker(ITracker):
    def track(self) -> str:
        return "Tracking with GPS."

class CameraTracker(ITracker):
    def track(self) -> str:
        return "Tracking with Road Cameras."

class SatelliteTracker(ITracker):
    def track(self) -> str:
        return "Tracking with Satellite."

Step 2: Define the Abstract Factory

This interface defines the contract for all our concrete factories. It has a creation method for each abstract product.

factories/factory_interface.py

from abc import ABC, abstractmethod
from products.transports import ITransport
from products.receipts import IReceipt
from products.trackers import ITracker

class ILogisticsFactory(ABC):
    @abstractmethod
    def create_transport(self) -> ITransport:
        pass

    @abstractmethod
    def create_receipt(self) -> IReceipt:
        pass

    @abstractmethod
    def create_tracker(self) -> ITracker:
        pass

Step 3: Implement the Concrete Factories

Each concrete factory implements the ILogisticsFactory interface to produce a consistent family of objects.

factories/concrete_factories.py

from factories.factory_interface import ILogisticsFactory
from products.transports import ITransport, Bike, Motor, Truck
from products.receipts import IReceipt, DigitalReceipt, PaperReceipt, OfficialReceipt
from products.trackers import ITracker, GPSTracker, CameraTracker, SatelliteTracker

class BikeLogisticsFactory(ILogisticsFactory):
    """Factory for creating a bike-based delivery family."""
    def create_transport(self) -> ITransport:
        return Bike()

    def create_receipt(self) -> IReceipt:
        return DigitalReceipt()

    def create_tracker(self) -> ITracker:
        return GPSTracker()

class MotorLogisticsFactory(ILogisticsFactory):
    """Factory for creating a motor-based delivery family."""
    def create_transport(self) -> ITransport:
        return Motor()

    def create_receipt(self) -> IReceipt:
        return PaperReceipt()

    def create_tracker(self) -> ITracker:
        return CameraTracker()

class TruckLogisticsFactory(ILogisticsFactory):
    """Factory for creating a truck-based delivery family."""
    def create_transport(self) -> ITransport:
        return Truck()

    def create_receipt(self) -> IReceipt:
        return OfficialReceipt()

    def create_tracker(self) -> ITracker:
        return SatelliteTracker()

[!TIP] Notice how each factory (BikeLogisticsFactory, TruckLogisticsFactory) is responsible for creating a full set of compatible products. This is the core strength of the pattern: it guarantees consistency within a product family.

Step 4: The Client Code

The client code now becomes incredibly clean. It decides which factory to use based on some criteria (like package weight), but after that, it only interacts with the abstract interfaces.

main.py

from factories.factory_interface import ILogisticsFactory
from factories.concrete_factories import BikeLogisticsFactory, MotorLogisticsFactory, TruckLogisticsFactory

def client_code(factory: ILogisticsFactory) -> None:
    """
    The client code works with factories and products only through abstract
    types: ILogisticsFactory, ITransport, etc.
    """
    transport = factory.create_transport()
    receipt = factory.create_receipt()
    tracker = factory.create_tracker()

    print(transport.deliver())
    print(receipt.print())
    print(tracker.track())

if __name__ == "__main__":
    package_weight = 11  # Example weight

    print(f"--- Preparing shipment for a package of {package_weight}kg ---")

    # The application selects the appropriate factory at runtime.
    if package_weight <= 3:
        factory_to_use = BikeLogisticsFactory()
    elif 3 < package_weight <= 50:
        factory_to_use = MotorLogisticsFactory()
    else:
        factory_to_use = TruckLogisticsFactory()

    client_code(factory_to_use)

Running this with package_weight = 11 would produce:

--- Preparing shipment for a package of 11kg ---
Delivering by Truck.
Printing Official Receipt.
Tracking with Satellite.

Visualizing the Refactoring

Let’s compare the client logic before and after applying the Abstract Factory pattern. The improvement in clarity and decoupling is immediately obvious.

# --- BEFORE: Messy, coupled client ---
- class LogisticsManager:
-     def create_delivery_bundle(self, package_weight: int):
-         if package_weight <= 3:
-             transport = Bike()
-             receipt = DigitalReceipt()
-             # ... and so on for all products
-         elif 3 < package_weight <= 50:
-             transport = Motor()
-             receipt = PaperReceipt()
-             # ...
-         else:
-             transport = Truck()
-             receipt = OfficialReceipt()
-             # ...
-         # Client has to know about Bike, DigitalReceipt, etc.

# --- AFTER: Clean, decoupled client ---
+ def client_code(factory: ILogisticsFactory):
+     # Client only knows about abstract interfaces
+     transport = factory.create_transport()
+     receipt = factory.create_receipt()
+     tracker = factory.create_tracker()
+     print(transport.deliver())
+     # ...
+
+ # Configuration code selects the factory
+ if package_weight <= 3:
+     factory_to_use = BikeLogisticsFactory()
+ else:
+     factory_to_use = TruckLogisticsFactory()
+
+ client_code(factory_to_use)

When to Use the Abstract Factory Pattern

This pattern is powerful, but it’s not a silver bullet. Here’s a quick summary of its pros and cons.

mindmap
  root((Abstract Factory))
    Pros
      Guarantees product compatibility
      Promotes loose coupling (client vs. concrete products)
      Follows Single Responsibility Principle
      Follows Open/Closed Principle (for adding new families)
    Cons
      Increases complexity (many new classes/interfaces)
      Hard to add new product types (requires changing the abstract factory)

Use the Abstract Factory pattern when:

  • Your system needs to be independent of how its products are created, composed, and represented.
  • You need to create families of related objects that are designed to be used together.
  • You want to provide a class library of products, revealing only their interfaces, not their implementations.

[!WARNING] The biggest drawback of the Abstract Factory pattern is the difficulty of adding new kinds of products. If we wanted to add an IPackingSlip to our logistics family, we would have to modify the ILogisticsFactory interface and all of its concrete implementations. This violates the Open/Closed Principle with respect to product types.

Quiz Yourself: Abstract Factory vs. Factory Method

Question: What is the primary difference in intent between the Abstract Factory and Factory Method patterns?

Answer:

  • Factory Method is concerned with creating a single object. Subclasses decide which concrete class to instantiate. It uses inheritance to delegate creation.
  • Abstract Factory is concerned with creating a family of related objects. It provides an interface for creating a whole set of products. It uses object composition (the client holds a factory object) to delegate creation.

Conclusion

The Abstract Factory pattern is an essential tool for managing complexity in systems that create multiple, interdependent objects. By grouping object creation into distinct families and hiding the implementation details behind abstract interfaces, it allows you to build scalable, maintainable, and highly decoupled applications. While it introduces more classes, the resulting consistency and separation of concerns are invaluable for large, long-lived projects.


Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?