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.

Medium Python 3.9+ Aug 9, 2026 System design patterns 12 views 0 copies

Python code

36 lines
Python 3.9+
import 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

stdout
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

  1. Use a module-level dictionary to cache instances per config file, avoiding the singleton limitation
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from System design patterns

Related tutorials and quizzes for this topic.