Loading episodes…
0:00 0:00

Stop Writing Messy If/Else Blocks: A Visual Guide to the Simple Factory Pattern in Python

00:00
BACK TO HOME

Stop Writing Messy If/Else Blocks: A Visual Guide to the Simple Factory Pattern in Python

10xTeam December 22, 2025 10 min read

Tangled in a web of if/elif/else statements just to create objects? You’re not alone. As an application grows, the logic for deciding which object to instantiate can become scattered, duplicated, and a nightmare to maintain. Every new type you add forces you to hunt down and update every conditional block across your codebase.

This violates core software design principles and leads to brittle, unreadable code. But there’s a classic, elegant solution: the Simple Factory pattern.

The Simple Factory is a creational design pattern that provides a centralized, static method to create objects, hiding the complex instantiation logic from the client. Instead of the client deciding which class to instantiate, it simply asks the factory for an object of a certain type.

Let’s break down how it works and refactor a messy codebase into a clean, professional one.

The Core Idea: A Visual Overview

At its heart, the Simple Factory pattern is about centralizing responsibility. It introduces a single point in your application for object creation, making your system cleaner and easier to manage.

mindmap
  root((Simple Factory))
    Purpose
      ::icon(fa fa-cogs)
      Centralize object creation logic
      Decouple client from concrete classes
    Problem Solved
      ::icon(fa fa-bug)
      Scattered `if/else` blocks
      Code duplication
      Violates Single Responsibility Principle (SRP)
    Components
      ::icon(fa fa-puzzle-piece)
      Client
      Factory
      Product (Interface/Abstract Class)
      Concrete Products
    Benefits
      ::icon(fa fa-thumbs-up)
      Improved Maintainability
      Enhanced Extensibility
      Cleaner Codebase

The Problem: A Logistic System Riddled with Conditionals

Imagine we’re building a logistics application. The business needs to ship goods via trucks or ships. A junior developer might approach this by creating a couple of classes and using an if/elif block in the client code to decide which transport method to use.

First, let’s set up the project structure.

logistics_project/
├── main.py
└── transports.py

Our transports.py file defines the “products”—an abstract transport class and the concrete implementations.

# transports.py
from abc import ABC, abstractmethod

class Transport(ABC):
    """An abstract base class for all transport types."""
    @abstractmethod
    def deliver(self, item: str):
        pass

class Truck(Transport):
    """A concrete class for truck transport."""
    def deliver(self, item: str):
        print(f"Delivering '{item}' by land in a truck.")

class Ship(Transport):
    """A concrete class for ship transport."""
    def deliver(self, item: str):
        print(f"Delivering '{item}' by sea in a container ship.")

The main.py file acts as the client. It takes user input and decides which object to create.

# main.py
from transports import Truck, Ship, Transport

def get_transport(transport_type: str, item: str) -> Transport:
    """
    Problematic function that creates transport objects based on a string type.
    """
    transport: Transport
    if transport_type == "truck":
        transport = Truck()
    elif transport_type == "ship":
        transport = Ship()
    else:
        raise ValueError(f"Unknown transport type: {transport_type}")
    
    transport.deliver(item)
    return transport

# --- Client Code ---
get_transport("ship", "Medical Supplies")

[!WARNING] This approach has several major flaws:

  1. Violation of Single Responsibility Principle (SRP): The client code is now responsible for both its primary job and the logic of object creation.
  2. Code Duplication: If another part of the application needs to create a transport object, you’ll have to duplicate this if/elif logic.
  3. Poor Maintainability: What happens when the business wants to add a new transport method?

The Pain of Extension

The business now wants to add airplanes as a shipping option. With our current design, we have to modify the if/elif block in main.py.

First, we add the new class to transports.py:

# transports.py

# ... (Truck and Ship classes remain the same) ...

class Airplane(Transport):
    """A new concrete class for airplane transport."""
    def deliver(self, item: str):
        print(f"Delivering '{item}' by air in a cargo plane.")

Now, we must update the client code. This is where the design starts to break down.

# main.py
- from transports import Truck, Ship, Transport
+ from transports import Truck, Ship, Airplane, Transport

def get_transport(transport_type: str, item: str) -> Transport:
    """
    This function gets more complex with every new transport type.
    """
    transport: Transport
    if transport_type == "truck":
        transport = Truck()
    elif transport_type == "ship":
        transport = Ship()
+   elif transport_type == "airplane":
+       transport = Airplane()
    else:
        raise ValueError(f"Unknown transport type: {transport_type}")
    
    transport.deliver(item)
    return transport

# --- Client Code ---
- get_transport("ship", "Medical Supplies")
+ get_transport("airplane", "Urgent Documents")

Imagine this logic is repeated in 5 different places in your application. You’d have to find and update all 5 locations. If you miss one, you introduce a bug. This is not scalable.

