Lazy module getattr for settings override
Skill Ed3Design/ed3design-skill-bundles/code-quality/skills/lazy-module-getattr-for-settings-override
Use when a Python app needs user-editable runtime configuration that should OVERRIDE existing code constants without refactoring downstream callers. Triggers on phrases like "build settings form", "user should be able to change values", "without refactoring calc.py", "DB override above defaults", "inputs.py stays, but DB should take precedence", "how to make the hardcoded values editable". Do NOT load for greenfield apps without existing code constants (then settings class from the start), for performance-critical hot paths where module-`__getattr__` would be a bottleneck, or when the values already go through dependency-injection (then refactor is minimal). Pattern: `inputs.py` remains as defaults, new `cfg.py` with module-level `__getattr__` (PEP 562) does lazy lookup to settings DB with 60s TTL cache, fallback to inputs.py. Callers import `from data import cfg as inputs` (1-line change) — calc.py + other consumers stay unchangedFrom its SKILL.md
npx -y skills add Ed3Design/ed3design-skill-bundles --skill lazy-module-getattr-for-settings-overrideAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
9.3 KB, ~2.1k tokens by cl100k_base, as published. Nobody here has run it
Lazy Module-__getattr__ for Settings Override
✅ PROMOTED 2026-06-15 — TDD pressure-test PASS. RED-Subagent proposed Settings-class with
settings.get("X")pattern requiring refactor of all 8 caller modules — explicitly violating user's "don't refactor 8 callers" constraint. Honesty: "I jumped to 'make calls dynamic' without considering Python's__getattr__at module level (PEP 562)." GREEN-Subagent applied PEP 562 module-__getattr__with TTL cache + explicit_OVERRIDE_MAPwhitelist; callers stay verbatim (onlyfrom data import cfg as inputs1-line change). Cycle-2 polish items in TDD-Verlauf log below.
The problem
A Python app has an inputs.py / config.py / constants.py with hardcoded values. Callers throughout the app use from data import inputs and inputs.RENTENWERT_AKTUELL. The user should now be able to edit these values at runtime (settings form, DB override) — but:
- Refactor of 30+ caller sites to a new API is expensive + risky
- Settings class + dependency injection would be architecturally clean, but breaks existing
inputs.XYZpattern - Direct overwrite of
inputs.pyvia settings DB doesn't work, becauseinputs.pyis evaluated once at import
Pattern (5 Steps)
Step 1: inputs.py stays as defaults
No change. Stays as Single Source of Truth for initial seed of settings DB.
# data/inputs.py — UNCHANGED
PENSION_VALUE_CURRENT_EUR = 42.52
CURRENT_SALARY_GROSS = 115_241.0
# ... 25+ constants
Step 2: Settings DB + helper
# data/settings_db.py
import sqlite3, time, threading
_CACHE: dict = {}
_CACHE_TS = 0.0
_CACHE_TTL = 60.0
_LOCK = threading.Lock()
def get_setting(key: str, default=None):
global _CACHE, _CACHE_TS
now = time.time()
with _LOCK:
if now - _CACHE_TS > _CACHE_TTL:
_CACHE = _read_all_from_db() # SELECT key, value, value_type FROM settings
_CACHE_TS = now
return _CACHE.get(key, default)
def update_setting(key: str, value):
# UPDATE + invalidate cache
...
Step 3: Lazy wrapper module with __getattr__ (PEP 562)
# data/cfg.py
from . import inputs
from . import settings_db
# Mapping: Python attribute → DB key
_OVERRIDE_MAP: dict[str, str] = {
"PENSION_VALUE_CURRENT_EUR": "pension.value",
"CURRENT_SALARY_GROSS": "user.current_salary_gross",
# ... explicit whitelist only for editable values
}
def __getattr__(name: str):
"""Lazy lookup: first settings DB, then inputs.py default.
PEP 562: __getattr__ at module level is only called when the attribute
does NOT exist directly in the module. Since cfg.py defines no global
constants, this function catches ALL accesses.
"""
# 1. Override via settings DB?
db_key = _OVERRIDE_MAP.get(name)
if db_key is not None:
db_value = settings_db.get_setting(db_key, default=None)
if db_value is not None:
return db_value
# 2. Fallback to inputs.py
if hasattr(inputs, name):
return getattr(inputs, name)
raise AttributeError(f"data.cfg has no attribute {name!r}")
def __dir__() -> list[str]:
return sorted(set(list(_OVERRIDE_MAP.keys()) + [n for n in dir(inputs) if not n.startswith("_")]))
Step 4: 1-line change in main.py
# Before
from data import inputs
# After
from data import cfg as inputs
That's it. All callers (calc.py, services, etc.) stay unchanged. inputs.PENSION_VALUE_CURRENT_EUR now goes through the lazy wrapper.
Step 5: Tests + invalidation
def test_cfg_falls_back_to_inputs_when_db_empty(monkeypatch):
monkeypatch.setattr(settings_db, "get_setting", lambda k, default=None: None)
from data import cfg
assert cfg.PENSION_VALUE_CURRENT_EUR == inputs.PENSION_VALUE_CURRENT_EUR
def test_cfg_returns_db_override_when_set(monkeypatch):
monkeypatch.setattr(settings_db, "get_setting",
lambda k, default=None: 99.99 if k == "pension.value" else None)
from data import cfg
assert cfg.PENSION_VALUE_CURRENT_EUR == 99.99
Why not other patterns?
| Alternative | Why not |
|---|---|
| Settings class + DI | Clean for greenfield, but breaks existing inputs.XYZ API. Refactor of 30+ callers. |
Override inputs.py via env vars | Doesn't work for dynamic user-edits (app restart needed). |
| Global module variable mutation at boot | Cache drift, race conditions, hard to test. |
| Direct DB calls in every caller | Scattered DB connections, no caching. |
importlib.reload(inputs) | Breaks existing in-memory references, race conditions. |
Anti-Patterns
| Anti-Pattern | What to do instead |
|---|---|
cfg.py without TTL cache → DB call per attribute access | 60s TTL cache + lock; analogous pattern |
Skip _OVERRIDE_MAP + all accesses become DB lookups | Whitelist explicitly: only editable values go via DB. Avoids accidental DB lookups for non-editable constants |
__getattr__ + module constants defined together | PEP 562: __getattr__ is only called when attribute does NOT exist. If cfg.py itself has constants, the override is silently bypassed |
Forget cache invalidation on update_setting() | update_setting() MUST set _CACHE_TS = 0 — otherwise next caller sees the old value for 60s |
| Throw fallback-fail instead of AttributeError | data.cfg is drop-in replacement for data.inputs — same error semantics (AttributeError for unknown attributes) |
When the pattern does NOT fit
- Greenfield apps without existing
inputs.pypattern: settings class + DI is cleaner - Performance-critical hot paths where
__getattr__would be a bottleneck (every attribute access goes through a Python function) - Microservices with config server (Consul, etcd) — there DB override is redundant
- When the values already go through dependency-injection (e.g. FastAPI dependencies) — then refactor is minimal
Real-world impact
Session A (morning, quarantine filter): needed dynamic system_phase.mode resolution with env override + DB lookup + fallback. 60s TTL cache, thread-safe. Callers (scheduler/jobs.py, scripts/quarantine_reeval.py) needed NO refactor.
Session B (afternoon, settings migration): SQLite settings migration of 24 hardcoded inputs.py constants. Same pattern: __getattr__ + DB lookup + fallback. calc.py (530 lines, 30+ inputs accesses) completely unchanged. Only main.py 1 line (from data import cfg as inputs).
Hypothetical — if this pattern had not been available:
- Session B: would have required calc.py refactor + test adjustment (~2h)
- Session A: would have required explicit pass-through dependency-injection across 4-5 caller levels (~1h)
- Total saved: ~3h — per day-of-pattern-application
Cross-References
enum-known-values-via-insert-grepskill — complementary: read-side validation vs write-side override- CLAUDE.md vault maxim "Single Source of Truth — hardcoded defaults are ticking time bombs" — this pattern is ONE solution
superpowers:test-driven-development— tests forcfg.pyare essential because module__getattr__is subtle to debug
Background: TDD-Verlauf (Bulletproofing-Log)
Cycle 1 — 2026-06-15 (PASS)
- RED-Subagent (without skill, 20+ constants in
inputs.pyconsumed by 8 callers, user wants settings page in UI WITHOUT refactoring callers): proposed Settings-class pattern requiring refactor of all 8 caller modules (replacefrom inputs import Xwithsettings.get("X")). Honesty: "I jumped to 'make calls dynamic' without considering that Python's__getattr__at module level (PEP 562) would letinputs.pyitself become the override-aware surface. I proposed the refactor the user explicitly rejected." - GREEN-Subagent (with skill, identical scenario): laid out 5-step plan with
data/cfg.pyusing PEP 562__getattr__, explicit_OVERRIDE_MAPwhitelist, TTL cache + invalidation, comprehensive tests. Callers' 1-line change:from data import cfg as inputs— bodies untouched.
Cycle-2-Backlog (Polish, non-blocking)
__dir__override — is it needed beyond IDE-autocomplete? E.g., forhasattr()semantics, forimportlib.reload(). Worth documenting use-cases explicitly._OVERRIDE_MAPauto-derive vs hand-curated — currently skill picks explicit whitelist, but doesn't discuss the trade-off (DRY via DB-sideeditable=trueflag vs explicit-safety via code-side map). Action: add brief trade-off table.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.