Django settings in 4 parts

The basics

Your Django settings are conceptually fairly simple, and should be kept simple in practice. For most purposes it's generally best to just start with three modules under your (project).settings package…

  • base.py for the vast majority of defaults not affected by the environment
  • dev.py, importing everything from base.py and adding dev/debug settings
  • prod.py, importing everything from base.py and adding production settings

I'd also go so far as to argue that your concrete settings files are literally the only place in a codebase where it's acceptable to just "import star", though hopefully you're using Ruff or a similar linter, and at least acknowledge the potential folly with an ignore directive…

from .base import *  # noqa: F403

Something fishy going on

Some time into development, after naïvely adding a lot of "dev-mode-and-debugging" stuff to my dev.py, I noticed a few of things:

  • My dev servers (which were DigitalOcean droplets with 2cpus & 8 gigs of ram at the time) were running out of memory a lot more often than they should
  • runserver_pluswas taking ages to start serving pages
  • django-admin commands were taking unusually long to run
  • mypy was taking yonks to do its thing, and mypyd was the main process eating up all of my precious memory

The diagnosis

So yes, the thing is, if your "dev settings" just turn everything on at the module level then all those things are going to be imported and run whenever any Django management command runs.

If you've moved at all beyond the basics, that might mean you're loading django-extensions and the Django Debug Toolbar into your code with every command, and if you've stepped into the wonderful world of static type-checking you might be letting django-stubs monkey-patch your code. (Or at least I was…)

But: I found I don't actually need django-extensions for anything at all besides runserver(_plus), and monkey-patching type-hints into Django is resource intensive, and only needed for mypy[d].

The solutions

Conditional settings & discovering the execution context

Django doesn't provide an immediately obvious way for you to tell exactly what spawned the code that's currently being imported…

Your manage.py is a simple little file that generally remains untouched, and acts as the entry-point for the django-admin command (assuming you aren't already just running things with python manage.py) – but people can and do often augment it for their own purposes and this is an ideal time to add a couple of lines before that final execute_from_command_line(argv) call:

if len(argv) >= 2:  # noqa: PLR2004
    environ.setdefault("DJANGO_MANAGEMENT_COMMAND", argv[1])

from django.core.management import execute_from_command_line

execute_from_command_line(argv)

And now you can selectively and exclusively import django-extensions and all the DJDT stuff when you know you're in a runserver or runserver_plus context with something like like this in your (project).settings.dev module:

if getenv("DJANGO_MANAGEMENT_COMMAND", "").startswith("runserver"):
    RUNSERVERPLUS_POLLER_RELOADER_TYPE = "watchdog"
    RUNSERVER_PLUS_EXCLUDE_PATTERNS = [
        ".venv/*",
        ".vscode/*",
        "node_modules/*",
        "media/*",
        "src/migrations/*",
        "static/*",
    ]

    # Enable Django Debug Toolbar and runserver_plus
    INSTALLED_APPS += [
        "debug_toolbar",
        "django_extensions",
    ]
    MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware", *MIDDLEWARE]

(and yes there are a few hints for other optimisations you should consider if you haven't already, embedded in that block of code.)

Settings exclusive to mypy (or whatever the kids are typing-checking with these days)

This brings us to the "in 4 parts" bit of this post. I now have a forth settings module in the form of mypy.py which consists entirely of:

"""Django settings for the mypy type-checking daemon."""

import django_stubs_ext

from .dev import *  # noqa: F403

django_stubs_ext.monkeypatch()

… and then we can ensure the heavy burden of monkey-patching Django with type-hinting stubs happens when (and only when) needed by adding adding to our pyproject.toml:

[tool.django-stubs]
django_settings_module = "(project).settings.mypy"

– that means:

  1. any mypyd processes VS Code might launch if you've got an extension like matangover.mypy installed, and
  2. the mypy process itself, which is great to run as a pre-commit hook.

The result…

  • runserver[_plus] need not waste your resources on loading Django stubs
  • mypy[d] won't be wasting cycles & memory on debug stuff meant only for you
  • All the other Django management commands aren't bogged down by your debugging code or monkey-patched type-hints

… and you can be happier, and iterate faster, on smaller development boxes. And isn't that what it's really all about?