Introduction to Django

Django is a "batteries-included" web framework for Python. Out of the box it gives you URL routing, an ORM for working with databases, templates, forms, authentication and an auto-generated admin site. It follows a Model–View–Template pattern: models describe your data, views handle requests, and templates render HTML.

1. Installation

Use a virtual environment so each project has its own packages:

mkdir my-site
cd my-site
python -m venv .venv

# macOS / Linux
source .venv/bin/activate
# Windows
.venv\Scripts\activate

pip install django
python -m django --version

2. Projects and apps

A project holds settings and top-level URLs. An app is a self-contained feature (a blog, a shop, user accounts). One project usually contains several apps.

django-admin startproject mysite .
python manage.py startapp blog

The . puts the project in the current folder. You now have:

my-site/
├── manage.py          # command-line helper
├── mysite/
│   ├── settings.py    # configuration
│   ├── urls.py        # root URL routes
│   ├── asgi.py
│   └── wsgi.py
└── blog/
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── views.py
    ├── tests.py
    └── migrations/

Register the app in mysite/settings.py:

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",  # add this
]

Start the development server and open http://127.0.0.1:8000:

python manage.py runserver

3. Views and URLs

A view is a function that takes a request and returns a response. In blog/views.py:

from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world!")

def post_detail(request, post_id):
    return HttpResponse(f"You are looking at post {post_id}")

Create blog/urls.py to map paths to views:

from django.urls import path
from . import views

app_name = "blog"

urlpatterns = [
    path("", views.index, name="index"),
    path("<int:post_id>/", views.post_detail, name="post_detail"),
]

Include the app's URLs in the project's mysite/urls.py:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
]

Now /blog/ shows "Hello, world!" and /blog/5/ shows post 5.

Path converters

ConverterMatches
<int:id>Zero or a positive integer
<str:name>Any text without / (the default)
<slug:slug>Letters, numbers, hyphens, underscores
<uuid:id>A UUID
<path:p>Any text, including /

4. Templates

Templates keep HTML out of your Python code. Django looks inside each app's templates/ folder. Create blog/templates/blog/index.html:

<h1>{{ title }}</h1>

{% if posts %}
  <ul>
    {% for post in posts %}
      <li>
        <a href="{% url 'blog:post_detail' post.id %}">{{ post.title }}</a>
        <small>{{ post.created|date:"j M Y" }}</small>
      </li>
    {% endfor %}
  </ul>
{% else %}
  <p>No posts yet.</p>
{% endif %}

Render it from a view with render():

from django.shortcuts import render

def index(request):
    context = {"title": "My Blog", "posts": []}
    return render(request, "blog/index.html", context)

Template syntax at a glance

SyntaxPurpose
{{ variable }}Output a value (HTML-escaped automatically)
{% tag %}Logic: if, for, url, block…
{{ value|filter }}Transform a value, e.g. |upper, |date
{# comment #}Comment, not rendered

Template inheritance

A base layout, blog/templates/blog/base.html:

<!doctype html>
<html>
<head><title>{% block title %}My Blog{% endblock %}</title></head>
<body>
  <main>{% block content %}{% endblock %}</main>
</body>
</html>

A page that extends it:

{% extends "blog/base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
  <h1>Welcome</h1>
{% endblock %}

5. Models and migrations

A model is a Python class that maps to a database table. Django uses SQLite by default, so no setup is needed to start. In blog/models.py:

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    published = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title


class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
    author = models.CharField(max_length=100)
    text = models.TextField()

Create and apply migrations, which turn your models into database tables:

python manage.py makemigrations
python manage.py migrate

Run these two commands every time you change a model.

6. Querying with the ORM

Try queries interactively with python manage.py shell:

from blog.models import Post

# Create
post = Post.objects.create(title="Hello Django", body="My first post")

# Read
Post.objects.all()
Post.objects.get(id=1)
Post.objects.filter(published=True)
Post.objects.filter(title__icontains="django")
Post.objects.exclude(published=True).order_by("-created")[:5]

# Update
post.title = "Hello again"
post.save()
Post.objects.filter(published=False).update(published=True)

# Delete
post.delete()

# Related objects
post.comments.all()
post.comments.create(author="Sam", text="Nice post!")

Use it in a view, with get_object_or_404 to return a 404 page when nothing matches:

from django.shortcuts import get_object_or_404, render
from .models import Post

def index(request):
    posts = Post.objects.filter(published=True).order_by("-created")
    return render(request, "blog/index.html", {"title": "My Blog", "posts": posts})

def post_detail(request, post_id):
    post = get_object_or_404(Post, id=post_id, published=True)
    return render(request, "blog/detail.html", {"post": post})

7. The admin site

Django generates a full admin interface for your models. Create a login first:

python manage.py createsuperuser

Register your models in blog/admin.py:

from django.contrib import admin
from .models import Post, Comment

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ["title", "published", "created"]
    list_filter = ["published"]
    search_fields = ["title", "body"]

admin.site.register(Comment)

Visit http://127.0.0.1:8000/admin/ to add, edit and delete posts.

8. Forms

A ModelForm builds a form, with validation, straight from a model. Create blog/forms.py:

from django import forms
from .models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ["author", "text"]

Handle it in a view:

from django.shortcuts import get_object_or_404, redirect, render
from .forms import CommentForm
from .models import Post

def add_comment(request, post_id):
    post = get_object_or_404(Post, id=post_id)

    if request.method == "POST":
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.post = post
            comment.save()
            return redirect("blog:post_detail", post_id=post.id)
    else:
        form = CommentForm()

    return render(request, "blog/add_comment.html", {"form": form, "post": post})

And in the template. {% csrf_token %} is required on every POST form:

<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Post comment</button>
</form>

9. Class-based views

For common patterns like "list objects" or "show one object", Django provides generic views that cut out boilerplate:

from django.views.generic import DetailView, ListView
from .models import Post

class PostListView(ListView):
    model = Post
    template_name = "blog/index.html"
    context_object_name = "posts"
    queryset = Post.objects.filter(published=True).order_by("-created")

class PostDetailView(DetailView):
    model = Post
    template_name = "blog/detail.html"
# blog/urls.py
urlpatterns = [
    path("", views.PostListView.as_view(), name="index"),
    path("<int:pk>/", views.PostDetailView.as_view(), name="post_detail"),
]

DetailView looks up objects by pk by default, so the URL parameter is named pk here.

10. Returning JSON

For a simple API endpoint, use JsonResponse:

from django.http import JsonResponse
from .models import Post

def post_api(request):
    posts = Post.objects.filter(published=True).values("id", "title", "created")
    return JsonResponse({"posts": list(posts)})

For anything larger, use Django REST Framework, which adds serializers, authentication and browsable APIs.

11. Next steps

Handy commands

CommandWhat it does
python manage.py runserverStart the development server
python manage.py startapp nameCreate a new app
python manage.py makemigrationsGenerate migrations from model changes
python manage.py migrateApply migrations to the database
python manage.py createsuperuserCreate an admin login
python manage.py shellPython shell with your project loaded
python manage.py testRun tests