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: Using Decorators in Python

Python's dynamic capabilities make implementing various design patterns straightforward. In this lab, you will explore the Singleton pattern using Python decorators, enabling you to easily apply this pattern to any class. By the end of the lab, you will understand how to utilize decorators to create shareable and maintainable code while effectively implementing the Singleton pattern.

Lab platform
Lab Info
Level
Beginner
Last updated
Dec 23, 2025
Duration
25m

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

    Guided: Using Decorators in Python

    Guided: Using Decorators in Python

    Welcome to this hands-on learning experience.

    Decorators in Python are like wrappers that add extra functionality to existing code without modifying it directly.

    Imagine you have a gift box (your code), and you want to enhance it by wrapping it in decorative paper (a decorator). The gift remains the same, but its presentation improves, making it more useful or appealing. Similarly, decorators "wrap" functions or classes, adding new behavior.

    You may have seen the common built-in @property decorator to declare a getter method:

    class Example: 
        . . .
        @property
        def attribute():
            . . .s
    
    

    In this lab, you will learn how to create your own decorators. Specifically, you'll create a singleton decorator that will wrap this design pattern around any class.

    Common Use Cases ______

    Logging: Automatically recording details of function calls, such as arguments, return values, and execution time

    Caching: Storing the results of expensive function calls to improve performance by reusing them when the same inputs occur again

    Authentication: Verifying user permissions or credentials before allowing the execution of sensitive functions

    Learning Objectives:

    By the end of this lab, you will create a decorator to implement the singleton pattern, ensuring a class has only one instance. This is useful for configuration management or logging.

    Through practical examples, you'll learn how to build a reusable and efficient decorator.

    Press the double right arrow button to begin. 

  2. Challenge

    Python Decorators

    Python Decorators

    Decorators in Python are higher-order functions, meaning they take another function as input, add extra functionality around it, and return the enhanced version.

    Like the gift box analogy being enhanced by the wrapping paper — the original function stays the same, but its functionality is enhanced.

    In code, this means the decorator takes a function, wraps it with additional behavior (like logging or access control), and returns a new function with the combined functionality.

    Key Benefits ----- Decorators provide several key benefits:

    Code Reusability: Encapsulate common functionality, such as logging or configuration.

    Reduce Redundancy: Adhere to the DRY (Don't Repeat Yourself) principle by avoiding repetitive code.

    Separation of Concerns: Isolate auxiliary logic from the core functionality of a function.

    Simple Example

    To see a simple version of this in action, open the **/demo/simple.py ** script file.

    Starting at the top, the simple_decorator() function is declared to accept another function (func) as its argument.

    The nested wrapper() function then adds new actions before and after calling the passed function. The results of calling this function are then returned to ensure that the decorated function produces the same output as the original.

    Understanding (*args, **kwargs) ______ The parameters `\*args` and `\*\*kwargs` are referred to as variable-length arguments. They allow functions to accept any number of positional and keyword arguments, respectively.

    This allows decorators to be flexible and able to handle functions with varying parameters.

    The simple_decorator() function then returns the nested wrapper function declaration, allowing it to replace the decorated function with an enhanced version. This enhanced version still preserves the original intended behavior.

    Notice that it does not return wrapper(). Doing that would execute the wrapper function immediately.

    This structure makes it easy to reuse the simple_decorator, consistently applying enhancements across multiple functions in your codebase.

    Applying the Decorator

    To apply the simple_decorator, place it directly above the example_function() prepending the @ symbol.

    The example_function() should now look like:

    @simple_decorator
    def example_function():
        . . .
    

    This shorthand @ syntax makes it clear that the function is being enhanced by the decorator.

    Testing the Decorator

    You can test this functionality by running the script:

    python3 demo/simple.py
    

    The expected output will look like this:

    Calling function example_function...
    Function executed.
    Additional functionality
    

    This simple example demonstrates how a decorator can enhance the behavior of a function while maintaining its original intended behavior.

    Before creating your own singleton decorator, take a moment to review the singleton pattern in the next step.

  3. Challenge

    Understanding the Singleton Pattern

    Understanding the Singleton Pattern

    A singleton is a design pattern that ensures a class has only one instance while providing a global point of access to that instance.

    It's often used when managing shared resources like configuration settings, logging systems, or connection pools.

    By controlling the instance creation process, a singleton ensures consistency across your application.

    Basic Implementation Example

    There are several ways to implement the singleton pattern.

    A straightforward example using the __new__ method has been provided in the **/demo/singleton.py ** script file.

    In a moment, you'll see the purpose for the class-level attribute _instance. Take note that as a class-level attribute, this value will be available to every caller of the Configuration class.

    When a new object of this class is instantiated, Python calls the __new__() method. Here, you'll see that the Configuration class is overriding this method in order to control the class instance creation process.

    The cls passed to this method refers to the class definition, which provides access to the class-level _instance attribute.

    cls vs self ______

    cls is used in class methods to refer to the class itself, whereas self is used in instance methods to refer to a specific instance of the class.

    If the cls._instance has not yet been set, then a new instance is created and stored in the shared class-level attribute _instance.

    This shared instance is then returned to the caller.

    Testing Singleton Behavior

    To verify the singleton behavior of the Configuration class, two instances of the the class are requested in config1 and config2.

    The identity of both of these instances are then compared using the is operator.

    Run this comparison in the Terminal using the command:

    python3 demo/singleton.py
    

    The expected output is: True. This means that both of these variables are pointing to the same object.

    Basic Singleton Implementation ----- This example demonstrates the basic concept of the singleton pattern, but lacks essential features for real-world applications.

    For instance, it does not ensure thread safety, which can cause issues in multi-threaded environments.

    Additionally, by omitting __init__ logic, it does not support instance-specific initialization, reducing its flexibility in practical use.

    These limitations highlight the importance of adapting the pattern for more robust and production-ready scenarios.

    With a basic understanding of the singleton pattern, you're ready to take the next step and create a reusable singleton decorator.

  4. Challenge

    Refactoring to a Singleton Decorator

    Refactoring to a Singleton Decorator

    In the previous implementation, the Configuration class directly managed the singleton behavior by overriding the __new__ method and storing a single instance in a class-level attribute.

    While this approach works, it can become repetitive when applied across multiple classes.

    Using a decorator reduces code duplication, improves readability, and makes the singleton logic reusable across different classes, ensuring consistency.

    In this step, you'll extract the singleton logic into a decorator and apply it to the Configuration class.

    Identifying the Singleton Decorator

    You can start by identifying the common elements of the singleton implementation in the same **/demo/singleton.py ** file.

    Specifically, identity the elements for the single instance storage and the logic for managing this single instance.

    Extracting the Singleton Components

    To transform this into a functional decorator, you will need the decorator function to:

    1. Accept the class to be decorated as a parameter.
    2. Implement the instance storage for all decorated classes.
    3. Manage the instantiation of each decorated class.
    4. Then return the managed instance to the caller.

    Note: If you get stuck with these refactorings, the solution will be provided below.

    Accept the Class to be Decorated:

    To get started, at the top of the file, declare a singleton() function that accepts a cls parameter. This will allow this decorator to operate on the specific class being decorated.

    def singleton(cls):
    

    Implement the Instance Storage:

    Analyzing the _instance storage in the Configuration example, it only needs to support one instance of this class.

    This decorator will need to support multiple instances for each class that will be decorated. This can be accomplished by using a dictionary.

    Nested in the singleton() function, declare the following local variable.

    _instances = {}
    

    Manage the Instance Instantiation:

    This task will require some refactoring of the Configuration examples __new__ override.

    As a function decorator there is not a __new__() method to override, instead you'll follow the earlier example by creating a nested wrapper(*args, **kwargs) function.

    As for the instance check, since _instances is now a dictionary you'll need to check if it contains the given cls.

    Note: Since _instances is nested inside of a function, it is now a local variable and can be accessed directly.

    As before, if the cls instance does not exist, a new one will need to be instantiated. This also differs from the example since the class can be instantiated directly by passing the variable-length arguments.

    This instance will also need to be stored differently in the _instances dictionary, using the passed cls as the key.

    def wrapper(*args, **kwargs):
        if cls not in _instances:
            _instances[cls] = cls(*args, **kwargs)
        return _instances[cls]
    

    Return the Managed Instance:

    With a completed wrapper() function, you can return this function definition to the caller.

    The final singleton decorator should now look like:

    def singleton(cls):
        _instances = {}
        def wrapper(*args, **kwargs):
            if cls not in _instances:
                _instances[cls] = cls(*args, **kwargs)
            return _instances[cls]
        return wrapper
    

    Testing the singleton decorator

    To test this singleton decorator, you'll need to apply it to the Configuration class and verify that multiple instances reference the same object.

    You will also need to remove all of the singleton logic from the Configuration class. For this simple implementation, you can replace the whole class body with the pass placeholder.

    The new Configuration declaration should now look like:

    @singleton
    class Configuration:
        pass
    

    If you are not sure you implemented this correctly, you can compare to the solution **/demo/singleton_decorator.py ** file.

    Run the script again in the Terminal:

    python3 demo/singleton.py
    

    You should see the same expected output of True, meaning that both of these variables are still pointing to the same object.

    Congratulations on successfully implementing and testing the singleton decorator!

    This is a foundational example of how decorators work. While it is a simple implementation, it provides a strong basis for exploring advanced decorator features and addressing edge cases, such as thread safety and handling arguments dynamically.

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