Singleton Config Loader in Python with Caution
Implements a singleton config loader in Python that reads JSON config files, but demonstrates the hidden gotcha of shared state across instances.
Python code
36 linesimport json
from pathlib import Path
class ConfigLoader:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, config_file="config.json"):
if not hasattr(self, "loaded"):
self.config_file = Path(config_file)
self.data = {}
self.loaded = True
def load(self):
try:
self.data = json.loads(self.config_file.read_text())
except FileNotFoundError:
self.data = {}
return self.data
if __name__ == "__main__":
with open("config.json", "w") as f:
json.dump({"debug": True, "port": 8080}, f)
loader1 = ConfigLoader("config.json")
loader2 = ConfigLoader("other_config.json")
print(f"Same instance: {loader1 is loader2}")
print(f"Loader1 config: {loader1.load()}")
print(f"Loader2 config: {loader2.load()}")
Path("config.json").unlink()
Output
Same instance: True
Loader1 config: {'debug': True, 'port': 8080}
Loader2 config: {'debug': True, 'port': 8080}
How it works
The __new__ method ensures that only one instance of ConfigLoader is created by checking _instance. The __init__ method only sets attributes once, using a loaded flag to avoid resetting them on subsequent instantiation calls. The load method reads the config file or returns an empty dict if it's missing. However, because the singleton holds one config_file, the second loader's load actually uses the first loader's file path, leading to surprising behavior. This demonstrates that singletons can cause unexpected state sharing when instance-specific configuration is needed.
Common mistakes
- Forgetting to guard the __init__ method, causing attributes to reset on each call
- Assuming each constructor call creates a fresh object with its own state
- Not considering the implications of shared config across different parts of the app
Variations
- Use a module-level dictionary to cache instances per config file, avoiding the singleton limitation
- Use a classmethod `get_instance` to manage instance creation explicitly
Real-world use cases
- Caching a global configuration object in a microservices entry point to avoid repeated file I/O.
- Sharing a single connection pool or logger setup across modules in a long-running application.
- Ensuring a database client is instantiated once to prevent exhausting connection limits.
Sponsored
More from System design patterns
- Build a BFF (Backend for Frontend) Mock Aggregator in Python medium
- Builder pattern for mocking complex objects in Python easy
- Circuit Breaker Pattern in Python: Closed, Open, and Half-Open States medium
- Create a Data Helper Class in Python easy
- Domain Driven Design Aggregate Root Example in Python medium
- Facade Pattern in Python with Mock Simplification medium
Keep learning
Related tutorials and quizzes for this topic.