Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8, Django 6.1.2 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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 Django 6.1.2 Python · sessions/venv_session.py
"""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

  1. Question 1 of 2 Which command installs exactly Django 6.1.2 into the active virtual environment?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: python -m pip install "Django==6.1.2"

    Pin the exact release with == and install it with the environment's own pip. pip freeze then records it, with asgiref and sqlparse, in requirements.txt.

  2. Question 2 of 2 Why does the lesson run python -m pip instead of plain pip?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It runs the pip of the python that comes first on PATH, so the package lands where that Python will look for it

    A computer often has several Pythons. python -m pip is the pip of the Python you name, so with the environment active, Django goes into .venv, where manage.py will find it.

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, startapp and the system checks, recorded Python · sessions/startproject_session.py
"""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:

Interactive · step by step Who reads each file that startproject writes Step through runserver on your computer, then a production server, and watch which file each one reads.
Who reads each file that startproject writes, step 6 of 6. Showing manage.py, config/settings.py, config/wsgi.py, config/urls.py, config/asgi.py, A production server.DJANGO_SETTINGS_MODULErunserver loads iteach requestimports applicationor imports this onemanage.pyruns commandsconfig/settings.pythe settings moduleconfig/wsgi.pyWSGI_APPLICATIONconfig/urls.pyROOT_URLCONFconfig/asgi.pyfor ASGI serversA production serverGunicorn or Uvicornconfig.asgi:application

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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/.
  5. 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.
  6. 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

  1. Question 1 of 2 What does the . do in django-admin startproject config . ?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It makes the current folder the project folder, so manage.py is written right there

    The recorded session listed manage.py and the config folder directly in the nivas folder. Without the ., the command would have made a new folder named config with another config package inside it.

  2. Question 2 of 2 Which of these project names does startproject refuse? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • site
    • test
    • my-site

    startproject applies two rules: the name must be a Python identifier, and no module the running Python can import may have the same name.

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:

config/settings.py of the Nivas project Python · nivas/config/settings.py
"""
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

  1. Question 1 of 2 You set DEBUG = False on your computer to see the production error pages, and every page now answers 400 Bad Request. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: ALLOWED_HOSTS is still empty, and only DEBUG = True lets localhost, 127.0.0.1 and [::1] through

    With DEBUG = True and an empty ALLOWED_HOSTS, Django accepts the local addresses only. With DEBUG = False it accepts only the hosts you list, so an empty list refuses every request with a 400.

  2. Question 2 of 2 What does python manage.py check --deploy do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It checks the settings for what is wrong or unsafe in production, such as DEBUG and an empty ALLOWED_HOSTS

    check runs the system checks; --deploy adds the checks for production settings. On a new 6.1.2 project that was mail.E001 for the console mailer and seven security warnings, W018 for DEBUG and W020 for ALLOWED_HOSTS among them.

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, and the first page it serves Python · sessions/runserver_session.py
"""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

  1. Question 1 of 2 What is python manage.py runserver for?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Serving the project on your own computer while you work on it

    runserver is a lightweight WSGI server for your own computer, on 127.0.0.1:8000 by default. Its own start-up warning says not to use it in production.

  2. Question 2 of 2 runserver reported 18 unapplied migrations, and the welcome page still answered 200. What were the 18 migrations?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The tables of the admin, authentication, content types and sessions, which migrate creates when they are needed

    A new project's contrib apps come with migrations for their tables. python manage.py migrate applies them; until a page needs those tables, the project works without them.

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:

  1. Add 'notices.apps.NoticesConfig' to INSTALLED_APPS, the form the official tutorial uses. Plain 'notices' works too, because Django uses the one configuration class it finds in notices/apps.py.
  2. Write a view in notices/views.py.
  3. List it in a new notices/urls.py, and connect that file to the root URLconf with include('notices.urls').

The three files as the Nivas project has them:

The notices app: its view, its URLconf and the include() in the root URLconf

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:

