3.2 KiB
| title | aliases | tags | sources | created | updated | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Module-Level Singletons Break pytest — Use Lazy Initialisation |
|
|
|
2026-04-30 | 2026-04-30 |
Module-Level Singletons Break pytest — Use Lazy Initialisation
Instantiating Settings(), SomeService(), or any object that reads environment variables at module import time causes pytest to fail when those env vars are not set in the test environment — even for tests that never call that module's functions. Python imports all referenced modules on import, so settings = Settings() at the top of config.py runs as soon as any test file imports anything from that package.
Key Points
Settings()at module level runs atimporttime, not at call time- pytest imports modules eagerly — a test for
routes/health.pymay triggerconfig.py→Settings()→ValidationError - The failure looks like a config error, not a test design problem
- Fix: wrap in
@lru_cachefunction or@propertyso instantiation is deferred to first use - Pydantic
BaseSettingsvalidation runs in__init__— there is no "lazy" mode
Details
Anti-Pattern
# config.py ← runs at import time
settings = Settings() # crashes if MONGO_URL not set in test env
# service.py
db_service = DatabaseService() # same problem
Fix 1 — @lru_cache function (recommended for FastAPI)
from functools import lru_cache
@lru_cache
def get_settings() -> Settings:
return Settings()
# Use as FastAPI dependency
@router.get("/")
async def handler(settings: Settings = Depends(get_settings)):
...
Tests can override with app.dependency_overrides[get_settings] = lambda: FakeSettings().
Fix 2 — @property on a config holder
class _Config:
_settings: Settings | None = None
@property
def settings(self) -> Settings:
if self._settings is None:
self._settings = Settings()
return self._settings
config = _Config() # safe — no Settings() call yet
Fix 3 — pytest monkeypatch / .env file
For tests that genuinely need the real Settings, provide env vars via a conftest.py:
@pytest.fixture(autouse=True)
def env_vars(monkeypatch):
monkeypatch.setenv("MONGO_URL", "mongodb://localhost:27017/test")
monkeypatch.setenv("SECRET_KEY", "test-secret")
Why Python 3.14 Makes This Worse
Python 3.14 has no pre-built wheels for Rust-extension packages (pydantic-core, cryptography). Poetry silently installs a pure-Python fallback that may behave differently or be missing functionality. Always pin python = "^3.11" in pyproject.toml and run tests in Docker matching the production Python version.
Related Concepts
- wiki/concepts/poetry-docker-version-mismatch — Poetry / Python version mismatch causing silent failures
- wiki/concepts/time-sleep-blocks-asyncio — another class of import-time footgun in async FastAPI
Sources
- daily/2026-04-30.md — Session 13:36, test suite fixes; module-level Settings() crashes, aiohttp mock pattern