ZetCode

Django introduction

last modified November 13, 2026 Python 3.15

In this article we show how to start with Django: install the framework, create a project and an application, write a view, map it to a URL, render a template, and run the development server. The examples use Django 6.1 and Python 3.15.

Django

Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. It is often described as batteries included: the framework ships with an ORM, a template engine, a form library, an authentication system, an automatic administration site, a caching framework, and a test runner.

Django follows the MTV (model, template, view) pattern. A model describes the data and maps it to a database table, a template describes how the data is presented, and a view is a Python function or class that receives an HTTP request and returns an HTTP response. A URL configuration connects URLs to views.

A Django project is a whole web application with its configuration. A project consists of one or more apps, which are Python packages that each do one thing, such as a blog or a polling feature. An app can be reused in several projects.

Django installation

We use uv, a fast Python package and project manager, to create the virtual environment and install Django. uv is installed separately; see its documentation for the installation options. The package is published on PyPI as Django. Django 6.1 supports Python 3.12 to 3.15; the examples were tested with Python 3.15.

We create a project directory and initialize it with uv init --bare, which creates only a pyproject.toml file, without any sample code. The uv add command creates the .venv virtual environment, installs Django into it, records the dependency in pyproject.toml, and writes the exact versions to the uv.lock file.

$ mkdir djangointro
$ cd djangointro
$ uv init --bare
$ uv add django

There is no need to activate the environment. The uv run command runs a command inside the project's environment, creating or updating the environment first if necessary. We use it for all commands in this article.

Verifying the installation

The django module can be run as a program; the --version option prints the installed version.

$ uv run python -m django --version
6.1.3

Creating a project

The django-admin command-line tool is installed with Django into the project's environment. Its startproject subcommand creates the skeleton of a new project. We name the project config: Django uses the name for the configuration package, and the same name in every project means that the settings module is always config.settings. The dot at the end tells Django to create the project in the current directory; without it, Django would create an extra directory level.

$ uv run django-admin startproject config .

Project structure

The command created the manage.py script and the config package. Next to them are the pyproject.toml and uv.lock files created by uv; the hidden .venv directory is not shown.

.
├── config
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── pyproject.toml
└── uv.lock

Project files

The files have the following roles.

manage.py

A command-line utility for the project. It is a thin wrapper around django-admin that sets the DJANGO_SETTINGS_MODULE environment variable, so the commands know which project they work with. We use it to run the server, create apps, apply migrations, and run tests.

config/settings.py

The project settings: installed apps, middleware, the database, templates, time zone, and static files.

config/urls.py

The root URL configuration. It maps URL paths to views.

config/wsgi.py
config/asgi.py

Entry points for WSGI-compatible and ASGI-compatible web servers. The development server uses the WSGI one; production servers such as Gunicorn (WSGI) or Uvicorn (ASGI) import application from these modules.

The development server

Django includes a lightweight web server for development. We start it with the runserver command. The server reloads itself when the source code changes.

In the output, the server listens on port 8000 of the loopback interface. The warning about unapplied migrations refers to the database tables needed by the built-in apps (authentication, sessions, and the admin site). We fix this in the next section. When we open http://127.0.0.1:8000/ in a browser, Django shows its welcome page. The development server is not meant for production use. The address and port can be changed, for example with uv run python manage.py runserver 0.0.0.0:9000.

$ uv run python manage.py runserver
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.
November 13, 2026 - 16:37:20
Django version 6.1.3, using settings 'config.settings'
Starting WSGI development server at http://127.0.0.1:8000/
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/

Applying migrations

A migration is a Python file that describes a change to the database schema. The migrate command applies all migrations that have not been applied yet. By default, Django uses an SQLite database stored in the db.sqlite3 file, so no database server is needed.

$ uv run python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

Creating an app

The startapp subcommand creates a new app. We create an app called hello.

$ uv run python manage.py startapp hello

App structure

Django creates the hello package. The models.py file will hold the data models, views.py the views, admin.py the admin site registrations, and tests.py the tests. The apps.py file contains the app configuration class, HelloConfig. The migrations directory stores the migrations of the app's models.

hello
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

Registering the app

Django knows only the apps listed in the INSTALLED_APPS setting. We add the new app to the list.

config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'hello',
]

The entry 'hello' is the name of the app package. It is a shortcut for 'hello.apps.HelloConfig'; Django finds the configuration class on its own. Registering the app lets Django discover its models, migrations, and templates.

The first view

A view takes an HttpRequest and returns an HttpResponse. We write two views in hello/views.py.

hello/views.py
from django.http import HttpResponse
from django.shortcuts import render


def index(request):
    return HttpResponse('Hello from Django!')


def greet(request, name):
    return render(request, 'hello/greet.html', {'name': name})

Each view is an ordinary Python function.

def index(request):
    return HttpResponse('Hello from Django!')

The index() view returns a plain text response. The response object has status code 200 and a text/html content type by default. See Django HttpRequest and Django HttpResponse for details on the two objects.

def greet(request, name):
    return render(request, 'hello/greet.html', {'name': name})

The greet() view receives the name argument captured from the URL. The render() shortcut loads a template, renders it with the given context dictionary, and returns the result as an HttpResponse.

