Manage Django settings with Pydantic - Part 1

Photo by Matt Artz on Unsplash

Manage Django settings with Pydantic - Part 1

without breaking your app

When our django project grows, we generally have to manage more and more settings: databases, security, cache, mails… And it quickly becomes a mess, scrolling hundreds of lines to change the CSRF header.

In this series of articles, we will see how to use pydantic to manage django settings. It will help us to better handle scaling issues while making our codebase far cleaner (yes we love python hacks that do what the framework does not allow natively). The main point here is that we will not modify files other than settings.py, so we won't break our apps!

This first article deals with the basic integration of pydantic inside a django app.

Off-the-shelf solution

If, like me, you really want to use pydantic inside your django app, the first option you have, is to install a third-party package like django-pydantic-settings. The main drawback is that you have to modify the manage.py and wsgi.py files. For me, this is not a good practice to start breaking the framework.

But why do we need to modify these files? In the django world, we call module variables

from django.conf import settings

option = settings.MY_OPTION

Unfortunately pydantic manages very well settings... through a dedicated python class instead (BaseSettings)!

from pydantic import BaseSettings

class Settings(BaseSettings):
    auth_key: str
    api_key: str = Field(..., env='my_api_key')
    # other settings...

Turning an object into a module

Not to break either django or pydantic, the trick is to fake the module variables. Let us define a minimal settings module:

# settings.py

DEBUG = True

Here the app can basically access settings.DEBUG, but actually we can hook this access through the __getattr__ python builtin function:

# settings.py
from typing import Any

def __getattr__(name: str) -> Any:
    if name == "DEBUG":
        return True

If you restart the app, an error raises: CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False. Weird, because DEBUG is well set to True.

This issue comes from the internal workings of the framework: django does not see any DEBUG variable inside the module, so it assumes it is False and then complains because other attributes are not set... So we need to fake the variable listing. How? Once again with a builtin function: __dir__.

# settings.py
from typing import Any, List

def __dir__() -> List[str]:
    return ["DEBUG"]

def __getattr__(name: str) -> Any:
    if name == "DEBUG":
        return True

\o/ Now it starts perfectly!

Pydantic, your turn!

Henceforth, we know how to hook settings access, so we just have to redirect them to a pydantic BaseSettings object. In particular, we must use its dictionary representation to coerce the access into being coherent with the framework (nested settings are generally structured through dict).

# settings.py
from typing import Any, List
from pydantic import BaseSettings

class DjangoSettings(BaseSettings):
    """Manage all the project settings"""
    DEBUG: bool = True
    TIME_ZONE: str = "Europe/Paris"
    ALLOWED_HOSTS: List[str] = []

_settings = DjangoSettings() # settings 
_settings_dict = _settings.dict() # dict representation

def __dir__() -> List[str]:
    """The list of available options are retrieved from 
    the dict view of our DjangoSettings object.
    """
    return list(_settings_dict.keys())

def __getattr__(name: str) -> Any:
    """Turn the module access into a DjangoSettings access"""
    return _settings_dict[name]

Now we have a good setup to use all the power of pydantic.

In the next article, we will look at some common use cases we are generally facing in django app development.