Mastering the Factory Method Pattern in Python: A Step-by-Step Guide

By
<h2 id="overview">Overview</h2> <p>The Factory Method pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. It's one of the most widely used patterns in software engineering because it helps decouple object creation from the rest of the code, making systems more flexible and maintainable. In this tutorial, you'll learn how to implement the Factory Method in Python, understand its components, and see practical examples. By the end, you'll be able to recognize opportunities to apply this pattern and write reusable, general-purpose solutions.</p><figure style="margin:20px 0"><img src="https://files.realpython.com/media/The-Factory-Method-Pattern-in-Python_Watermarked.6516a91d4d41.jpg" alt="Mastering the Factory Method Pattern in Python: A Step-by-Step Guide" style="width:100%;height:auto;border-radius:8px" loading="lazy"><figcaption style="font-size:12px;color:#666;margin-top:5px">Source: realpython.com</figcaption></figure> <h2 id="prerequisites">Prerequisites</h2> <p>Before diving into the Factory Method, ensure you have:</p> <ul> <li>A basic understanding of Python object-oriented programming (classes, inheritance, polymorphism).</li> <li>Familiarity with design patterns concepts (what they are and why they're useful).</li> <li>Python 3.6 or newer installed on your machine for running the examples.</li> </ul> <h2 id="step-by-step">Step-by-Step Implementation</h2> <h3 id="problem-statement">1. Problem Statement: A Logging System</h3> <p>Imagine you're building an application that needs to log messages to different destinations: console, file, or remote server. You want to add new logging types in the future without modifying existing code. The Factory Method pattern lets you define a common interface for loggers and a creator that delegates the instantiation of specific loggers to subclasses.</p> <h3 id="define-product">2. Define the Product Interface</h3> <p>First, create an abstract base class (or protocol) for the product – in our case, a Logger. This defines the interface that all concrete loggers must implement.</p> <pre><code>from abc import ABC, abstractmethod class Logger(ABC): @abstractmethod def log(self, message: str) -> None: pass</code></pre> <h3 id="concrete-products">3. Implement Concrete Products</h3> <p>Create concrete classes that implement the Logger interface. Each provides its own logging behavior.</p> <pre><code>class ConsoleLogger(Logger): def log(self, message: str) -> None: print(f"Console: {message}") class FileLogger(Logger): def __init__(self, filename: str = "log.txt"): self.filename = filename def log(self, message: str) -> None: with open(self.filename, "a") as f: f.write(f"File: {message}\n") class RemoteLogger(Logger): def __init__(self, endpoint: str): self.endpoint = endpoint def log(self, message: str) -> None: # Simulate sending to remote server print(f"Remote to {self.endpoint}: {message}")</code></pre> <h3 id="creator-base">4. Create the Creator Base Class</h3> <p>The creator (also called factory) declares the factory method that returns a Logger object. It may also contain core business logic that uses the logger. The factory method is abstract, forcing subclasses to define which concrete logger to instantiate.</p> <pre><code>class LoggerCreator(ABC): @abstractmethod def create_logger(self) -> Logger: pass def log_message(self, message: str) -> None: # This is the core logic that uses the product logger = self.create_logger() logger.log(message)</code></pre> <h3 id="concrete-creators">5. Implement Concrete Creators</h3> <p>Each concrete creator overrides the factory method to return a specific logger. This is where the decision about which class to instantiate is made.</p> <pre><code>class ConsoleLoggerCreator(LoggerCreator): def create_logger(self) -> Logger: return ConsoleLogger() class FileLoggerCreator(LoggerCreator): def __init__(self, filename: str = "log.txt"): self.filename = filename def create_logger(self) -> Logger: return FileLogger(self.filename) class RemoteLoggerCreator(LoggerCreator): def __init__(self, endpoint: str): self.endpoint = endpoint def create_logger(self) -> Logger: return RemoteLogger(self.endpoint)</code></pre> <h3 id="using-pattern">6. Using the Factory Method</h3> <p>Now you can use the creators to log messages. The client code only interacts with the creator abstract class, not with concrete loggers directly.</p> <pre><code>def client_code(creator: LoggerCreator, message: str) -> None: creator.log_message(message) if __name__ == "__main__": console_creator = ConsoleLoggerCreator() client_code(console_creator, "Hello, Console!") file_creator = FileLoggerCreator("app.log") client_code(file_creator, "Writing to file.") remote_creator = RemoteLoggerCreator("https://log-server.example.com") client_code(remote_creator, "Remote log entry.")</code></pre> <p>Output:</p><figure style="margin:20px 0"><img src="https://realpython.com/static/cheatsheet-stacked-sm.c9ac81c58bcc.png" alt="Mastering the Factory Method Pattern in Python: A Step-by-Step Guide" style="width:100%;height:auto;border-radius:8px" loading="lazy"><figcaption style="font-size:12px;color:#666;margin-top:5px">Source: realpython.com</figcaption></figure> <pre><code>Console: Hello, Console! File: Writing to file. Remote to https://log-server.example.com: Remote log entry.</code></pre> <h3 id="parameterized-factory">7. Parameterized Factory Method (Optional)</h3> <p>Sometimes you want a single creator to produce different types based on a parameter. This is a common variant. For example, you could pass a string to the factory method to decide which logger to create.</p> <pre><code>class ConfigurableLoggerCreator(LoggerCreator): def __init__(self, logger_type: str): self.logger_type = logger_type def create_logger(self) -> Logger: if self.logger_type == "console": return ConsoleLogger() elif self.logger_type == "file": return FileLogger() elif self.logger_type == "remote": return RemoteLogger("https://default-endpoint.com") else: raise ValueError(f"Unknown logger type: {self.logger_type}")</code></pre> <p>This approach reduces the number of creator subclasses but violates the Open/Closed Principle because adding a new logger requires modifying the factory method. Use it sparingly.</p> <h2 id="common-mistakes">Common Mistakes</h2> <h3>Forgetting the Abstract Method</h3> <p>Without making <code>create_logger</code> abstract (or raising <code>NotImplementedError</code>), subclasses might forget to override it. The pattern's core is the factory method; ensure it's enforced using <code>@abstractmethod</code> or an explicit check.</p> <h3>Violating the Open/Closed Principle</h3> <p>If you use parameterized factories with if-else chains, adding a new product forces you to modify the factory. Instead, create separate concrete creators for each product to keep the system extensible.</p> <h3>Overusing the Pattern</h3> <p>Not every creation needs a factory method. Use it when your code needs to work with families of related objects or when you anticipate changes in instantiation logic. Avoid adding unnecessary abstraction for simple cases.</p> <h3>Ignoring the Creator's Core Logic</h3> <p>The factory method is meant to be used by the creator's other methods. Don't put the factory method in a separate class that only creates objects – then it's just a Simple Factory, not the Factory Method pattern. The creator should contain business logic that calls the factory method.</p> <h2 id="summary">Summary</h2> <p>The Factory Method pattern in Python provides a clean way to separate object creation from usage. By defining an abstract creator with a factory method, you allow subclasses to decide which concrete product to instantiate. This promotes loose coupling and makes your code open for extension but closed for modification. In this tutorial, you learned how to implement it with a logger example, explored a parameterized variant, and reviewed common pitfalls. Now you can apply the Factory Method pattern to improve the flexibility of your Python projects.</p>
Tags:

Related Articles