The Solution: Centralizing Creation with a Simple Factory

Let’s refactor this mess using the Simple Factory pattern. We’ll create a dedicated TransportFactory class whose only job is to create transport objects.

First, let’s update our file structure to include the factory.

logistics_project/
├── main.py
├── transports.py
└── factory.py

Now, we create the TransportFactory in factory.py. This class will contain a static method that encapsulates the entire creation logic.

# factory.py
from transports import Transport, Truck, Ship, Airplane

class TransportFactory:
    """
    A Simple Factory for creating transport objects.
    It centralizes the creation logic.
    """
    @staticmethod
    def create_transport(transport_type: str) -> Transport:
        """
        Creates and returns a transport object based on the type.
        
        This is a more Pythonic way than a switch or if/elif chain.
        """
        transport_map = {
            "truck": Truck,
            "ship": Ship,
            "airplane": Airplane,
        }
        
        transport_class = transport_map.get(transport_type)
        
        if not transport_class:
            raise ValueError(f"Invalid transport type: {transport_type}")
            
        return transport_class()

[!TIP] Pythonic Implementation: Instead of an if/elif/else chain or a match/case statement, using a dictionary to map strings to classes is a common and highly extensible Python idiom. It’s clean, readable, and easy to add new types to.

The factory’s process is simple and clear:

graph TD
    A[Client] -- Requests 'truck' --> B(TransportFactory);
    B -- Looks up 'truck' in its map --> C{Finds Truck Class};
    C -- Instantiates --> D[Truck Object];
    B -- Returns object --> A;

Refactoring the Client to Use the Factory

With the factory in place, our client code in main.py becomes incredibly simple and clean. It no longer needs to know about Truck, Ship, or Airplane. It only knows about the factory.

Here is the evolution of our main.py:

# main.py
- from transports import Truck, Ship, Airplane, Transport
+ from factory import TransportFactory

def get_transport(transport_type: str, item: str):
    """
-   Problematic function that creates transport objects based on a string type.
+   Clean function that uses a factory to create transport objects.
    """
-   transport: Transport
-   if transport_type == "truck":
-       transport = Truck()
-   elif transport_type == "ship":
-       transport = Ship()
-   elif transport_type == "airplane":
-       transport = Airplane()
-   else:
-       raise ValueError(f"Unknown transport type: {transport_type}")
+   transport = TransportFactory.create_transport(transport_type)
    
    transport.deliver(item)
-   return transport

# --- Client Code ---
- get_transport("airplane", "Urgent Documents")
+ get_transport("truck", "Heavy Machinery")
+ get_transport("ship", "Consumer Electronics")
+ get_transport("airplane", "Vaccines")

Look at how clean that is! The if/elif chain is gone, replaced by a single, descriptive call to the factory.

The benefits are now crystal clear:

  1. SRP is Restored: The client code is no longer responsible for creation logic.
  2. Logic is Centralized: All creation logic is in TransportFactory. If we need to add a Drone transport, we only modify the factory’s dictionary. No client code needs to change.
  3. Code is Decoupled: main.py is no longer coupled to the concrete Truck, Ship, or Airplane classes. It only depends on the TransportFactory.


🧠 Pop Quiz: Test Your Understanding

Question: If the business decides to add "Drone" as a new transport method, which file(s) MUST you modify? **A)** `main.py` only **B)** `transports.py` and `main.py` **C)** `transports.py` and `factory.py` **D)** `main.py`, `transports.py`, and `factory.py`
Click for Answer

Correct Answer: C) `transports.py` and `factory.py`

You would first create the `Drone` class in `transports.py` (implementing the `Transport` interface). Then, you would update the `transport_map` dictionary in `factory.py` to include the new "drone" type. The client code in `main.py` requires no changes at all!


When Should You Use a Simple Factory?

While powerful, the Simple Factory isn’t a silver bullet.

[!NOTE] Use the Simple Factory pattern when:

  • The object creation process is complex and involves conditional logic.
  • You want to decouple your client code from the specific classes it needs to instantiate.
  • You have a set of classes that implement the same interface or share a common base class, and the client needs to select one at runtime.
  • You want to centralize creation logic to improve maintainability and avoid code duplication.

However, if your object creation is simple (e.g., a direct new MyObject()) and unlikely to change, a factory might be an unnecessary layer of abstraction. As the original transcript noted, if the logic is only used in one single place and is very simple, you might not need the pattern.

Conclusion

The Simple Factory pattern is a fundamental tool for writing clean, maintainable, and scalable object-oriented code. By centralizing object creation, you decouple your client from concrete implementations, adhere to the Single Responsibility Principle, and make your system far easier to extend.

The next time you find yourself writing an if/elif/else block to instantiate objects, take a moment to consider if a Simple Factory could do the job better. Your future self will thank you.


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?