Loading episodes…
0:00 0:00

Mastering the Factory Method in Python: A Visual Guide to Decoupling Your Code

00:00
BACK TO HOME

Mastering the Factory Method in Python: A Visual Guide to Decoupling Your Code

10xTeam December 23, 2025 12 min read

The Factory Method is a creational design pattern that solves a recurring problem in object-oriented programming: how do you create objects when the exact type isn’t known until runtime? It provides a blueprint for creating objects in a parent class but empowers subclasses to modify the type of objects that will be created.

This approach is fundamental to building flexible and scalable systems, allowing you to introduce new types of objects without modifying the core application logic.

The Journey to the Factory Method

To appreciate the Factory Method, let’s understand the problem it solves. Imagine you’re building a logistics system. Initially, you might handle object creation with a simple conditional.

Stage 1: The Simple if/else

If only one part of your application needs to create transport objects, a simple function with if/elif/else is perfectly acceptable.

# product.py
class Truck:
    def deliver(self):
        print("Delivering by land in a truck.")

class Ship:
    def deliver(self):
        print("Delivering by sea in a ship.")

# main.py
def create_transport(transport_type: str):
    if transport_type == "TRUCK":
        return Truck()
    elif transport_type == "SHIP":
        return Ship()
    return None

# Client code
transport = create_transport("TRUCK")
transport.deliver()

This is straightforward. But what happens when multiple parts of your application need this logic? You’d be duplicating that if/elif/else block everywhere, which is a maintenance nightmare.

Stage 2: The Simple Factory

To avoid duplication, you centralize the creation logic into a single class—the Simple Factory.

# simple_factory.py
class TransportFactory:
    def create_transport(self, transport_type: str):
        if transport_type == "TRUCK":
            return Truck()
        elif transport_type == "SHIP":
            return Ship()
        return None

# Client code (now much cleaner)
factory = TransportFactory()
transport = factory.create_transport("SHIP")
transport.deliver()

This is a huge improvement! We’ve centralized creation logic. However, a new business requirement emerges:

Business Requirement: Each transport type now needs unique, complex initialization parameters.

  • A Truck needs a plate_number.
  • A Ship needs a port_code.
  • A Plane needs a flight_number.

Our Simple Factory starts to break down.

# simple_factory.py (The Messy Version)
class TransportFactory:
-   def create_transport(self, transport_type: str):
+   def create_transport(self, transport_type: str, **kwargs):
        if transport_type == "TRUCK":
-           return Truck()
+           # Requires a 'plate_number'
+           if 'plate_number' not in kwargs:
+               raise ValueError("Truck requires a plate_number")
+           return Truck(kwargs['plate_number'])
        elif transport_type == "SHIP":
-           return Ship()
+           # Requires a 'port_code'
+           if 'port_code' not in kwargs:
+               raise ValueError("Ship requires a port_code")
+           return Ship(kwargs['port_code'])
        return None

This factory is now a tangled mess. It violates two key SOLID principles:

  1. Single Responsibility Principle: It knows about every single product and its specific creation details.
  2. Open/Closed Principle: To add a new Plane type, we must modify this factory class directly.

This is where the Factory Method shines.

The Solution: The Factory Method Pattern

The Factory Method delegates the responsibility of instantiation to subclasses. The parent class knows that an object must be created, but the subclasses decide which object to create.

Here is the classic structure:

classDiagram
    class Creator {
        <<abstract>>
        +create_product(): Product
        +some_operation()
    }
    class ConcreteCreatorA {
        +create_product(): Product
    }
    class ConcreteCreatorB {
        +create_product(): Product
    }
    class Product {
        <<interface>>
        +operation()
    }
    class ConcreteProductA {
        +operation()
    }
    class ConcreteProductB {
        +operation()
    }

    Creator --|> Product : creates
    ConcreteCreatorA --|> Creator : inherits
    ConcreteCreatorB --|> Creator : inherits
    ConcreteCreatorA ..> ConcreteProductA : creates
    ConcreteCreatorB ..> ConcreteProductB : creates
    ConcreteProductA --|> Product : implements
    ConcreteProductB --|> Product : implements

Let’s apply this to our logistics system.

Step 1: Define the Product Interface and Concrete Products

First, we define our products. They all share a common interface (ITransport).

# products.py
from abc import ABC, abstractmethod

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

class Truck(ITransport):
    def __init__(self, plate_number: str):
        self._plate_number = plate_number
        print(f"Truck created with plate: {self._plate_number}")

    def deliver(self):
        print(f"Delivering by land with truck {self._plate_number}.")

class Ship(ITransport):
    def __init__(self, port_code: int):
        self._port_code = port_code
        print(f"Ship created for port: {self._port_code}")

    def deliver(self):
        print(f"Delivering by sea from port {self._port_code}.")

class Plane(ITransport):
    def __init__(self, flight_number: str):
        self._flight_number = flight_number
        print(f"Plane created for flight: {self._flight_number}")

    def deliver(self):
        print(f"Delivering by air via flight {self._flight_number}.")

Step 2: Define the Abstract Creator with the Factory Method

Next, we create our abstract Logistics class. It has an abstract create_transport method (the factory method) and a plan_delivery method that uses it.

# creators.py
from abc import ABC, abstractmethod
from products import ITransport

