Use Pathlib in Your Django Settings File

2020-03-16 Path through the Sahara

Django’s default settings file has always included a BASE_DIR pseudo-setting. I call it a “pseudo-setting” since it’s not read by Django itself. But it’s useful for configuring path-based settings, it is mentioned in the documentation, and some third party packages use it.

(One that I maintain, the Scout APM Python integration, uses it.)

Django has, up until now, defined it as:

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

This changes in version 3.1, which as I write is still months in the future. Thanks to a contribution by Jon Dufresne and Curtis Maloney, it’s instead defined using pathlib:

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent

Note this is in the new project template only. If you upgrade an older project to Django 3.1, your settings file won’t be changed.

pathlib was added to Python’s standard library in Python 3.4, thanks to PEP 428. All file-path using functions across Python were then enhanced to support pathlib.Path objects (or anything with a __fspath__ method) in Python 3.6, thanks to PEP 519.

pathlib is great! It has an easier API than os.path.join(), allows method chaining, and handles path normalization automatically. See how you can define a subdirectory using BASE_DIR / 'subdir'. If you want to read more, see Trey Hunner’s articles Why you should be using pathlib and No really, pathlib is great.

Even though this is in the future (or maybe not, if you are in the future), you can convert your projects to use it today. (Or, if you are using Django 3.1+, but your project was started before, you can convert.)

If you haven’t used pathlib before, I think this is a great place to try it out.

To migrate, you’d pretty much want to copy the commit from Django itself. First copy the new definition of BASE_DIR, as above. Then, update your other settings that use it. For example, the documentation changed around defining STATICFILES_DIRS, from:

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
    '/var/www/static/',
]

to:

STATICFILES_DIRS = [
    BASE_DIR / "static",
    '/var/www/static/',
]

Update (2020-06-15): Rewrote the following, thanks to John Sandall.

In theory, on Python 3.6+, Path() objects should be accepted anywhere that strings are. This should mean you don’t need to worry about converting all uses at the same time, as the old os.path.join() pattern accepts Path() objects as well. However in practice some code paths still expect strings as they use non-path operations.

For example some settings in Django that expected strings were found and had Path() support added in commit 77aa74cb (again, for Django 3.1). For older versions or third party package settings, you will probably need to use str() around the result.

For example, if you are using the SQLite database backend, before opening the database file it checks if the path contains "mode=memory". For this to work, you’ll need to use str() when passing it the path in NAME:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": str(BASE_DIR / "db.sqlite3"),
    }
}

For any other errors you might encounter, using str() should fix them.

Fin

Thanks to Carlton Gibson, Curtis Maloney, Jon Dufresne, and Nick Pope for changing this in Django 3.1. Hope this helps your use of paths,

—Adam


Working on a Django project? Check out my book Speed Up Your Django Tests which covers loads of best practices so you can write faster, more accurate tests.


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: django, python