Project Settings Architecture
The project settings architecture is built around a lazy-loading proxy that manages the transition from default global configurations to project-specific overrides. This system ensures that settings are only loaded when needed and provides a consistent interface for both module-based configuration and manual runtime overrides.
The Settings Entry Point
The primary interface for configuration is the settings object, defined in django/conf/__init__.py. It is an instance of LazySettings, which acts as a proxy for the actual configuration data.
# django/conf/__init__.py
settings = LazySettings()
By using a lazy proxy, the framework can be imported without immediately requiring a configuration module. The actual loading process is deferred until the first time a setting attribute is accessed.
Lazy Loading with LazySettings
LazySettings inherits from django.utils.functional.LazyObject. Its core responsibility is to determine which concrete settings implementation to use and to cache accessed values for performance.
When an attribute is accessed, LazySettings triggers its _setup() method if it hasn't been initialized yet. This method looks for the DJANGO_SETTINGS_MODULE environment variable:
# django/conf/__init__.py
def _setup(self, name=None):
settings_module = os.environ.get(ENVIRONMENT_VARIABLE)
if not settings_module:
# Raises ImproperlyConfigured if no module is defined
# and configure() hasn't been called.
...
self._wrapped = Settings(settings_module)
Attribute Caching
To avoid repeated lookups and processing (like path prefixing), LazySettings caches values in its own __dict__ after the first access:
# django/conf/__init__.py
def __getattr__(self, name):
if (_wrapped := self._wrapped) is empty:
self._setup(name)
_wrapped = self._wrapped
val = getattr(_wrapped, name)
# Special case modification for relative paths
if name in {"MEDIA_URL", "STATIC_URL"} and val is not None:
val = self._add_script_prefix(val)
self.__dict__[name] = val
return val
Module-Based Configuration: The Settings Class
The Settings class is the concrete implementation used when loading from a Python module (e.g., myproject.settings). It performs a two-stage merge:
- Global Defaults: It first loads all uppercase attributes from
django.conf.global_settings. - Project Overrides: It then imports the module specified by
DJANGO_SETTINGS_MODULEand overrides the defaults with any uppercase variables found there.
Validation and Initialization
The Settings class performs critical validation during initialization:
- Type Checking: Settings like
INSTALLED_APPS,ALLOWED_HOSTS, andTEMPLATE_DIRSare forced to be lists or tuples. - Timezone Setup: If
TIME_ZONEis set, it attempts to validate the zone against/usr/share/zoneinfoand updatesos.environ["TZ"].
# django/conf/__init__.py
class Settings:
def __init__(self, settings_module):
for setting in dir(global_settings):
if setting.isupper():
setattr(self, setting, getattr(global_settings, setting))
mod = importlib.import_module(settings_module)
for setting in dir(mod):
if setting.isupper():
setting_value = getattr(mod, setting)
# Validation logic for tuple_settings...
setattr(self, setting, setting_value)
Manual Configuration: UserSettingsHolder
In scenarios where a settings module is not appropriate (such as standalone scripts or tests), settings can be configured manually using settings.configure(). This internalizes a UserSettingsHolder object.
UserSettingsHolder acts as a dynamic container that wraps a set of default settings (usually global_settings). It allows for runtime modification and deletion of settings while falling back to the defaults for any unprovided values.
# django/conf/__init__.py
class UserSettingsHolder:
def __init__(self, default_settings):
self.__dict__["_deleted"] = set()
self.default_settings = default_settings
def __getattr__(self, name):
if not name.isupper() or name in self._deleted:
raise AttributeError
return getattr(self.default_settings, name)
Settings in Testing
The architecture supports temporary configuration changes during testing via the override_settings utility found in django/test/utils.py.
When override_settings is used, it creates a new UserSettingsHolder that wraps the current settings._wrapped object. It then replaces the _wrapped attribute of the global settings proxy.
# django/test/utils.py
def enable(self):
override = UserSettingsHolder(settings._wrapped)
for key, new_value in self.options.items():
setattr(override, key, new_value)
self.wrapped = settings._wrapped
settings._wrapped = override
Because LazySettings.__setattr__ monitors the _wrapped attribute, it automatically clears its internal cache whenever _wrapped is replaced, ensuring that the overridden values are immediately visible to the rest of the framework.
Key Constraints
- Uppercase Requirement: Only attributes with all-uppercase names are recognized as settings. Both
SettingsandUserSettingsHolderenforce this during initialization and attribute access. - Immutability: While the
settingsobject itself is a proxy, the underlyingSettingsclass does not provide a public API for modification after initialization. Runtime changes should be handled viaoverride_settingsorsettings.configure(). - Configuration Order:
settings.configure()must be called before any settings are accessed. If_setup()has already run, callingconfigure()will raise aRuntimeError.