I love Django as a framework. It's modularity makes building new and complex projects a breeze. When Django Ninja came around and layered Pydantic Schema onto the framework for serialization I was over the moon.
That said, Django's settings.py
is a forever thorn in my side. I've tried following the canonical advice from the Django Wiki with varying but mostly disappointing results. I invested some time and came up with a configuration pattern using Pydantic Settings that I'm very happy with and it seemed worth sharing.
1. Define your config
Every good configuration needs some kind of specification.
from functools import lru_cache
from pathlib import Path
from typing import List
import structlog
from pydantic import BaseModel
from pydantic_settings import (
BaseSettings,
SettingsConfigDict,
)
CURRENT_DIR = Path(__file__).resolve().parent
class Database(BaseModel):
name: str
user: str
password: str
host: str
port: int
class CORS(BaseModel):
allowed_origins: List[str] = ["*"]
class Configuration(BaseSettings):
model_config = SettingsConfigDict(
env_file=(CURRENT_DIR / ".env", CURRENT_DIR / ".env.prod"),
env_file_encoding="utf-8",
env_nested_delimiter="__",
)
debug: bool = False
database: Database
allowed_hosts: List[str] = ["*"]
cors: CORS
secret_key: str
@lru_cache()
def get_config() -> Configuration:
return Configuration()
You can read about most of this in Pydantics docs, but I'll break down the big functions. Configuration
defines the configuration. The model_config
has some specific instructions to load .env
files in an ordered precedence. It also specifies a delimiter for defining nested configuration through environment variables.
Configuration items can be nested; the Database
class for instance is just a bunch of nested fields. If I wanted, I could even write a computed field to produce a DSN string from these details. You also get all the same things you get from Pydantic schema. Things like validation, defaults, etc
Last is the get_config()
function which is backed by an LRU Cache. This is so that we don't repeatedly read configuration from disk, which would be costly.
2. Use it in settings.py
The rest is pretty straight forward. You import get_config()
:
from core.config import get_config
config = get_config()
and then use it:
SECRET_KEY = config.secret_key
Now when I want to swap in different configurations I can name them differently and add them to the list in step 1.