class Logistics(ABC):
    """
    The Creator class declares the factory method that is supposed to return an
    object of a Product class. The Creator's subclasses usually provide the
    implementation of this method.
    """

    @abstractmethod
    def create_transport(self) -> ITransport:
        """This is the factory method."""
        pass

    def plan_delivery(self):
        """
        The Creator's primary responsibility is not creating products. It usually
        contains some core business logic that relies on Product objects, returned
        by the factory method. Subclasses can indirectly change that business logic
        by overriding the factory method and returning a different type of product.
        """
        transport = self.create_transport()
        print("Factory method created a transport.")
        transport.deliver()

[!NOTE] The plan_delivery method is decoupled from the concrete products. It works with any product that conforms to the ITransport interface, thanks to the factory method.

Step 3: Implement the Concrete Creators

Now, we create concrete subclasses. Each one knows exactly how to instantiate its specific product.

# concrete_creators.py
from creators import Logistics
from products import ITransport, Truck, Ship, Plane

class RoadLogistics(Logistics):
    """
    Concrete Creators override the factory method in order to change the
    resulting product's type.
    """
    def __init__(self, plate_number: str):
        self._plate_number = plate_number

    def create_transport(self) -> ITransport:
        return Truck(self._plate_number)

class SeaLogistics(Logistics):
    def __init__(self, port_code: int):
        self._port_code = port_code

    def create_transport(self) -> ITransport:
        return Ship(self._port_code)

class AirLogistics(Logistics):
    def __init__(self, flight_number: str):
        self._flight_number = flight_number

    def create_transport(self) -> ITransport:
        return Plane(self._flight_number)

Each creator handles its own complexity. RoadLogistics only knows about plate_number, and SeaLogistics only knows about port_code. The responsibilities are now cleanly separated.

Step 4: The Client Code

The client code now decides which creator to use based on its needs.

# main.py
from concrete_creators import RoadLogistics, SeaLogistics, AirLogistics

def client_code(logistics_creator: Logistics):
    """
    The client code works with an instance of a concrete creator, albeit through
    its base interface. As long as the client keeps working with the creator via
    the base interface, you can pass it any creator's subclass.
    """
    logistics_creator.plan_delivery()


print("App: Launched with RoadLogistics.")
client_code(RoadLogistics(plate_number="XYZ-123"))

print("\n" + "="*20 + "\n")

print("App: Launched with SeaLogistics.")
client_code(SeaLogistics(port_code=8080))

print("\n" + "="*20 + "\n")

print("App: Launched with AirLogistics.")
client_code(AirLogistics(flight_number="SN050"))

This is clean, maintainable, and adheres to the Open/Closed Principle. If we need to add a DroneLogistics, we just create the Drone product and DroneLogistics creator. No existing code needs to change.

Advanced Use Case: Combining Factory Method with Simple Factory

You might have noticed a new problem: the client code now has the responsibility of choosing the right Logistics creator.

# Client's burden
user_input = "road"
if user_input == "road":
    creator = RoadLogistics(plate_number="ABC-456")
elif user_input == "sea":
    creator = SeaLogistics(port_code=9001)
# ... and so on

We can solve this by using a Simple Factory whose job is to create the correct creator. This powerful combination gives us the best of both worlds: centralized creator selection and decoupled product instantiation.

Let’s create a LogisticsFactory.

# logistics_factory.py
from creators import Logistics
from concrete_creators import RoadLogistics, SeaLogistics, AirLogistics

class LogisticsFactory:
    """A Simple Factory for creating the correct Logistics creator."""
    def create_logistics(self, transport_type: str, **kwargs) -> Logistics:
        if transport_type.upper() == "ROAD":
            return RoadLogistics(kwargs.get("plate_number", "DEFAULT-PLATE"))
        elif transport_type.upper() == "SEA":
            return SeaLogistics(kwargs.get("port_code", 9999))
        elif transport_type.upper() == "AIR":
            return AirLogistics(kwargs.get("flight_number", "DEFAULT-FLIGHT"))
        else:
            raise ValueError("Invalid transport type specified.")

# The final, clean client code
logistics_simple_factory = LogisticsFactory()

# Let's say we get this from user input or a config file
delivery_type = "AIR"
params = {"flight_number": "BA2490"}

# The factory handles the logic
logistics_provider = logistics_simple_factory.create_logistics(delivery_type, **params)

# Our client code remains clean and decoupled
logistics_provider.plan_delivery()

This layered pattern is extremely common in real-world applications. It provides a single, clean entry point while keeping the underlying product creation logic flexible and extensible.

Summary: When to Use the Factory Method

mindmap
  root((Factory Method))
    "When to Use?"
      When a class can't anticipate the class of objects it must create.
      When you want to provide users of your library/framework a way to extend its internal components.
      When you want to save system resources by reusing existing objects instead of rebuilding them each time.
    Pros
      Avoids tight coupling between the creator and concrete products.
      "Single Responsibility Principle: You can move the product creation code into one place."
      "Open/Closed Principle: You can introduce new products without breaking existing client code."
    Cons
      The code can become more complicated since you need to introduce a lot of new subclasses.


Quiz Yourself: Test Your Understanding

Question: If you add a new `BicycleLogistics` creator, which files do you absolutely need to modify?

  1. creators.py (the abstract Logistics class)
  2. products.py
  3. concrete_creators.py
  4. logistics_factory.py (the Simple Factory)

Click for Solution

You would need to:

  1. Add a Bicycle class to products.py.
  2. Add a BicycleLogistics class to concrete_creators.py.
  3. Update the LogisticsFactory in logistics_factory.py to handle the "BICYCLE" type.
You would not need to modify the abstract Logistics creator or any of the original client code that consumes the products. That's the power of the pattern!


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?