The Singleton Pattern

Learn about the Singleton design pattern in Python.

We'll cover the following

Overview

The Singleton pattern is a source of some controversy; many have accused it of being an anti-pattern, a pattern that should be avoided, not promoted. In Python, if someone is using the Singleton pattern, they’re almost certainly doing something wrong, probably because they’re coming from a more restrictive programming language.

So, why discuss it at all? Singleton is useful in overly object-oriented languages and is a vital part of traditional object-oriented programming. More relevantly, the idea behind singleton is useful, even if we implement the concept in a totally different way in Python.

The basic idea behind the Singleton pattern is to allow exactly one instance of a certain object to exist. Typically, this object is a sort of manager class. Such manager objects often need to be referenced by a wide variety of other objects; passing references to the manager object around to the methods and constructors that need them can make code hard to read.

Instead, when a singleton is used, the separate objects request the single instance of the manager object from the class. The UML diagram doesn’t fully describe it, but here it is for completeness:

Press + to interact
The UML diagram of the Singleton pattern
The UML diagram of the Singleton pattern

In most programming environments, singletons are enforced by making the constructor private (so no one can create additional instances of it), and then providing a static method to retrieve the single instance. This method creates a new instance the first time it is called, and then returns that same instance for all subsequent calls.

Singleton implementation

Python doesn’t have private constructors, but for this purpose, we can use the __new__() class method to ensure that only one instance is ever created:

Press + to interact
class OneOnly:
_singleton = None
def __new__(cls, *args, **kwargs):
if not cls._singleton:
cls._singleton = super().__new__(cls, *args, **kwargs)
return cls._singleton

When __new__() is called, it normally constructs a new instance of the requested class. When we override it, we first check whether our singleton instance has been created; if not, we create it using a super call. Thus, whenever we call the constructor on OneOnly, we always get the exact same instance:

Press + to interact
class OneOnly:
_singleton = None
def __new__(cls, *args, **kwargs):
if not cls._singleton:
cls._singleton = super().__new__(cls, *args, **kwargs)
return cls._singleton
o1 = OneOnly()
o2 = OneOnly()
print(o1 == o2)
print(id(o1) == id(o2))
print(o1)
print(o2)

The two objects are equal and located at the same address; thus, they are the same object. This particular implementation isn’t very transparent, since it’s not obvious that the special method is being used to create a singleton object.

We don’t actually need this. Python provides two built-in Singleton patterns we can leverage. Rather than invent something hard to read, there are two choices:

  • A Python module is a singleton. One import will create a module. All subsequent attempts to import the module return the one-and-only singleton instance of the module. In places where an application-wide configuration file or cache is required, make this part of a distinct module. Library modules like logging, random, and even re have module-level singleton caches. We’ll look at using module-level variables below.

  • A Python class definition can also be pressed into service as a singleton. A class can only be created once in a given namespace. Consider using a class with class-level attributes as a singleton object. This means defining methods with the @staticmethod decorator because there will never be an instance created, and there’s no self variable.

To use module-level variables instead of a complex Singleton pattern, we instantiate the class after we’ve defined it. We can improve our State pattern implementation from earlier on to use singleton objects for each of the states. Instead of creating a new object every time we change states, we can create a collection of module-level variables that are always accessible.

We’ll make a small but very important design change, also. In the examples above, each state has a reference to the Message object that is being accumulated. This required us to provide the Message object as part of constructing a new NMEA_State object; we used code like return Body(self.message) to switch to a new state, Body, while working on the same Message instance.

If we don’t want to create (and recreate) state objects, we need to provide the Message as an argument to the relevant methods.

Here’s the revised NMEA_State class:

Press + to interact
class NMEA_State:
def enter(self, message: "Message") -> "NMEA_State":
return self
def feed_byte(
self,
message: "Message",
input: int
) -> "NMEA_State":
return self
def valid(self, message: "Message") -> bool:
return False
def __repr__(self) -> str:
return f"{self.__class__.__name__}()"

This variant on the NMEA_State class doesn’t have any instance variables. All the methods work with argument values passed in by a client. Here are the individual state definitions:

Press + to interact
class NMEA_State:
def __init__(self, message: "Message") -> None:
self.message = message
def feed_byte(self, input: int) -> "NMEA_State":
return self
def valid(self) -> bool:
return False
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.message})"
class Waiting(NMEA_State):
def feed_byte(
self,
message: "Message",
input: int
) -> "NMEA_State":
return self
if input == ord(b"$"):
return HEADER
return self
class Header(NMEA_State):
def enter(self, message: "Message") -> "NMEA_State":
message.reset()
return self
def feed_byte(
self,
message: "Message",
input: int
) -> "NMEA_State":
return self
if input == ord(b"$"):
return HEADER
size = message.body_append(input)
if size == 5:
return BODY
return self
class Body(NMEA_State):
def feed_byte(
self,
message: "Message",
input: int
) -> "NMEA_State":
return self
if input == ord(b"$"):
return HEADER
if input == ord(b"*"):
return CHECKSUM
size = message.body_append(input)
return self
class Checksum(NMEA_State):
def feed_byte(
self,
message: "Message",
input: int
) -> "NMEA_State":
return self
if input == ord(b"$"):
return HEADER
if input in {ord(b"\n"), ord(b"\r")}:
# Incomplete checksum... Will be invalid.
return END
size = message.checksum_append(input)
if size == 2:
return END
return self
class End(NMEA_State):
def feed_byte(
self,
message: "Message",
input: int
) -> "NMEA_State":
return self
if input == ord(b"$"):
return HEADER
elif input not in {ord(b"\n"), ord(b"\r")}:
return WAITING
return self
def valid(self, message: "Message") -> bool:
return message.valid

