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 frombase.py
and adding dev/debug settings -
prod.py
, importing everything frombase.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_plus
was taking ages to start serving pages -
django-admin
commands were taking unusually long to run -
mypy
was taking yonks to do its thing, andmypyd
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:
- any
mypyd
processes VS Code might launch if you've got an extension likematangover.mypy
installed, and - 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?