Django Dynamic Settings
When building Django applications, one of the features that makes your application more robust and maintainable is the ability to change settings dynamically. By default, Django loads configurations from a static settings.py
file, but as your application grows, you may need to update settings based on environment variables, database values, or user preferences without restarting the application.
Understanding Static vs Dynamic Settings
Static Settings (Default Approach)
In a typical Django project, settings are defined in the settings.py
file:
# settings.py
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
These settings are loaded when the Django application starts and remain constant throughout the application's lifecycle. To change them, you need to modify the file and restart the application.
Dynamic Settings (Enhanced Approach)
Dynamic settings allow you to:
- Change configurations without restarting the application
- Use different settings for different environments
- Allow administrators to modify settings through an admin interface
- Override settings based on external factors
Approaches to Implementing Dynamic Settings
Let's explore several ways to implement dynamic settings in Django.
1. Environment Variables
One of the simplest approaches is to use environment variables:
# settings.py
import os
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
SECRET_KEY = os.environ.get('SECRET_KEY', 'default-development-key')
DATABASE_URL = os.environ.get('DATABASE_URL', 'sqlite:///db.sqlite3')
Advantages:
- Easy to configure for different environments
- Works well with containerization platforms (Docker, Kubernetes)
- No need to store sensitive information in version control
Example with python-decouple:
# settings.py
from decouple import config
DEBUG = config('DEBUG', default=False, cast=bool)
SECRET_KEY = config('SECRET_KEY', default='default-development-key')
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: v.split(','))
To run your application with different settings:
# Production environment
DEBUG=False SECRET_KEY=my_production_key python manage.py runserver
# Development environment
DEBUG=True python manage.py runserver
2. Database-Stored Settings
For truly dynamic settings that can be changed without application restarts:
- First, create a model to store settings:
# app/models.py
from django.db import models
class SiteSetting(models.Model):
key = models.CharField(max_length=50, unique=True)
value = models.TextField()
def __str__(self):
return self.key
- Create a settings manager:
# app/settings_manager.py
from django.core.cache import cache
from .models import SiteSetting
def get_setting(key, default=None):
# Try to get from cache first
cache_key = f'site_setting_{key}'
cached_value = cache.get(cache_key)
if cached_value is not None:
return cached_value
try:
setting = SiteSetting.objects.get(key=key)
# Cache the result
cache.set(cache_key, setting.value, timeout=3600) # Cache for 1 hour
return setting.value
except SiteSetting.DoesNotExist:
return default
def set_setting(key, value):
setting, created = SiteSetting.objects.update_or_create(
key=key,
defaults={'value': value}
)
# Update cache
cache_key = f'site_setting_{key}'
cache.set(cache_key, value, timeout=3600)
return setting
- Use the settings in your application:
from .settings_manager import get_setting
# Get a dynamic setting, with a fallback to a default value
maintenance_mode = get_setting('MAINTENANCE_MODE', 'False') == 'True'
site_title = get_setting('SITE_TITLE', 'My Django Site')
3. Creating a Custom Settings Module
You can create a custom settings module that combines static settings with dynamic ones:
# dynamic_settings.py
from django.conf import settings as django_settings
from django.core.cache import cache
from .models import SiteSetting
class DynamicSettings:
def __init__(self):
self._static_settings = django_settings
def __getattr__(self, name):
# First check if it's a Django setting
if hasattr(self._static_settings, name):
return getattr(self._static_settings, name)
# Then look for a dynamic setting
cache_key = f'site_setting_{name}'
value = cache.get(cache_key)
if value is None:
try:
setting = SiteSetting.objects.get(key=name)
value = setting.value
cache.set(cache_key, value, timeout=3600)
except SiteSetting.DoesNotExist:
# Setting doesn't exist, return None
return None
return value
# Create a singleton instance
settings = DynamicSettings()
Then in your code, you can use:
from .dynamic_settings import settings
# Access static Django settings
debug_mode = settings.DEBUG
# Access dynamic settings
maintenance_mode = settings.MAINTENANCE_MODE == 'True'
Building an Admin Interface for Dynamic Settings
To make it easy for administrators to change settings, you can create a custom admin interface:
# admin.py
from django.contrib import admin
from .models import SiteSetting
@admin.register(SiteSetting)
class SiteSettingAdmin(admin.ModelAdmin):
list_display = ('key', 'value')
search_fields = ('key', 'value')
def has_delete_permission(self, request, obj=None):
# Prevent accidental deletion of critical settings
return request.user.is_superuser
For a more user-friendly interface, you could create a custom form with validation for specific setting types:
# forms.py
from django import forms
from .models import SiteSetting
class SiteSettingForm(forms.ModelForm):
class Meta:
model = SiteSetting
fields = ['key', 'value']
def clean(self):
cleaned_data = super().clean()
key = cleaned_data.get('key')
value = cleaned_data.get('value')
# Validate boolean settings
if key.endswith('_ENABLED') or key.endswith('_MODE'):
if value not in ['True', 'False']:
raise forms.ValidationError(f"{key} must be 'True' or 'False'")
return cleaned_data
# admin.py
@admin.register(SiteSetting)
class SiteSettingAdmin(admin.ModelAdmin):
form = SiteSettingForm
# ... rest of admin configuration
Real-World Example: Feature Flags System
Let's implement a complete feature flags system using dynamic settings:
# models.py
from django.db import models
class FeatureFlag(models.Model):
name = models.CharField(max_length=100, unique=True)
is_active = models.BooleanField(default=False)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.name} ({'ON' if self.is_active else 'OFF'})"
# feature_flags.py
from django.core.cache import cache
from .models import FeatureFlag
def is_feature_active(feature_name, default=False):
"""Check if a feature flag is active"""
cache_key = f'feature_flag_{feature_name}'
is_active = cache.get(cache_key)
if is_active is None:
try:
flag = FeatureFlag.objects.get(name=feature_name)
is_active = flag.is_active
# Cache for 5 minutes
cache.set(cache_key, is_active, timeout=300)
except FeatureFlag.DoesNotExist:
is_active = default
return is_active
In your views or templates, you can then check if features are enabled:
# views.py
from .feature_flags import is_feature_active
def my_view(request):
context = {
'show_new_ui': is_feature_active('NEW_UI_ENABLED'),
'enable_comments': is_feature_active('COMMENTS_ENABLED', default=True),
}
return render(request, 'my_template.html', context)
In templates:
{% if show_new_ui %}
{# New UI components here #}
{% else %}
{# Old UI components here #}
{% endif %}
{% if enable_comments %}
{# Comments section here #}
{% endif %}
You could also create a template tag for easier use:
# templatetags/feature_flags.py
from django import template
from ..feature_flags import is_feature_active
register = template.Library()
@register.simple_tag
def feature_enabled(feature_name, default=False):
return is_feature_active(feature_name, default)
Which can be used in templates:
{% load feature_flags %}
{% if feature_enabled 'NEW_DASHBOARD' %}
<div class="new-dashboard">
{# New dashboard content #}
</div>
{% else %}
<div class="old-dashboard">
{# Old dashboard content #}
</div>
{% endif %}
Performance Considerations
When implementing dynamic settings, consider these performance aspects:
-
Cache aggressively: Always cache dynamic settings to avoid database queries on every request.
-
Bulk load settings: Load all settings at once when possible instead of individual queries.
-
Use signals to invalidate cache: When settings are updated, invalidate the relevant cache entries.
# In your models.py
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=SiteSetting)
def invalidate_setting_cache(sender, instance, **kwargs):
cache_key = f'site_setting_{instance.key}'
cache.delete(cache_key)
Best Practices
-
Categorize settings - Group settings by functionality (e.g., email, security, features)
-
Provide defaults - Always include fallback values for dynamic settings
-
Document settings - Keep a record of what each setting does and valid values
-
Validate input - Ensure settings have the correct format and values
-
Limit access - Restrict who can change critical settings
-
Audit changes - Log when important settings are modified and by whom
Summary
Dynamic settings provide flexibility and control over your Django application's behavior. Through environment variables, database-stored settings, or custom settings modules, you can create applications that adapt to changing requirements without code modifications or restarts.
Key takeaways:
- Use environment variables for deployment-specific settings
- Use database-stored settings for runtime-changeable configurations
- Cache settings to maintain performance
- Build appropriate interfaces for managing settings
- Implement feature flags for controlled rollout of new functionality
Additional Resources
- Django Settings Documentation
- python-decouple - A library for separating settings from code
- Django-Constance - A popular dynamic settings app for Django
- Django-Waffle - A feature flags app for Django
Exercises
- Create a simple Django app that uses environment variables for configuration
- Implement a database-backed settings system with an admin interface
- Build a feature flag system that allows gradual rollout (e.g., enabling features for a percentage of users)
- Create a settings module that combines values from settings.py, environment variables, and database in a prioritized order
- Implement a logging system that records changes to critical settings
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)