How I structure Django projects for better maintainability 2025

When starting a new Django project I've found myself tweaking the default project structure to make it more maintainable and organized. Here's my preferred layout and the small adjustments needed to make it work from an empty directory through to a running project.

The default Django layout works fine, but I prefer using a src directory to cleanly separate the Django application code from project configuration files (pyproject.toml, README.md, etc.).

Why I Prefer This Structure

I make a few organizational changes that help keep larger projects maintainable:

  1. Rename the project directory to config - This makes it immediately clear that this directory contains configuration rather than application logic.
  2. Split settings and URLs into subdirectories - config/settings/ and config/urls/ allow for clean environment-specific configurations (local, staging, production).
  3. Create a dedicated apps directory - This keeps all your Django applications organized in one place, separate from configuration.

Setting Up the Project

These steps assume you have Django installed (for example, via uv add django or pip install django).

1. Create the layout

Create the repository directory and the src/<projectname> directory that will hold your Django project:

mkdir myproject
cd myproject
mkdir -p src/myproject

The outer directory is your repository or project name. The inner config package is what django-admin startproject would normally have named after your project; we rename it to config to make its purpose obvious.

2. Run django-admin startproject

Run the startproject command with config as the project name:

django-admin startproject config src/myproject

This creates:

  • src/myproject/manage.py
  • src/myproject/config/__init__.py
  • src/myproject/config/settings.py
  • src/myproject/config/urls.py
  • src/myproject/config/asgi.py
  • src/myproject/config/wsgi.py

3. Create the apps directory

Create a directory for your applications:

mkdir -p src/myproject/apps
touch src/myproject/apps/__init__.py

4. Convert config/settings.py into a package

Create a settings package and move the generated file into it:

mkdir src/myproject/config/settings
touch src/myproject/config/settings/__init__.py
mv src/myproject/config/settings.py src/myproject/config/settings/default.py

Create a local settings file:

# src/myproject/config/settings/local.py
from .default import *

5. Convert config/urls.py into a package

Do the same for URLs:

mkdir src/myproject/config/urls
touch src/myproject/config/urls/__init__.py
mv src/myproject/config/urls.py src/myproject/config/urls/default.py

You can optionally create a local URL file:

# src/myproject/config/urls/local.py
from .default import urlpatterns

6. Update generated files that reference the old settings module

After renaming the project package to config, the settings module is no longer myproject.settings -- it is config.settings.local. The following files were generated by django-admin startproject and still contain the original default:

  • src/myproject/manage.py
  • src/myproject/config/wsgi.py
  • src/myproject/config/asgi.py

In each file, change:

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

to:

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local')

Tip: Setting this directly in the generated files means manage.py check and runserver work out of the box without relying on an environment variable or .envrc. The DJANGO_SETTINGS_MODULE environment variable can still be used to override it (for example, in production or staging).

7. Set the DJANGO_SETTINGS_MODULE environment variable

If you use direnv, add this to .envrc:

# .envrc
export DJANGO_SETTINGS_MODULE="config.settings.local"

Or set it manually whenever you work on the project:

export DJANGO_SETTINGS_MODULE="config.settings.local"

Required Configuration Changes

A few small changes inside config/settings/default.py are still required.

1. Adjust BASE_DIR

Because the config/settings directory is now nested one level deeper, BASE_DIR needs an additional .parent:

# config/settings/default.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent.parent

2. Update ROOT_URLCONF

Since urls.py was moved into a package, Django needs the new module path:

# config/settings/default.py
ROOT_URLCONF = "config.urls.default"

3. Add the apps directory to the Python path

Also in config/settings/default.py, add the apps directory to the Python path so Django can find your applications with short names:

# config/settings/default.py
import os
import sys

sys.path.insert(0, os.path.join(BASE_DIR, "apps"))

Note: Mutating sys.path is convenient for short app names like myapp, but it is order-sensitive and can cause confusing import errors. An alternative is to avoid sys.path changes and import apps using their full package path (myproject.apps.myapp).

Running the Project

Because manage.py is nested inside src/myproject/, run commands like this:

cd src/myproject
python ./manage.py check
python ./manage.py migrate
python ./manage.py runserver

Example Directory Structure

.
├── Justfile
├── README.md
├── pyproject.toml
├── src
│   └── myproject
│       ├── __init__.py
│       ├── apps
│       │   ├── __init__.py
│       │   └── mycoolapp
│       │       ├── __init__.py
│       │       ├── admin.py
│       │       ├── apps.py
│       │       ├── migrations
│       │       │   └── __init__.py
│       │       ├── models.py
│       │       ├── templates
│       │       │   └── mycoolapp
│       │       │       └── ...
│       │       ├── tests.py
│       │       ├── urls.py
│       │       └── views.py
│       ├── config
│       │   ├── __init__.py
│       │   ├── asgi.py
│       │   ├── settings
│       │   │   ├── __init__.py
│       │   │   ├── default.py
│       │   │   └── local.py
│       │   ├── urls
│       │   │   ├── __init__.py
│       │   │   ├── default.py
│       │   │   └── local.py
│       │   └── wsgi.py
│       ├── db.sqlite3
│       └── manage.py
└── uv.lock

Verification

Run the Django system check:

cd src/myproject
python ./manage.py check

Expected output:

System check identified no issues (0 silenced).