In Django code, why is copying a setting into a module-level constant or default argument at import time a bug, and what should you do instead?
answer
- evaluated once, when imported
- test overrides cannot reach it
- imports before configuration
- read at call time instead
- setting_changed for derived caches
basics
~20 sA module-level TAX_RATE = settings.PAYROLL_TAX_RATE or a default argument freezes the value when the module is imported: test overrides cannot reach it, and importing the module before settings exist fails. Read settings inside functions at call time instead.
solid answer
~40 sModule-level code and default argument values run once, at import. `TAX_RATE = settings.PAYROLL_TAX_RATE` or `def net(gross, rate=settings.PAYROLL_TAX_RATE)` copies the value then and never looks again. Three bugs follow: `override_settings` in tests changes what `django.conf.settings` returns but not the copied constant, so tests pass or fail depending on import order; importing the module where settings are not yet configured — a tool, a docs build, a library used before `configure()` — raises `ImproperlyConfigured` at import; and the reads also force the settings to load earlier than the rest of the program expects. The fix is to read `settings.PAYROLL_TAX_RATE` inside the function each time; the lazy proxy caches values, so it is cheap. For an expensive object derived from a setting, build it lazily and clear it on the `setting_changed` signal.
go deeper
Remember that module-level code runs once at import, so read settings inside functions rather than copying them into constants.
Explain why override_settings cannot reach import-time copies and why importing such a module without configured settings fails.
Diagnose order-dependent test failures back to import-time reads, and fix derived caches with lazy construction plus a setting_changed receiver.
Set conventions for how apps read configuration, keeping environment reads in settings and runtime code reading settings at call time.
## The pattern that causes trouble Python evaluates module-level statements, class bodies and default argument values **once, when the module is first imported**. Django settings are designed to be read through the lazy `django.conf.settings` proxy at the moment you need them. Copying a setting at import time turns a live lookup into a frozen snapshot. ```python # payroll/calc.py -- the buggy version from django.conf import settings TAX_RATE = settings.PAYROLL_TAX_RATE # read once, at import def net_pay(gross, rate=settings.PAYROLL_TAX_RATE): # default frozen at def time return gross * (1 - rate) ``` ## What goes wrong | Symptom | Cause | |---|---| | a test using `override_settings(PAYROLL_TAX_RATE=...)` still sees the old rate | the override swaps what the proxy returns, but `TAX_RATE` and the default argument hold copies | | tests pass alone and fail in the full suite, or the reverse | whether the module was imported before or during an override decides the frozen value | | importing the module in a script or tool raises `ImproperlyConfigured` | the import reads a setting before `DJANGO_SETTINGS_MODULE` is set or `configure()` is called | | a reusable app forces configuration too early for its host | its modules read settings on import, before the host has configured them | The same applies to class attributes on forms, models or views that read settings in the class body, and to values computed from settings at module level, such as a compiled regex or a client object. ## The fix: read at call time ```python # payroll/calc.py -- fixed from django.conf import settings def net_pay(gross, rate=None): if rate is None: rate = settings.PAYROLL_TAX_RATE # read when called return gross * (1 - rate) ``` - Reading from `settings` inside a function is cheap: the proxy caches each value after the first read and clears the cache when the underlying settings change. - Use `None` as the default and resolve it inside the function, rather than a setting in the signature. - Keep `from django.conf import settings` at module level — importing the proxy is safe; **reading** from it at import is the problem. ## Derived objects that are expensive to build Sometimes a setting feeds something costly — a parsed tax table, an HTTP client with a configured base URL. Build it lazily on first use, cache it, and invalidate the cache when the setting changes in tests. Django sends the **`setting_changed`** signal (documented as `django.test.signals.setting_changed`) whenever `override_settings` or `TestCase.settings()` changes a value: ```python from functools import cache from pathlib import Path from django.conf import settings from django.dispatch import receiver from django.test.signals import setting_changed @cache def tax_table(): return Path(settings.PAYROLL_TAX_TABLE_PATH).read_text().splitlines() @receiver(setting_changed) def _reset_tax_table(*, setting, **kwargs): if setting == "PAYROLL_TAX_TABLE_PATH": tax_table.cache_clear() ``` Django uses the same technique internally for settings it caches. ## Related anti-patterns 1. **Writing to settings at runtime**, such as `settings.PAYROLL_TAX_RATE = 0.2` in a view. The docs say the only place to assign settings is a settings file; runtime writes are per-process and invisible to other workers. 2. **Reading environment variables outside settings.** Scattering `os.environ` reads across modules hides configuration from the settings module; read the environment once in settings and expose it as a setting. 3. **Settings that trigger work on import**, like opening a connection in `settings.py`, which runs for every management command. ## What an interviewer is listening for A senior candidate explains the import-time evaluation rule, links it to flaky tests and to `ImproperlyConfigured` at import, and gives both fixes: read at call time, and for derived objects cache lazily with invalidation on `setting_changed`.
- Is it a problem to write from django.conf import settings at module level?No. Importing the lazy proxy does not load anything, so module-level imports are safe and conventional. The bug is reading an attribute such as `settings.PAYROLL_TAX_RATE` at module level, in a class body or in a default argument, because that forces setup and freezes the value.
- Why does the setting_changed receiver matter outside tests?Normally it does not fire in production, because settings do not change while a process runs. It exists so caches derived from settings stay correct when tests change them with `override_settings`; without it, a cached object built from the first test's value leaks into later tests.
saying these in an interview costs you the question
- Copies settings into module-level constants for speed
- Uses a setting as a default argument value in a function signature
- Thinks importing django.conf.settings at module level is the bug
- Changes settings from a view to adjust behaviour at runtime
- Expects override_settings to update constants copied at import