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:
- Rename the project directory to
config- This makes it immediately clear that this directory contains configuration rather than application logic. - Split settings and URLs into subdirectories -
config/settings/andconfig/urls/allow for clean environment-specific configurations (local, staging, production). - Create a dedicated
appsdirectory - 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.pysrc/myproject/config/__init__.pysrc/myproject/config/settings.pysrc/myproject/config/urls.pysrc/myproject/config/asgi.pysrc/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.pysrc/myproject/config/wsgi.pysrc/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 checkandrunserverwork out of the box without relying on an environment variable or.envrc. TheDJANGO_SETTINGS_MODULEenvironment 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.pathis convenient for short app names likemyapp, but it is order-sensitive and can cause confusing import errors. An alternative is to avoidsys.pathchanges 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).