Templates

A template is a text file with placeholders for dynamic data. Django looks for templates in the templates directory of each installed app, because the default settings enable APP_DIRS. We put the template into hello/templates/hello/. The extra hello subdirectory is a namespace: template names are shared by all apps, so two apps with a greet.html file would otherwise clash.

hello/templates/hello/greet.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Greeting</title>
</head>
<body>
    <h1>Hello, {{ name }}!</h1>
</body>
</html>

The {{ name }} expression prints the value of the name variable from the context. Django escapes HTML special characters in variables automatically.

URL configuration

Views are reachable only after they are mapped to URLs. By convention, each app has its own urls.py file.

hello/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('greet/<str:name>/', views.greet, name='greet'),
]

The urlpatterns list contains path() entries. Each entry has a route, a view, and an optional name.

path('greet/<str:name>/', views.greet, name='greet'),

The <str:name> part is a path converter. It matches a non-empty string without a slash and passes it to the view as the name argument. The name parameter of path() gives the route a name, so code and templates can build the URL with reverse() or {% url %} instead of hard-coding it.

Next, we include the app's URL configuration in the project's root configuration.

config/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('hello.urls')),
]

The include() function delegates all URLs with the given prefix, here the empty one, to the app's URL configuration. The generated file also contains a long docstring, which we omit. The admin/ route serves the built-in administration site.

Running the application

We start the server in the background and request the home page with the curl tool. The -i option includes the response headers. The response is the plain text returned by the index() view; the headers added by the security middleware (X-Frame-Options, X-Content-Type-Options, and others) are part of the default configuration.

$ uv run python manage.py runserver &
$ curl -i http://127.0.0.1:8000/
HTTP/1.1 200 OK
Date: Fri, 13 Nov 2026 16:39:35 GMT
Server: WSGIServer CPython/3.15.2
Content-Type: text/html; charset=utf-8
X-Frame-Options: DENY
Content-Length: 18
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

Hello from Django!

Rendering a template

The /greet/Peter/ URL matches the second route. The name converter captures Peter, the view passes it to the template, and the template engine inserts it into the HTML.

$ curl http://127.0.0.1:8000/greet/Peter/
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Greeting</title>
</head>
<body>
    <h1>Hello, Peter!</h1>
</body>
</html>

Automatic escaping

In this request the name is the string <b>, URL-encoded as %3Cb%3E. The template engine escapes it, so the browser shows the characters instead of interpreting them as a tag. This protects the page from cross-site scripting (XSS) through values that come from the user.

$ curl http://127.0.0.1:8000/greet/%3Cb%3E/
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Greeting</title>
</head>
<body>
    <h1>Hello, &lt;b&gt;!</h1>
</body>
</html>

A URL that does not exist

A URL that matches no route gives status 404. The -s option hides the progress meter, -o /dev/null discards the body, and -w prints the status code. In debug mode the body is a detailed error page listing the URL patterns that were tried.

$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/nothing/
404

The request log

The development server prints one line for every request: the time, the request line, the status code, and the size of the response body. Requests that end with a 404 status also log the missing path.

[13/Nov/2026 16:39:35] "GET / HTTP/1.1" 200 18
[13/Nov/2026 16:39:35] "GET /greet/Peter/ HTTP/1.1" 200 153
[13/Nov/2026 16:39:35] "GET /greet/%3Cb%3E/ HTTP/1.1" 200 157
Not Found: /nothing/
[13/Nov/2026 16:39:35] "GET /nothing/ HTTP/1.1" 404 2788

Testing

Every app has a tests.py file. Django tests are built on the unittest module of the standard library; the TestCase class adds a test client and a separate test database, so tests never touch our data.

hello/tests.py
from django.test import TestCase
from django.urls import reverse


class HelloTests(TestCase):

    def test_index(self):
        response = self.client.get(reverse('index'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Hello from Django!')

    def test_greet_escapes_name(self):
        response = self.client.get(reverse('greet', args=['<b>']))
        self.assertContains(response, 'Hello, &lt;b&gt;!')

The test client self.client issues requests directly to the application without a running server. The reverse() function builds the URL from the route name that we gave in urls.py; the assertContains() method checks the status code and that the response body contains the text. The test command discovers and runs the tests.

$ uv run python manage.py test
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.007s

OK
Destroying test database for alias 'default'...

Project settings

The config/settings.py module is a plain Python module whose upper-case names are the settings. The most important ones are the following.

The diffsettings command prints the settings that differ from the Django defaults, which is handy for reviewing a project.

Common manage.py commands

Running uv run python manage.py help --commands lists the available commands. We list the ones we use most often.

In this article we created a Django project and an app, wrote a function-based view and a template, mapped them to URLs, ran the development server, and wrote tests. The other Django tutorials on this site show how to work with requests, responses, files, JSON, and email.

Source

Django documentation: Writing your first Django app

uv documentation

In this article we have installed Django with uv, created a project and an app, written a view, mapped it to a URL, rendered a template, and tested the result. The request and response objects are described in Django HttpRequest and Django HttpResponse.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Django tutorials.