Card 04/ 09

ExampleDifficulty: Intermediate1 min

Singleton Example: An Application Configuration

Read the settings file once, and give every part of the program the same settings.

An application's configuration is the most common Singleton: the settings file should be read once, and every module should see the same values. This example builds a Config singleton in a small package, uses it from a billing module and an email module, and shows what those modules now depend on.

py
import json
from pathlib import Path
from typing import Optional


class Config:
    _instance: Optional["Config"] = None

    def __init__(self, path: Path) -> None:
        print(f"reading {path.name}")
        self.values = json.loads(path.read_text())

    @classmethod
    def instance(cls) -> "Config":
        if cls._instance is None:
            cls._instance = cls(Path(__file__).with_name("settings.json"))
        return cls._instance
`config.py`: the file is read inside the constructor, which `instance()` calls once.
py
from sgconf.config import Config


def invoice_total(net: float) -> str:
    config = Config.instance()
    return f"{net * (1 + config.values['tax_rate']):.2f} {config.values['currency']}"
`billing.py`: the module reaches the configuration through the class. `emails.py` does the same.
py
from sgconf import billing, emails
from sgconf.config import Config

print(billing.invoice_total(100))
print(emails.footer())
print(billing.invoice_total(40))
print("one Config for both modules:", Config.instance() is Config.instance())
OutputRan with python:3.12-slim (Python 3.12.13)
reading settings.json
120.00 GBP
Questions? Write to [email protected]
48.00 GBP
one Config for both modules: True
Two modules, three calls, and the file read once.

reading settings.json appears once, although the configuration was used three times from two modules, and the last line confirms both modules received the same object. Every part of the program agrees on the currency and the tax rate because there is only one place they are held.

Look at invoice_total(net) from outside. Its signature says it needs a number. It also needs a settings file at a particular path, read through a class it never mentions in its parameters, and any test of it reads that file too. That hidden dependency is the price of the global access point, and in tests it lets one test change what the next one reads.