Here are the module-level variables created from instances of each of these NMEA_State classes.

Press + to interact
WAITING = Waiting()
HEADER = Header()
BODY = Body()
CHECKSUM = Checksum()
END = End()

Within each of these classes, we can refer to these five global variables to change parsing state. The ability to refer to a global that’s defined after the class can seem a little mysterious at first. It works out beautifully because Python variable names are not resolved to objects until runtime. When each class is being built, a name like CHECKSUM is little more than a string of letters. When evaluating the Body.feed_byte() method and it’s time to return the value of CHECKSUM, then the name is resolved to the singleton instance of the Checksum() class:

Note how the Header class was refactored. In the version where each state has an __init__(), we could explicitly evaluate Message.reset() when entering the Header state. Since we’re not creating new state objects in this design, we need a way to handle the special case of entering a new state, and performing an enter() method one time only to do initialization or setup. This requirement leads to a small change in the Reader class:

Press + to interact
from __future__ import annotations
from typing import Optional, Iterable, Iterator, cast
class NMEA_State:
def __init__(self, message: "Message") -> None:
self.message = message
def feed_byte(self, input: int) -> "NMEA_State":
return self
def valid(self) -> bool:
return False
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.message})"
class Waiting(NMEA_State):
def feed_byte(self, input: int) -> NMEA_State:
if input == ord(b"$"):
return Header(self.message)
return self
class Header(NMEA_State):
def __init__(self, message: "Message") -> None:
self.message = message
self.message.reset()
def feed_byte(self, input: int) -> NMEA_State:
if input == ord(b"$"):
# Reset any accumulated bytes
return Header(self.message)
size = self.message.body_append(input)
if size == 5:
return Body(self.message)
return self
class Body(NMEA_State):
def feed_byte(self, input: int) -> NMEA_State:
if input == ord(b"$"):
return Header(self.message)
if input == ord(b"*"):
return Checksum(self.message)
self.message.body_append(input)
return self
class Checksum(NMEA_State):
def feed_byte(self, input: int) -> NMEA_State:
if input == ord(b"$"):
return Header(self.message)
if input in {ord(b"\n"), ord(b"\r")}:
return End(self.message)
size = self.message.checksum_append(input)
if size == 2:
return End(self.message)
return self
class End(NMEA_State):
def feed_byte(self, input: int) -> NMEA_State:
if input == ord(b"$"):
return Header(self.message)
elif input not in {ord(b"\n"), ord(b"\r")}:
return Waiting(self.message)
return self
def valid(self) -> bool:
return self.message.valid
class Message:
def __init__(self) -> None:
self.body = bytearray(80)
self.checksum_source = bytearray(2)
self.body_len = 0
self.checksum_len = 0
self.checksum_computed = 0
def reset(self) -> None:
self.body_len = 0
self.checksum_len = 0
self.checksum_computed = 0
def body_append(self, input: int) -> int:
self.body[self.body_len] = input
self.body_len += 1
self.checksum_computed ^= input
return self.body_len
def checksum_append(self, input: int) -> int:
self.checksum_source[self.checksum_len] = input
self.checksum_len += 1
return self.checksum_len
@property
def valid(self) -> bool:
return (
self.checksum_len == 2
and int(self.checksum_source, 16) == self.checksum_computed
)
def header(self) -> bytes:
return bytes(self.body[:5])
def fields(self) -> list[bytes]:
return bytes(self.body[: self.body_len]).split(b",")
def __repr__(self) -> str:
body = self.body[: self.body_len]
checksum = self.checksum_source[: self.checksum_len]
return f"Message({body}, {checksum}, computed={self.checksum_computed:02x})"
def message(self) -> bytes:
return (
b"$"
+ bytes(self.body[: self.body_len])
+ b"*"
+ bytes(self.checksum_source[: self.checksum_len])
)
class Reader:
def __init__(self) -> None:
self.buffer = Message()
self.state: NMEA_State = Waiting(self.buffer)
def read(self, source: Iterable[bytes]) -> Iterator[Message]:
for byte in source:
self.state = self.state.feed_byte(cast(int, byte))
if self.buffer.valid:
yield self.buffer
self.buffer = Message()
self.state = Waiting(self.buffer)
message = b'''
$GPGGA,161229.487,3723.2475,N,12158.3416,W,1,07,1.0,9.0,M,,,,0000*18
$GPGLL,3723.2475,N,12158.3416,W,161229.487,A,A*41
'''
rdr = Reader()
result = list(rdr.read(message))
print(result)

We don’t trivially replace the value of the self.state instance variable with the result of the self.state.feed_byte() evaluation. Instead, we compare the previous value of self.state with the next value, new_state, to see if there was a state change. If there was a change, then we need to evaluate enter() on the new state, to allow the state change to do any required one-time initialization.

In this example we aren’t wasting memory creating a bunch of new instances of each state object that must later be garbage collected. Instead, we are reusing a single state object for each piece of the incoming data stream. Even if multiple parsers are running at once, only these state objects need to be used. The stateful message data is kept separate from the state processing rules in each state object.

Note: We’ve combined two patterns, each with different purposes. The State pattern covers how processing is completed. The Singleton pattern covers how object instances are managed. Many software designs involve numbers of overlapping and complementary patterns.