Using Pydantic Settings with Django and Django Ninja

matt

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.

Back to top