Skip to Content
Course content

367: The Singleton Pattern in Python

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You'll hear a lot of debates about the Singleton pattern. Some developers call it an "anti-pattern" because it introduces global state, which can make unit testing a nightmare. But in the real world, there are times when having two versions of the same thing is just plain wrong. Think of a configuration manager: you don't want three different objects reading your settings.json file and potentially holding different values in memory.

The goal: A single source of truth for settings

Let's build a AppSettings class. The idea is that no matter where in your application you try to instantiate this class, you always get back the exact same object. I want to be able to set a theme or an API key in one module and have it immediately reflected in another without passing the object through ten different function calls.

Where I tripped up on the implementation

When I first started doing this in Python, I tried to handle the logic inside __init__. It seemed intuitive—check if an instance exists, and if so, return it. My code looked something like this:

class AppSettings:
    _instance = None
    def __init__(self):
        if AppSettings._instance is None:
            AppSettings._instance = self
            self.theme = "light"

I quickly realized this was a failure. In Python, __init__ is an initializer, not a constructor. By the time __init__ is called, Python has already created the object. So, every time I called AppSettings(), I was creating a new object and then just overwriting the _instance reference. I wasn't stopping the creation of new objects; I was just losing track of the old ones.

Controlling instantiation with __new__

To actually implement a Singleton, we have to move upstream to the __new__ method. This is the method that actually creates the instance in memory. If we intercept the process here, we can tell Python: "Wait, I already have an object for this class, just use that one instead of making a new one."

class AppSettings:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            print("Creating the actual instance for the first time...")
            # We call the superclass __new__ to actually allocate the memory
            cls._instance = super(AppSettings, cls).__new__(cls)
            # Initialize our data here since __init__ will run every time
            cls._instance.theme = "light"
            cls._instance.api_key = "12345-ABCDE"
        return cls._instance

Notice that I put the default values inside the if block in __new__. If I had put them in __init__, they would be reset to "light" every single time I called AppSettings(), even if the instance was the same!

Proving the identity

The best way to verify this is using the is operator, which checks if two variables point to the exact same memory address.

# Let's simulate two different parts of an app
config1 = AppSettings()
config1.theme = "dark"

config2 = AppSettings()

print(f"Config 1 theme: {config1.theme}") # dark
print(f"Config 2 theme: {config2.theme}") # dark (It changed!)
print(f"Are they the same object? {config1 is config2}") # True

Now, no matter how many times you call AppSettings(), you're just getting a pointer to that one original object. It's simple, effective, and keeps your configuration synchronized across your entire codebase.




📋 Practical Task

Implementing a Global Database Connection Pool

In a real application, opening a new connection to a database every time you need to run a query is incredibly expensive. You usually want a "Connection Pool" that is shared across the entire app.

Your Task: Create a class named DatabasePool that implements the Singleton pattern using the __new__ method. It should meet these requirements:

  • It must ensure that only one instance of DatabasePool ever exists.
  • The instance should have an attribute called connection_count, initialized to 0 when the instance is first created.
  • Add a method called get_connection() that increments connection_count by 1 and returns a string: "Connection established. Total active: [count]".

Test your implementation by creating two different variables (e.g., pool_a = DatabasePool() and pool_b = DatabasePool()). Call get_connection() on pool_a, and then verify that pool_b.connection_count has also increased.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.