Interactive · stage by stage How include() hands the rest of the path to the app Step through GET /notices/ with the include() line, then without it, as startproject wrote config/urls.py.
How include() hands the rest of the path to the app, step 6 of 6. Showing GET /notices/, config/urls.py (admin/ only), notices/urls.py (never reached), notices.views.index (never called).no match: 404GET /notices/the path: notices/config/urls.pyadmin/ onlynotices/urls.pynever reachednotices.views.indexnever called404tried admin/ only

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Only the path counts. Django drops the leading slash and matches notices/ against the root URLconf, config/urls.py, the module ROOT_URLCONF names.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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, with and without include() Python · nivas/check_notices.py
"""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()

  1. Question 1 of 2 config/urls.py has path('notices/', include('notices.urls')), and notices/urls.py has path('<int:pk>/', views.detail). For GET /notices/7/, what does notices/urls.py match against?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 7/

    An app's URLconf only ever sees the rest of the path, which is why notices/urls.py does not need to know where the project mounted it.

  2. Question 2 of 2 The notices view already answers in the browser, but python manage.py makemigrations notices says No installed app with label 'notices'. What is missing?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The app's entry in INSTALLED_APPS

    Add 'notices.apps.NoticesConfig', or simply 'notices', to INSTALLED_APPS. Then migrations, templates, the admin and the other parts that look for apps through that list find it.

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:

Timeline · 5 events in order The release lines a new Django project can start on Walk through the lines in order. Each says what it brought and how long it gets fixes.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

startproject with Django 6.1.2 and with 5.2.18 LTS, compared Python · releases/compare_releases.py
"""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

  1. Question 1 of 2 A new project starts now and no package it needs holds it back. Which release line does the lesson recommend?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 6.1.2, with one upgrade planned to 6.2 LTS in 2027

    Start on the latest feature release and plan the step to the next LTS. Choose 5.2 LTS only when a package you cannot replace does not support 6.x yet.

  2. Question 2 of 2 Once links and keys are set aside, what differs between the settings.py files that 6.1.2 and 5.2.18 write?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 5.2.18 writes DEFAULT_AUTO_FIELD and 6.1.2 writes MAILERS

    BigAutoField has been the default primary key since 6.0, so 6.x no longer writes DEFAULT_AUTO_FIELD, and 6.1 added MAILERS, which startproject now writes with the console backend.

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:

  1. 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.
  2. 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. …".

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.

  1. Question 1 of 15 What does a virtual environment hold?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: A Python and its own installed packages, apart from every other project's

    An environment holds packages only, so it can be deleted and built again from requirements.txt or a lock file at any time.

  2. Question 2 of 15 A teammate clones the Nivas repository, which has a requirements.txt with Django==6.1.2. Which commands give them the same environment?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: python3 -m venv .venv, then source .venv/bin/activate, then python -m pip install -r requirements.txt

    requirements.txt records exact versions, so anyone can rebuild the same environment from it. That is why pip freeze writes it.

  3. Question 3 of 15 Should the .venv folder be committed to the project's Git repository?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: No; commit requirements.txt or uv.lock, which rebuild it anywhere

    Commit what describes the environment, the pinned versions, and let each computer build its own .venv from it.

  4. Question 4 of 15 In a project created with django-admin startproject config ., which file holds the root URLconf?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: config/urls.py

    startproject writes config/urls.py with one pattern, admin/, and settings.py points ROOT_URLCONF at it.

  5. Question 5 of 15 Why does startproject refuse the name site, while it accepts nivas?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: Python already has a module named site, and Django would import the project under that name

    A project is a Python package, imported by name. A name another module already has would clash with it, so startproject refuses it, as it refuses a name that is not an identifier, such as my-site.

  6. Question 6 of 15 What does DEBUG = True change when a view raises an error?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: Django shows a detailed error page with the code and the settings

    With DEBUG on, an error page shows everything that helps on your own computer, including code and settings, which is exactly what a public site must never show.

  7. Question 7 of 15 Where should the SECRET_KEY of the production site come from?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: From the environment or a secret store, set on the server, never from the repository

    The generated key is for your computer. In production, read the key from the environment, keep it out of Git, and let check --deploy confirm it is long and random.

  8. Question 8 of 15 Which address does python manage.py runserver use when you give it no arguments?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: 127.0.0.1, port 8000

    runserver listens on 127.0.0.1:8000 unless you give it an address or a port, as the session did with 8741.

  9. Question 9 of 15 Through which interface does runserver serve the project, and which file does it load?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: WSGI, through config/wsgi.py, the application that WSGI_APPLICATION names

    runserver reads WSGI_APPLICATION, config.wsgi.application, and serves it as a WSGI server, the same door that Gunicorn uses in production.

  10. Question 10 of 15 Which of these is an app rather than a project?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: notices, with its views, URLs and models

    The project configures one site; an app is one feature of it. INSTALLED_APPS lists the apps a project uses.

  11. Question 11 of 15 GET /notices/ answers 404, and the debug page says Django tried only admin/. What is missing?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: path('notices/', include('notices.urls')) in config/urls.py

    The debug 404 page names the URLconf and the patterns it tried. If the app's prefix is not among them, its include() line is missing from the root URLconf.

  12. Question 12 of 15 Nivas moves its notices from /notices/ to /board/. With the URLconfs of this lesson, what has to change?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: Only the root URLconf's line, which becomes path('board/', include('notices.urls'))

    Because include() hands on only the rest of the path, the prefix lives in one place, the root URLconf, and an app can be mounted anywhere, even in another project.

  13. Question 13 of 15 What does a Django LTS release get that the feature releases between LTS releases do not?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: Security and data-loss fixes for about three years

    An LTS line, such as 5.2 or 6.2, gets security and data-loss fixes for about three years. From Django 2028 every feature release gets three years of support.

  14. Question 14 of 15 What changes with Django 2028, under DEP 20?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: One feature release a year, named after the year, each with three years of support

    From Django 2028 a feature release comes out every year and is supported for three years, one of mainstream support and two of security fixes. 6.2 LTS keeps its own window, until 2030.

  15. Question 15 of 15 A new project needs the database-level on_delete options. Which release lines can it use?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: 6.1 and later, because 6.1 added them

    Check the release notes of the line before choosing it: a feature a project needs rules out every older line, and here that rules out 5.2 LTS.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A virtual environment gives each project its own packages; activate it, install with python -m pip and pin an exact release such as Django==6.1.2, or let uv record the versions in uv.lock.
  • django-admin startproject config . writes manage.py and five files in config/, and refuses a name that is not an identifier or that Python can already import, such as site.
  • The generated settings are development settings: DEBUG, ALLOWED_HOSTS, SECRET_KEY and the mailer change before production, and check --deploy lists what is left.
  • runserver is a WSGI development server on 127.0.0.1:8000 that reloads your code; it is never for production.
  • An app needs its entry in INSTALLED_APPS and its URLconf connected with include(), 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.