Featured resource
2025 Tech Upskilling Playbook
Tech Upskilling Playbook

Build future-ready tech teams and hit key business milestones with seven proven plays from industry leaders.

Check it out
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Guided: Implementing a Design Pattern in Python

In this hands-on lab, you’ll implement the Factory Method design pattern in Python by refactoring a simple command-line tool that sends messages via Email or SMS. You’ll start with tightly coupled procedural code and gradually introduce abstraction through interfaces, concrete classes, and a factory method. Along the way, you'll see how this pattern supports cleaner, more maintainable code that follows object-oriented design principles.

Lab platform
Lab Info
Level
Intermediate
Last updated
Dec 23, 2025
Duration
30m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Design Patterns

    Design Patterns

    Design patterns are proven solutions to common software design problems. They help developers create code that is more maintainable, flexible, and reusable.

    A common design pattern is the Factory Method. This pattern allows you to create objects without specifying the exact class of object that will be created. Instead, you define an interface for creating an object, and let subclasses decide which class to instantiate.

    Why Use the Factory Method Pattern?

    When object creation is tightly coupled with business logic, applications become rigid and hard to extend. Mixing object instantiation with core functionality makes it challenging and error-prone to add new features or modify existing ones.

    The Factory Method pattern solves this problem by separating object creation from the main application logic. This separation leads to code that is easier to maintain, extend, and adapt as requirements change.

    What You Will Accomplish

    In this lab, you'll discover the value of the Factory Method pattern and how it supports scalable software design.

    You'll refactor a simple command-line messaging application to use the Factory Method pattern in Python.


    To get started, click the right arrow below to review the current code and identify areas for improvement.

  2. Challenge

    Review the Starting Code

    Review the Starting Code

    Before you start refactoring, it's important to understand the limitations of the current code.

    Open the /main.py file in your editor. This script prompts the user to choose a channel—either email or SMS—and then uses a conditional statement to determine which channel to use for sending the message. If the user enters an invalid channel, the script raises an error.

    To try it out, run this command in your terminal:

    python3 main.py
    

    Issues with the Current Implementation

    Using conditional statements to select the message channel works for simple scripts, but it quickly becomes problematic as your application grows.

    The current approach tightly couples message-sending logic to user input, so adding a new channel—like push notifications—requires editing main.py directly, increasing the risk of bugs and making maintenance harder.

    Responsibilities for handling user input, selecting the channel, and sending messages are all mixed together, leading to less organized and harder-to-understand code.

    Refactoring to use the Factory Method design pattern will make your application more modular, extensible, and easier to maintain.

    Implementing the Factory Method Design Pattern

    To help you get started, a /channels directory has been created with all the files you'll need to implement the Factory Method design pattern.

    Tip: In the activity bar to the left, you can click on the double page icon to switch to the explorer view to see these files. When done you can click on the book icon to return to this instructional pane.

    Here’s a quick summary of the files you’ll find in the /channels directory:

    __init__.py: Marks the directory as a Python package.

    interface.py: Defines a common interface for all message channels.

    email_channel.py and sms_channel.py: Provide concrete implementations for each channel.

    factory.py: Contains the factory method for creating channel instances.

    This structure keeps your code organized and makes it easy to add new channels in the future—just create a new file that implements the shared interface and include in the factory method of the factory.py file.


    The first task is to define a shared interface that all channels will implement.

    Click the right arrow below to see how to create this interface.

  3. Challenge

    Define a Common Interface

    Define a Common Interface

    To begin, establish a shared interface that all message channels will follow. This ensures your code remains consistent and easy to extend as you add new channel types.

    Python’s abc module lets you define an abstract base class for this purpose.

    Open the /channels/interface.py file and add the following interface definition:

    from abc import ABC, abstractmethod
    
    class Channel(ABC):
        @abstractmethod
        def send(self, recipient: str, message: str):
            pass
    

    Tip: If you need guidance or want to compare your work, you can find a complete example implementation in the /solution directory.

    Benefits of a Shared Interface

    A shared interface ensures all channel classes implement the same methods, like send, making your codebase consistent and easier to maintain. It also allows you to add new channels without modifying existing code, following the Open/Closed Principle for better scalability and flexibility.


    Next, you'll create classes that inherit from Channel and implement the send method. Each class will define how messages will be delivered for its specific channel type.

  4. Challenge

    Implement Concrete Channel Classes

    Implement Concrete Channel Classes

    With the Channel interface defined, it's time to create concrete classes for each communication channel: EmailChannel and SMSChannel.

    These classes will inherit from Channel and provide their own implementation of the send method, allowing each to handle message delivery in a way that's specific to the channel.

    EmailChannel Implementation

    To get started, open the /channels/email_channel.py file.

    Begin by importing the Channel interface from interface.py using a relative import:

    from .interface import Channel
    

    Next, define the EmailChannel class that inherits from Channel:

    class EmailChannel(Channel):
    

    Then, implement the send method to mock the process of sending an email by printing a message indicating that an email is being sent.

        def send(self, recipient: str, message: str):
            print(f"Sending email to {recipient}: {message}")
    

    SMSChannel Implementation

    Following the EmailChannel example, create the SMSChannel class in the /channels/sms_channel.py file:

    The final implementation of the SMSChannel class should look like this:

    Solution
    from .interface import Channel
    
    class SMSChannel(Channel):
        def send(self, recipient: str, message: str):
            print(f"Sending SMS to {recipient}: {message}")
    	
    

    By following this structure, you ensure that both channels adhere to the same interface, making your codebase more consistent and extensible.


    With the concrete channel classes in place, your next task is to implement a factory method.

  5. Challenge

    The Factory Method

    The Factory Method

    Now that you have your concrete channel classes, it's time to implement a factory method. This method will create and return the correct channel object based on user input.

    By moving object creation out of your main application logic, you make your code cleaner and more flexible. Adding or updating channels in the future becomes much simpler.

    Writing the Factory Method

    Open the /channels/factory.py file. In this file, you'll define a get_channel() function. This function will accept a channel type as an argument and return an instance of the appropriate channel class.

    Begin by importing the channel interface and the concrete channel classes:

    from .interface import Channel
    from .email_channel import EmailChannel
    from .sms_channel import SMSChannel
    

    Now, define the get_channel() function. Move the conditional logic that selects the correct channel from the /main.py file into this function.

    The get_channel() function should return an instance of the appropriate channel class based on the input, or raise an error if the channel type is unknown.

    def get_channel(channel: str) -> Channel:
        if channel == "email":
            return EmailChannel()
        elif channel == "sms":
            return SMSChannel()
        else:
            raise ValueError(f"Unknown channel type: {channel}")
    

    Benefits of Delegating Object Creation

    By moving the logic for creating channel instances into the get_channel() function, you separate the concerns of object creation from your main application code.

    This approach makes your main application simpler and more focused on its core task—sending messages—while the factory method handles the details of which channel to use and how to create it.


    Next, you'll modify your main application to use the factory method for creating channel instances.

  6. Challenge

    Refactor the Main Application Logic

    Refactor the Main Application Logic

    Now that you have implemented the factory method and channel classes, it's time to update the main application logic to use the factory method for channel selection.

    Using the Factory Method

    Open the /main.py file. In the main function, find the section where the code chooses which channel to use based on the user's input.

    To begin refactoring, add the following import statement at the top of your main.py file:

    from channels.factory import get_channel
    

    Next, remove the existing conditional statements that select the channel based on user input. Instead, use the factory method to obtain the correct channel instance and call its .send() method.

    Your updated main function should look like this:

    try:
        channel = get_channel(channel_type)
        channel.send(recipient, message)
    except ValueError as e:
        print(e)
    

    Advantages of the Refactored Design

    This refactoring brings several key benefits:

    Decoupling: The main application logic is no longer tied to specific channel implementations. This separation makes it much easier to add or modify channels in the future.

    Maintainability: The codebase becomes cleaner and more readable. Channel selection is now handled by the factory method, reducing complexity in the main logic.

    Extensibility: Adding new channels is straightforward—just create a new channel class and update the factory method. There's no need to change the main application logic.

    Testing the Refactored Application

    After refactoring the main logic, it's important to verify that everything works as intended.

    To test your application:

    python3 main.py
    

    When you enter email or sms as the channel type, the application should still correctly simulate sending messages using the appropriate channel class.

    If you enter an unsupported channel type, the application will raise a ValueError and display an error message.


    To experience the advantages of the factory method, try adding a new channel: implement a PushChannel that handles the push channel type.

    If you’d like step-by-step instructions, continue to the next section, where you’ll learn how to create this channel class and update the factory method to support it.

  7. Challenge

    Adding a New Channel

    Adding a New Channel

    With the factory method in place and the main application logic refactored, adding new channel types becomes straightforward.

    To add push notifications as a new channel, follow these steps:

    In the /channels directory, create a new file called channel_push.py. This file will define the PushChannel class, responsible for handling push notification messages.

    Structure this file similarly to the other channel files, but implement logic specific to push notifications. For example:

    from .interface import Channel
    
    class PushChannel(Channel):
        def send(self, recipient: str, message: str):
            print(f"Sending push notification to {recipient}")
    

    Next, open the /channels/factory.py file and update the get_channel function to include the new PushChannel.

    First, import the new class at the top of the file:

    from .channel_push import PushChannel
    

    Then, add a new condition to handle the push channel:

    elif channel == "push":
        return PushChannel()
    

    Thanks to the Factory Method pattern, adding a new channel type is simple and does not require changes to existing code. Now, when a user enters push as the channel type, the factory method will return an instance of PushChannel.

    To verify your changes, run main.py and enter push when prompted for the channel type. You should see a message indicating that a push notification is being sent.


    Congratulations! You have successfully extended the application to support a new channel type. Notice how you achieved this without altering the existing code structure—this demonstrates the flexibility and power of the Factory Method design pattern. By following this approach, you can easily add more channels in the future with minimal changes.

About the author

Jeff Hopper is a polyglot solution developer with over 20 years of experience across several business domains. He has enjoyed many of those years focusing on the .Net stack.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight