Django Module 1 – Django foundations
Set up a Django 6.1 project
Pin Django 6.1 in a virtual environment, generate a project and an app, run the development server, read the settings and choose a release line.
What you will learn
- Create a virtual environment with venv or uv and install a pinned Django version
- Generate a project and an app, run the development server and read the generated settings
- Connect an app's URLs to the project with include(), and fix the setup mistakes that stop a first project
- Explain Django's release lines (5.2 LTS, 6.1, 6.2 LTS, Django 2028) and choose one for a new project
Before you start
What you’ll learn · Before you start
What you will learn
- Create a virtual environment with venv or uv and install a pinned Django version
- Generate a project and an app, run the development server and read the generated settings
- Connect an app's URLs to the project with include(), and fix the setup mistakes that stop a first project
- Explain Django's release lines (5.2 LTS, 6.1, 6.2 LTS, Django 2028) and choose one for a new project
Before you start
On this page
A first Django project often stalls on one of three mistakes: startproject refuses the name, an app missing from
INSTALLED_APPS stays invisible to makemigrations, or a URL answers 404 because nothing connects it. Each takes
one line to avoid, and this lesson shows all three, with the fix, on recorded runs of Django 6.1.2.
You will pin Django in a virtual environment, generate a project and a first app for Nivas, run the development
server, read the settings startproject wrote and connect the app’s URLs with include(). Then you will compare
what 6.1 and 5.2 LTS write and choose a release line for a new project.
A virtual environment pins one Django version per project
Two projects on one computer, one on 5.2 LTS and one on 6.1, cannot share one installed Django. A virtual environment: A folder, often named .venv, with its own Python packages, apart from every other project's. Activating it puts its python and pip first on PATH; it is never committed and can be rebuilt from requirements.txt or a lock file. gives each project its own folder of packages, next to its code. Python’s documentation describes one as never checked into source control and disposable: simple to delete and build again, because no code of yours lives in it. This session builds one for Nivas and installs an exact Django version into it:
"""A virtual environment with an exact Django version, the way the lesson's first section builds it."""
from session import run
run(
"""
mkdir nivas && cd nivas
python3 --version
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "Django==6.1.2"
python -m django --version
python -m pip freeze > requirements.txt
cat requirements.txt
"""
) Output
$ mkdir nivas && cd nivas $ python3 --version Python 3.14.8 $ python3 -m venv .venv $ source .venv/bin/activate $ python -m pip install "Django==6.1.2" Installing collected packages: sqlparse, asgiref, Django Successfully installed Django-6.1.2 asgiref-3.12.1 sqlparse-0.6.0 $ python -m django --version 6.1.2 $ python -m pip freeze > requirements.txt $ cat requirements.txt asgiref==3.12.1 Django==6.1.2 sqlparse==0.6.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 venv_session.py
Each line does one job. python3 -m venv .venv creates the environment in a folder named .venv, and
source .venv/bin/activate puts its python first on your PATH; on Windows the command is .venv\Scripts\activate.
python -m pip runs the pip of that same Python, so the package cannot land in another one. Django==6.1.2 asks for
one exact release, and pip added the two packages Django needs, asgiref and sqlparse (on Windows, tzdata as well).
pip freeze wrote all three to requirements.txt, from which python -m pip install -r requirements.txt builds the
same environment on any computer. pip also prints a “Collecting” and a “Downloading” line for each package, left out
above because they only show where pip found each file.
Pin the newest patch release of the line you choose, and move the pin when the next one comes out: patch releases are “100% compatible with the associated feature release”, in the words of Django’s release process, so the download page’s answer to whether you should upgrade to the latest patch is always yes. Django 6.1 runs on Python 3.12, 3.13 and 3.14; 5.2 LTS runs on 3.10 to 3.14.
The same environment with uv, a newer tool: uv init creates a project with a pyproject.toml, uv add "django==6.1.2"
adds the dependency and writes the exact versions to uv.lock, and uv run python manage.py runserver runs a command
inside the environment without activating it. In the 2026 Django Developers Survey, 63 % of respondents managed their
environments with venv and 43 % with uv; either works with everything below.
Running manage.py outside the environment
With the environment not active, python manage.py runserver runs another Python, which has no Django, and
manage.py stops with Couldn't import Django. Are you sure it's installed and available on your PYTHONPATH environment variable? Did you forget to activate a virtual environment?
Run source .venv/bin/activate in each new terminal first, or prefix the command with uv run.
Checkpoint
Check: A virtual environment with a pinned Django
startproject writes six files and refuses names Python already uses
django-admin startproject config . creates a project in the current folder. The first argument names the Python
package that holds the configuration, here config, a name that says what the folder is for. The . names the
destination, “the current working directory” in the words of the django-admin documentation, so manage.py sits at
the top of the repository, beside .venv and requirements.txt. The official tutorial’s form,
django-admin startproject mysite djangotutorial, makes a new folder instead. This session generates the project, lists
what it wrote, tries two names that startproject refuses, adds an app and runs the system checks:
"""startproject and startapp, two mistakes that stop a first project (a name startproject refuses, an app missing
from INSTALLED_APPS), and what the system checks say about the generated settings."""
from session import run
run(
"""
mkdir nivas && cd nivas
django-admin startproject config .
find . | sort
django-admin startproject site
django-admin startproject my-site
python manage.py startapp notices
python manage.py makemigrations notices
python manage.py check
python manage.py check --deploy
""",
fails=(
"django-admin startproject site",
"django-admin startproject my-site",
"python manage.py makemigrations notices",
"python manage.py check --deploy",
),
) Output
$ mkdir nivas && cd nivas $ django-admin startproject config . $ find . | sort . ./config ./config/__init__.py ./config/asgi.py ./config/settings.py ./config/urls.py ./config/wsgi.py ./manage.py $ django-admin startproject site CommandError: 'site' conflicts with the name of an existing Python module and cannot be used as a project name. Please try another name. [exit status 1] $ django-admin startproject my-site CommandError: 'my-site' is not a valid project name. Please make sure the name is a valid identifier. [exit status 1] $ python manage.py startapp notices $ python manage.py makemigrations notices No installed app with label 'notices'. [exit status 2] $ python manage.py check System check identified no issues (0 silenced). $ python manage.py check --deploy SystemCheckError: System check identified some issues: ERRORS: ?: (mail.E001) Your MAILERS setting uses a development-only email backend in the 'default' entry (django.core.mail.backends.console.EmailBackend). HINT: Use a production-ready email backend, such as the SMTP backend, otherwise email will not be sent. WARNINGS: ?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting. If your entire site is served only over SSL, you may want to consider setting a value and enabling HTTP Strict Transport Security. Be sure to read the documentation first; enabling HSTS carelessly can cause serious, irreversible problems. ?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. Unless your site should be available over both SSL and non-SSL connections, you may want to either set this setting True or configure a load balancer or reverse-proxy server to redirect all connections to HTTPS. ?: (security.W009) Your SECRET_KEY has less than 50 characters, less than 5 unique characters, or it's prefixed with 'django-insecure-' indicating that it was generated automatically by Django. Please generate a long and random value, otherwise many of Django's security-critical features will be vulnerable to attack. ?: (security.W012) SESSION_COOKIE_SECURE is not set to True. Using a secure-only session cookie makes it more difficult for network traffic sniffers to hijack user sessions. ?: (security.W016) You have 'django.middleware.csrf.CsrfViewMiddleware' in your MIDDLEWARE, but you have not set CSRF_COOKIE_SECURE to True. Using a secure-only CSRF cookie makes it more difficult for network traffic sniffers to steal the CSRF token. ?: (security.W018) You should not have DEBUG set to True in deployment. ?: (security.W020) ALLOWED_HOSTS must not be empty in deployment. System check identified 8 issues (0 silenced). [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 startproject_session.py
startproject wrote six files. manage.py runs every command of the project. config/__init__.py makes config a
Python package. config/settings.py holds the configuration, config/urls.py the root URLconf, and config/wsgi.py
and config/asgi.py the two entry points for servers from the previous lesson. Step through who reads each file:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- You run a command. You type python manage.py runserver. manage.py sets DJANGO_SETTINGS_MODULE to config.settings, unless your shell has already set it, and starts the command.
- Settings first. Every command and every server starts by importing the settings module. config/settings.py holds DEBUG, INSTALLED_APPS, MIDDLEWARE, DATABASES and the names of the other files.
- runserver loads the app. runserver reads WSGI_APPLICATION, config.wsgi.application, and serves that object: the development server is a WSGI server, as its start-up line says.
- A request arrives. For every request Django reads the URLconf that ROOT_URLCONF names, config.urls. In a new project it has one pattern, admin/.
- On a server. A production server does not go through manage.py. A WSGI server imports config.wsgi.application itself, and wsgi.py sets DJANGO_SETTINGS_MODULE the same way manage.py does.
- Or the ASGI door. An ASGI server imports config.asgi.application instead. Both doors lead to the same settings, URLconf and views.
So settings.py is read by everything, urls.py on every request, and manage.py only when you type a command; a
production server starts from wsgi.py or asgi.py. The two refusals in the session come from one check that startproject makes before it writes anything. A project name must be a
Python identifier, because Django imports the project as a package, so my-site fails: a hyphen cannot appear in an
identifier. And it must not be a module the Python running Django can already import, so site fails: Python’s own
site module has that name. The official tutorial warns about django and test for the same reason.
The rest of the session comes back in later sections: startapp notices made the app’s folder, makemigrations
did not know the app yet, and check --deploy listed what to change before production.
Single or double quotes in your files
The django-admin documentation says that startproject and startapp format the files they write with black when
black is on your PATH. The sessions on this page ran without black, so the generated files use single quotes; yours
may use double quotes. Python treats both the same.
Checkpoint
Check: What startproject writes and refuses
The generated settings are made for your computer
config/settings.py, the project’s settings module: The Python module, such as config.settings, that holds a project's configuration. DJANGO_SETTINGS_MODULE names it; manage.py and wsgi.py set that variable only when it is not set already., starts as development
settings: everything works on your own computer, and several values are wrong for a server. This is the file of the
Nivas project as startproject wrote it, with two changes: a placeholder in place of the random SECRET_KEY, and
one line for the app of a later section, the last entry of INSTALLED_APPS:
"""
Django settings for config project.
Generated by 'django-admin startproject' using Django 6.1.2.
For more information on this file, see
https://docs.djangoproject.com/en/6.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.1/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-this-key-is-for-this-lesson-only-make-your-own'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'notices.apps.NoticesConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.1/howto/static-files/
STATIC_URL = 'static/'
# Email
# https://docs.djangoproject.com/en/6.1/topics/email/#topic-email-configuration
MAILERS = {
'default': {
'BACKEND': 'django.core.mail.backends.console.EmailBackend',
},
} These are the values to know first:
| Setting | Generated value | What it means |
|---|---|---|
DEBUG |
True |
Error pages show code and settings; Django’s documentation says never to deploy with it on |
ALLOWED_HOSTS |
[] |
With DEBUG on, requests for localhost, 127.0.0.1 and [::1] only; with it off, every request gets 400 until you list your hosts |
SECRET_KEY |
'django-insecure-…' |
A random key for signing, new in every project; keep the real one out of the repository |
INSTALLED_APPS |
six django.contrib apps |
The admin, authentication, content types, sessions, messages and static files; your apps join them |
MIDDLEWARE |
seven entries | From SecurityMiddleware to XFrameOptionsMiddleware, in the order of the previous lesson |
DATABASES |
SQLite in db.sqlite3 |
One file next to manage.py, made the first time a command needs it |
MAILERS |
the console backend | New in 6.1: email is printed in the terminal instead of sent |
The last command of the session, python manage.py check --deploy, checks the settings the way a production server
needs them. On this new project it reported 8 issues: the console mailer as an error, mail.E001, and seven security
warnings, among them security.W018 for DEBUG and security.W020 for the empty ALLOWED_HOSTS. Leave them while
you learn; the deployment module of this track fixes each one.
Key idea
The generated settings are for your own computer: DEBUG, ALLOWED_HOSTS, SECRET_KEY and the mailer all change
before production, and check --deploy lists what is left.
Checkpoint
Check: The generated settings
runserver serves the project while you work
python manage.py runserver starts Django’s development server: The server that python manage.py runserver starts: a WSGI server on 127.0.0.1:8000 by default that reloads your code when files change. Django's documentation says it is not for production.. It serves the
project at 127.0.0.1:8000 by default, an address that other computers on your network cannot reach. The session below
gives it port 8741, so that it never meets another server, and sends two requests from a second terminal:
"""runserver on a new project: the development server's start-up lines and the first page it serves."""
from session import serve
serve(
setup="""
mkdir nivas && cd nivas
django-admin startproject config .
""",
server="python manage.py runserver 8741",
client="""
curl -s -o /dev/null -w "%{http_code}\\n" http://127.0.0.1:8741/
curl -s http://127.0.0.1:8741/ | grep -m 1 -o "<title>.*</title>"
""",
port=8741,
folder="nivas",
) Output
$ mkdir nivas && cd nivas
$ django-admin startproject config .
$ python manage.py runserver 8741
# The server keeps that terminal. In a second terminal:
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8741/
200
$ curl -s http://127.0.0.1:8741/ | grep -m 1 -o "<title>.*</title>"
<title>The install worked successfully! Congratulations!</title>
# Ctrl+C in the first terminal stops the server. That terminal showed:
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
…
Django version 6.1.2, using settings 'config.settings'
Starting WSGI development server at http://127.0.0.1:8741/
Quit the server with CONTROL-C.
WARNING: This is a development server. Do not use it in a production setting. Use a production WSGI or ASGI server instead.
For more information on production servers see: https://docs.djangoproject.com/en/6.1/howto/deployment/
[…] "GET / HTTP/1.1" 200 12149
[…] "GET / HTTP/1.1" 200 12149
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 runserver_session.py
Read the server’s lines from the top. “Watching for file changes” is the reloader: when you save a Python file, the
server restarts with your change. The system checks run before the server starts. “18 unapplied migration(s)” are the
tables of the admin, authentication, content types and sessions, which python manage.py migrate creates when you
first need them; the page works without them. “Starting WSGI development server” says what the previous lesson showed:
runserver is a WSGI server. The warning is Django’s own, and its documentation is blunt: the development server “has
not gone through security audits or performance tests”.
The page itself is Django’s welcome page, titled “The install worked successfully! Congratulations!”. Django shows it
only for /, while DEBUG is on and the URLconf holds nothing but the admin/ pattern that startproject wrote, so it
disappears as soon as you add a pattern of your own. Stop the server with Ctrl+C.
Checkpoint
Check: The development server
An app holds one feature, and include() connects its URLs
A project is the configuration of one site; an app: In Django, a Python package that does one job for a site, such as notices or bookings, with its own views, models and URLs. A project lists its apps in INSTALLED_APPS; startapp writes a new one. is a Python package that does one job
for it, such as notices or bookings, with its own views, models and URLs, and a project can hold many. The session’s
python manage.py startapp notices wrote the notices folder. Three changes make it serve a page:
- Add
'notices.apps.NoticesConfig'toINSTALLED_APPS, the form the official tutorial uses. Plain'notices'works too, because Django uses the one configuration class it finds innotices/apps.py. - Write a view in
notices/views.py. - List it in a new
notices/urls.py, and connect that file to the root URLconf withinclude('notices.urls').
The three files as the Nivas project has them:
notices/views.py · nivas/notices/views.py
from django.http import HttpResponse
def index(request):
return HttpResponse('Nivas notices: water tank cleaning on Sunday, 7 to 11 in the morning') notices/urls.py · nivas/notices/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
] config/urls.py · nivas/config/urls.py
"""
URL configuration for config project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('notices/', include('notices.urls')),
] include() cuts off the part of the path that matched and hands the rest to the app’s URLconf, so notices/urls.py
never needs to know where the project put it. The official tutorial’s rule is short: “You should always use include()
when you include other URL patterns.” Step through GET /notices/ with the include() line, and then without it:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Only the path counts. Django drops the leading slash and matches notices/ against the root URLconf, config/urls.py, the module ROOT_URLCONF names.
- The root URLconf. admin/ does not match. notices/ does, and its entry is include('notices.urls'): the root URLconf does not pick a view itself.
- include() cuts the path. include() cuts off the part that matched, notices/, and hands what is left, here the empty string, to notices/urls.py.
- The view runs. The app's pattern '' matches the empty rest, so Django calls notices.views.index, and the recorded check got 200 and the notice text.
- Without include(). Now the same request against config/urls.py as startproject wrote it, with admin/ only. notices/ matches nothing, and nothing ever reaches notices/urls.py.
- A 404 that names the patterns. With DEBUG = True the 404 page lists the patterns Django tried, here admin/ alone, and says that the path notices/ matched none of them.
This program checks the app the way a browser would, through Django’s test client, and then sends the same request
to config/urls.py as startproject wrote it, before the include() line:
"""Check the notices app from outside, as a browser would see it, without starting a server.
Run it next to manage.py. It also shows what the same request gets from config/urls.py as startproject wrote it,
before the include() line was added.
"""
import html
import logging
import os
import re
import types
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
import django # noqa: E402
django.setup()
from django.apps import apps # noqa: E402
from django.contrib import admin # noqa: E402
from django.test import Client, override_settings # noqa: E402
from django.test.utils import setup_test_environment # noqa: E402
from django.urls import path # noqa: E402
setup_test_environment() # what manage.py test does first: the test client's host, testserver, is allowed
logging.getLogger('django.request').setLevel(logging.ERROR) # the 404 below is expected; no warning for it
client = Client()
print('notices installed:', apps.is_installed('notices'))
response = client.get('/notices/')
print('GET /notices/ ->', response.status_code, response.content.decode())
as_generated = types.ModuleType('config.urls')
as_generated.urlpatterns = [path('admin/', admin.site.urls)]
with override_settings(ROOT_URLCONF=as_generated):
response = client.get('/notices/')
print('Without the include() line: GET /notices/ ->', response.status_code, 'and the debug page says:')
page = html.unescape(re.sub(r'<[^>]+>', ' ', response.content.decode()))
words = re.sub(r' ([,.])', r'\1', ' '.join(page.split()))
tried, current = re.search(r'(Using the URLconf.*?) (The current path.*?any of these\.)', words).groups()
print(' ', tried)
print(' ', current) Output
notices installed: True GET /notices/ -> 200 Nivas notices: water tank cleaning on Sunday, 7 to 11 in the morning Without the include() line: GET /notices/ -> 404 and the debug page says: Using the URLconf defined in config.urls, Django tried these URL patterns, in this order: admin/ [namespace='admin'] The current path, notices/, didn’t match any of these.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_notices.py
Those are two of the three mistakes from the start of this lesson. Without include(), /notices/ is a 404 whose
debug page lists the one pattern Django tried, admin/. Without the INSTALLED_APPS line, the view still works, but
every part of Django that finds apps through that list misses this one: the session’s makemigrations notices
answered “No installed app with label ‘notices’”, and the template loader and the admin would skip the app’s
templates folder and its admin.py in the same way.
Checkpoint
Check: An app and include()
Choose a release line: 6.1 now, 6.2 LTS next
Django ships a feature release about every eight months until 6.2, and certain releases are LTS release: A long-term support release of Django, such as 5.2 or 6.2, which gets security and data-loss fixes for about three years. From Django 2028 every feature release gets three years of support. releases, with security and data-loss fixes for about three years. From Django 2028, one release a year replaces both kinds. Walk through the lines a new project can start on:
- 5.2 LTS
Long-term support, now with security fixes only LTS
The current long-term support line, released in 2025. Its mainstream support is over, so it gets only security and data-loss fixes, into 2028. It runs on Python 3.10 to 3.14, and its startproject still writes DEFAULT_AUTO_FIELD.
- 6.0
A built-in Content Security Policy, partials and tasks
Needs Python 3.12 or newer. It added a built-in Content Security Policy, template partials and a framework for background tasks, and made BigAutoField the default primary key. Its security fixes end in 2027.
- 6.1
The latest feature release, the one this track uses this track
The latest release is 6.1.2. It added fetch modes for model fields, database-level on_delete options and the MAILERS setting that startproject now writes. Python 3.12 to 3.14; security fixes until the end of 2027.
- 6.2 LTS
The next long-term support line LTS
Planned for 2027 as the last release under the old policy, with security fixes until 2030. A project on 6.1 reaches it in one upgrade.
- Django 2028
One feature release a year, named after the year new policy
Under DEP 20, the release that would have been 7.0 is called Django 2028, and from then on one feature release comes out every year. Each gets three years of support, one of mainstream support and two of security fixes, so the separate LTS label is retired.
The timeline has 5 events, from 5.2 LTS to Django 2028.
What does the choice change on the first day? This program creates a project with each release, in a fresh environment
of its own, and compares what they write, setting aside each release’s documentation links and the random
SECRET_KEY:
"""Run `django-admin startproject config .` with Django 6.1.2 and with 5.2.18 LTS, each in a fresh virtual
environment, and print what differs in the projects they write.
Each release's comments link to its own documentation (/en/6.1/, /en/5.2/) and every project gets a new random
SECRET_KEY, so those are set aside before comparing. pip installs from the wheelhouse folder of the Python running this
file when there is one (as where this lesson's runs are recorded), and from PyPI otherwise.
"""
import difflib
import os
import re
import subprocess
import sys
import venv
from pathlib import Path
RELEASES = ["6.1.2", "5.2.18"]
WHEELHOUSE = Path(sys.prefix) / "wheelhouse"
def new_project(release):
"""A fresh virtual environment with this Django, and the project its startproject writes."""
env_dir = Path(f"env-{release}").absolute()
venv.create(env_dir, with_pip=False)
source = ["--no-index", "--find-links", str(WHEELHOUSE)] if WHEELHOUSE.is_dir() else []
subprocess.run([sys.executable, "-m", "pip", "--python", str(env_dir / "bin" / "python"), "install",
"--quiet", "--disable-pip-version-check", *source, f"Django=={release}"], check=True)
project = Path(f"nivas-{release}")
project.mkdir()
subprocess.run([str(env_dir / "bin" / "django-admin"), "startproject", "config", "."], cwd=project, check=True)
return project
def comparable(text):
text = re.sub(r"docs\.djangoproject\.com/en/\d+\.\d+/", "docs.djangoproject.com/en/<release>/", text)
text = re.sub(r"using Django [\d.]+\.", "using Django <release>.", text)
return re.sub(r"SECRET_KEY = (['\"]).*\1", "SECRET_KEY = '<random>'", text)
def files(project):
return sorted(str(p.relative_to(project)) for p in project.rglob("*.py"))
new, lts = (new_project(release) for release in RELEASES)
print(f"Both write {len(files(new))} files: {', '.join(files(new))}")
assert files(new) == files(lts)
same = [name for name in files(new) if comparable((new / name).read_text()) == comparable((lts / name).read_text())]
print(f"The same once links and keys are set aside: {', '.join(same)}")
for name in files(new):
if name in same:
continue
print(f"\n{name}, Django {RELEASES[0]} (-) against {RELEASES[1]} (+):")
a = comparable((new / name).read_text()).splitlines()
b = comparable((lts / name).read_text()).splitlines()
for line in difflib.unified_diff(a, b, lineterm="", n=1):
if not line.startswith(("---", "+++")):
print(line) Output
Both write 6 files: config/__init__.py, config/asgi.py, config/settings.py, config/urls.py, config/wsgi.py, manage.py
The same once links and keys are set aside: config/__init__.py, config/asgi.py, config/urls.py, config/wsgi.py, manage.py
config/settings.py, Django 6.1.2 (-) against 5.2.18 (+):
@@ -118,10 +118,5 @@
+# Default primary key field type
+# https://docs.djangoproject.com/en/<release>/ref/settings/#default-auto-field
-# Email
-# https://docs.djangoproject.com/en/<release>/topics/email/#topic-email-configuration
-
-MAILERS = {
- 'default': {
- 'BACKEND': 'django.core.mail.backends.console.EmailBackend',
- },
-}
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compare_releases.py
Five of the six files are the same. In settings.py, 5.2.18 still sets DEFAULT_AUTO_FIELD to BigAutoField, the
value that has been the default since 6.0, and 6.1.2 writes the MAILERS setting that 6.1 introduced.
For a new project, start on the latest feature release, 6.1.2, and plan one upgrade, to 6.2 LTS when it comes out in 2027; from there the project has security fixes until 2030. 5.2 LTS is already past its mainstream support, so a new project on it gets security fixes only, and has to move by 2028 anyway. Choose 5.2 only when a package you cannot do without does not support 6.x yet. Teams are split between the two: in the 2026 Django Developers Survey, 43 % of respondents used 6.0 and 44 % a 5.x release.
Checkpoint
Check: Choosing a release line
Practise: check a project name the way startproject does
Write the check startproject makes before it writes a file: given a name and the modules Python can already import,
return the message it prints, or None for a name it accepts. The sample tests run in your browser and use the
messages of the recorded session:
Exercise · Easy · Python
Predict which project names startproject refuses
django-admin startproject <name> checks the name before it writes a single file. Write startproject_error(name, modules), which returns the message it prints for a name it refuses, or None for a name it accepts. modules is the set of module names the Python that runs Django can already import.
It applies two rules, in this order:
- The name must be a valid Python identifier (
str.isidentifier()), because Django imports the project as a package. Otherwise the message is'<name>' is not a valid project name. Please make sure the name is a valid identifier. - The name must not be one of
modules. Otherwise the message is'<name>' conflicts with the name of an existing Python module and cannot be used as a project name. Please try another name.
For example, with modules = {"site", "json", "django"}, startproject_error("config", modules) returns None, startproject_error("my-site", modules) returns the first message and startproject_error("site", modules) the second. These are the messages Django 6.1.2 printed in this lesson's recorded session.
Starter code · project_name.py
def startproject_error(name, modules):
"""Return the message django-admin startproject prints for `name`, or None when it accepts the name.
modules: the names of the modules Python can already import.
"""
return None The sample tests · test_project_name.py
from project_name import startproject_error
MODULES = {"site", "json", "django", "test"}
NOT_IDENTIFIER = "is not a valid project name. Please make sure the name is a valid identifier."
CONFLICT = "conflicts with the name of an existing Python module and cannot be used as a project name. Please try another name."
def test_accepts_a_good_name():
"""accepts names that are identifiers and no module uses"""
assert startproject_error("config", MODULES) is None
assert startproject_error("nivas", MODULES) is None
def test_refuses_a_name_python_already_uses():
"""refuses site, which Python's own site module already uses"""
assert startproject_error("site", MODULES) == f"'site' {CONFLICT}"
def test_refuses_django_and_test():
"""refuses django and test, the two names the official tutorial warns about"""
assert startproject_error("django", MODULES) == f"'django' {CONFLICT}"
assert startproject_error("test", MODULES) == f"'test' {CONFLICT}"
def test_refuses_a_hyphen():
"""refuses my-site: a hyphen cannot appear in an identifier"""
assert startproject_error("my-site", MODULES) == f"'my-site' {NOT_IDENTIFIER}"
def test_refuses_a_leading_digit():
"""refuses 2fast: an identifier cannot start with a digit, and this rule is checked first"""
assert startproject_error("2fast", MODULES | {"2fast"}) == f"'2fast' {NOT_IDENTIFIER}" A hint
Check the identifier rule first, with name.isidentifier(), and return its message as soon as it fails. Only a name that passes reaches the second rule, name in modules. Build each message with an f-string around the exact text the prompt gives, quotes included: f"'{name}' is not a valid project name. …".
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then check all six steps, from the environment to the release line, in a short adaptive quiz: it opens with the step you have answered least well so far, and every answer comes with its reason.
Practice that adapts to you
Practise this lesson
15 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What is the difference between a Django project and a Django app?
A project is the configuration of one website: its settings, its root URLconf and the WSGI and ASGI entry points, which startproject writes. An app is a Python package that does one job, such as notices or bookings, with its own views, models and URLs; startapp writes one. The project lists the apps it uses in INSTALLED_APPS and connects their URLs with include(). One project usually holds several apps, and a well-made app can be used in more than one project.
Two design questions of this module start from this lesson’s setup: settings that work everywhere without edits, and the release line for a new service in a company that runs many.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
The settings.py of your project must work on a laptop, in CI and in production without anyone editing it between them. How do you structure the settings, and which values must differ between the three?
-
Mid to staff
A company runs twelve Django services on 5.2 LTS, and a thirteenth starts now. Would you start it on 5.2 LTS or on 6.1? Defend the choice with the support windows and the upgrades each choice leads to.
Key takeaways
- A virtual environment gives each project its own packages; activate it, install with
python -m pipand pin an exact release such asDjango==6.1.2, or let uv record the versions inuv.lock. django-admin startproject config .writesmanage.pyand five files inconfig/, and refuses a name that is not an identifier or that Python can already import, such assite.- The generated settings are development settings:
DEBUG,ALLOWED_HOSTS,SECRET_KEYand the mailer change before production, andcheck --deploylists what is left. runserveris a WSGI development server on127.0.0.1:8000that reloads your code; it is never for production.- An app needs its entry in
INSTALLED_APPSand its URLconf connected withinclude(), or its URLs answer 404. - Start a new project on 6.1.2 and plan the upgrade to 6.2 LTS; 5.2 LTS gets security fixes only, into 2028.
Terms in this lesson
- app
- In Django, a Python package that does one job for a site, such as notices or bookings, with its own views, models and URLs. A project lists its apps in INSTALLED_APPS; startapp writes a new one.
- development server
- The server that python manage.py runserver starts: a WSGI server on 127.0.0.1:8000 by default that reloads your code when files change. Django's documentation says it is not for production.
- LTS release
- A long-term support release of Django, such as 5.2 or 6.2, which gets security and data-loss fixes for about three years. From Django 2028 every feature release gets three years of support.
- settings module
- The Python module, such as config.settings, that holds a project's configuration. DJANGO_SETTINGS_MODULE names it; manage.py and wsgi.py set that variable only when it is not set already.
- virtual environment
- A folder, often named .venv, with its own Python packages, apart from every other project's. Activating it puts its python and pip first on PATH; it is never committed and can be rebuilt from requirements.txt or a lock file.
References
- How to install Django (Django Software Foundation)
- Writing your first Django app, part 1 (Django Software Foundation)
- django-admin and manage.py (Django Software Foundation)
- Django documentation: Settings (Django Software Foundation)
- Django documentation: Applications (Django Software Foundation)
- How to deploy with WSGI (Django Software Foundation)
- Django FAQ: Installation (Django Software Foundation)
- Download Django: supported versions (Django Software Foundation)
- Django's release process (Django Software Foundation)
- Django 6.0 release notes (Django Software Foundation)
- Django 6.1 release notes (Django Software Foundation)
- DEP 0020: Annual Release Cycle (Django Software Foundation (GitHub))
- venv: creation of virtual environments (Python Software Foundation)
- Working on projects (uv documentation) (Astral)
- Django Developers Survey 2026 results (Django Software Foundation and JetBrains)
Report a problem with this lesson
Kept only in this browser. Your Learn progress