W3docs

Django Tutorial

Learn Django from scratch: install the framework, create models, views, templates, run migrations, and launch a working web app step by step.

Django is a high-level Python web framework that lets you build secure, scalable web applications quickly. It follows the Model-Template-View (MTV) architectural pattern — a close relative of MVC — and comes with an ORM, an admin interface, URL routing, authentication, and much more built in. Django's "batteries-included" philosophy means you spend your time on your application logic rather than assembling infrastructure.

This tutorial walks through the essential steps: installing Django inside a virtual environment, creating a project and an app, defining models, writing views, building templates, running database migrations, and verifying everything in the browser.

Why Use Django

Batteries Included

Django ships with a built-in admin panel, user authentication, form handling, an ORM, caching support, and internationalization utilities. There is no need to hunt for third-party packages to cover these basics.

Security by Default

Django guards against SQL injection by using parameterized queries through its ORM. It also provides built-in protection against Cross-Site Scripting (XSS), Cross-Site Request Forgery (CSRF), and clickjacking. The secret key, DEBUG mode, and ALLOWED_HOSTS settings further reduce the attack surface.

Scalability

Django powers high-traffic sites including Instagram and Disqus. Its caching framework integrates with Memcached and Redis, and the ORM supports database connection pooling. You can scale vertically or horizontally without changing your application code.

Large Ecosystem

Django's package index lists thousands of third-party apps — REST APIs (djangorestframework), image handling, payment gateways, and more. The framework's stability and long release cycle mean these packages are actively maintained.

How Django's MTV Pattern Works

LayerDjango termResponsibility
DataModelPython class that maps to a database table
PresentationTemplateHTML file with {{ variable }} placeholders
LogicViewPython function (or class) that reads data and returns a response

The URL dispatcher (urls.py) maps incoming request paths to the right view. The view queries models, then passes data to a template. The rendered HTML is returned to the browser.

Setting Up a Virtual Environment

Always isolate Django projects in a virtual environment so that dependencies do not conflict across projects.

# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate        # macOS / Linux
venv\Scripts\activate           # Windows

After activation your prompt shows (venv). Every pip install from this point affects only this environment.

Installing Django

With the virtual environment active, install Django using pip:

pip install django

Verify the installation:

python -m django --version

Django prints a version string such as 5.1.4.

Creating a Django Project

A project is the top-level container for your entire site. Create one with:

django-admin startproject mysite
cd mysite

This generates the following layout:

mysite/
├── manage.py          # Command-line utility for this project
└── mysite/
    ├── __init__.py
    ├── settings.py    # Project configuration (database, installed apps, …)
    ├── urls.py        # Root URL dispatcher
    ├── asgi.py        # ASGI entry point
    └── wsgi.py        # WSGI entry point

manage.py is your main tool for running commands such as starting the dev server, making migrations, and creating a superuser. The inner mysite/ folder is the Python package for the project itself.

Creating a Django App

A project can contain multiple apps — self-contained modules, each responsible for one feature area. Create a blog app:

python manage.py startapp blog

Register it so Django can find its models, templates, and static files. Open mysite/settings.py and add 'blog' to INSTALLED_APPS:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',                   # <-- add this line
]

The app directory looks like this after creation:

blog/
├── __init__.py
├── admin.py       # Register models with the admin interface
├── apps.py        # App configuration
├── migrations/    # Auto-generated migration files
│   └── __init__.py
├── models.py      # Database models
├── tests.py
└── views.py       # View functions

Defining Models

A model is a Python class that subclasses django.db.models.Model. Each class attribute maps to a database column. Django's ORM translates your Python code into SQL and handles all database interactions.

Open blog/models.py and define a Post model:

from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    date_posted = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['-date_posted']   # Newest posts first

Key field types used here:

FieldPurpose
CharField(max_length=N)Short text with a maximum length
TextField()Unlimited-length text
ForeignKey(…, on_delete=CASCADE)Many-to-one relationship; deletes posts when their author is deleted
DateTimeField(auto_now_add=True)Set once when the record is created
DateTimeField(auto_now=True)Updated every time the record is saved

The __str__ method controls how the object displays in the admin panel and in the Python shell.

Running Migrations

After defining or changing models you must create and apply migrations — version-controlled instructions that update the database schema.

# Generate a new migration file from your model changes
python manage.py makemigrations

# Apply all pending migrations to the database
python manage.py migrate

You will see output like:

Migrations for 'blog':
  blog/migrations/0001_initial.py
    - Create model Post

Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Applying blog.0001_initial... OK

Run migrate any time you add, remove, or modify model fields. Never edit migration files by hand unless you fully understand their SQL.

Using the Django Admin Panel

The built-in admin interface lets you create, read, update, and delete records through a browser — no custom UI required. Register the Post model in blog/admin.py:

from django.contrib import admin
from .models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'date_posted')
    list_filter = ('date_posted', 'author')
    search_fields = ('title', 'content')

Create a superuser account so you can log in:

python manage.py createsuperuser

Django prompts for a username, email, and password. Then start the dev server and visit http://127.0.0.1:8000/admin/:

python manage.py runserver

Log in with the superuser credentials and you will see the Posts model listed under the "Blog" section.

Writing Views

A view is a Python function (or class) that receives an HTTP request and returns an HTTP response. Open blog/views.py:

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

def post_list(request):
    """Display all published posts, newest first."""
    posts = Post.objects.all()
    return render(request, 'blog/post_list.html', {'posts': posts})

def post_detail(request, pk):
    """Display a single post by primary key."""
    post = get_object_or_404(Post, pk=pk)
    return render(request, 'blog/post_detail.html', {'post': post})

get_object_or_404 is a helpful shortcut: it fetches the object from the database or automatically returns a 404 response if it does not exist. The third argument to render is a context dictionary — its keys become template variables.

Creating Templates

Templates are HTML files that contain Django Template Language (DTL) tags. Create the directory structure:

blog/
└── templates/
    └── blog/
        ├── base.html
        ├── post_list.html
        └── post_detail.html

blog/templates/blog/base.html — a shared layout:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}My Blog{% endblock %}</title>
</head>
<body>
    <header><h1>My Blog</h1></header>
    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>

blog/templates/blog/post_list.html — the list page:

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

{% block title %}All Posts{% endblock %}

{% block content %}
  {% for post in posts %}
    <article>
      <h2><a href="/blog/{{ post.pk }}/">{{ post.title }}</a></h2>
      <p>By {{ post.author }} on {{ post.date_posted|date:"N j, Y" }}</p>
      <p>{{ post.content|truncatewords:30 }}</p>
    </article>
  {% empty %}
    <p>No posts yet.</p>
  {% endfor %}
{% endblock %}

blog/templates/blog/post_detail.html — the detail page:

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

{% block title %}{{ post.title }}{% endblock %}

{% block content %}
  <h2>{{ post.title }}</h2>
  <p>By {{ post.author }} on {{ post.date_posted|date:"N j, Y" }}</p>
  <div>{{ post.content }}</div>
  <a href="/">Back to all posts</a>
{% endblock %}

Key DTL features used above:

Tag / FilterWhat it does
{% extends "…" %}Inherit from a base template
{% block name %}Define a replaceable section
{{ variable }}Output a value (HTML-escaped automatically)
{% for … %} / {% empty %}Loop with a fallback for empty lists
|date:"N j, Y"Format a datetime value
|truncatewords:30Shorten text to 30 words

Configuring URLs

URL configuration maps request paths to view functions.

blog/urls.py — create this file in the blog directory:

from django.urls import path
from . import views

app_name = 'blog'   # Enables namespaced URL reversing

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('<int:pk>/', views.post_detail, name='post_detail'),
]

mysite/urls.py — include the app URLs in the project root:

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls', namespace='blog')),
    path('', include('blog.urls', namespace='blog_root')),
]

The <int:pk> path converter captures an integer from the URL and passes it as the pk keyword argument to post_detail.

Verifying the Full Flow

Start the development server:

python manage.py runserver

Then open http://127.0.0.1:8000/ in your browser. If no posts exist yet, the template shows "No posts yet." Log in to http://127.0.0.1:8000/admin/ and add a post. Refresh the home page — the post appears immediately.

The request lifecycle for the home page is:

  1. Browser sends GET /
  2. Django's URL dispatcher matches '' and calls post_list
  3. post_list queries Post.objects.all() (translates to SELECT * FROM blog_post ORDER BY date_posted DESC)
  4. Django renders post_list.html, inserting the posts into the template
  5. The rendered HTML is returned to the browser

Django ORM Query Cheatsheet

The ORM lets you build SQL queries using Python method chains. Understanding the most common lookups saves significant time:

# All posts
Post.objects.all()

# Posts by a specific author
Post.objects.filter(author__username='alice')

# Posts containing a word in the title (case-insensitive)
Post.objects.filter(title__icontains='django')

# The five most recent posts
Post.objects.order_by('-date_posted')[:5]

# Count posts
Post.objects.count()

# Get one object (raises DoesNotExist if not found)
Post.objects.get(pk=1)

# Exclude posts by a specific author
Post.objects.exclude(author__username='alice')

Each of these generates a SQL query only when the queryset is evaluated (iterated, sliced, or converted to a list). This is called lazy evaluation and keeps unnecessary database calls to a minimum.

Project Structure Summary

After completing this tutorial, the project structure looks like this:

mysite/
├── manage.py
├── mysite/
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── blog/
    ├── admin.py
    ├── migrations/
    │   └── 0001_initial.py
    ├── models.py
    ├── templates/
    │   └── blog/
    │       ├── base.html
    │       ├── post_list.html
    │       └── post_detail.html
    ├── urls.py
    └── views.py

Common Gotchas

Forgetting to register the app. If 'blog' is missing from INSTALLED_APPS, Django will not find its models or templates and makemigrations will produce no output for that app.

Skipping migrate after makemigrations. Running only makemigrations creates the migration file but does not touch the database. Always follow up with migrate.

Template not found errors. Django looks for templates inside each app's templates/ directory. The extra blog/ subfolder inside templates/ (templates/blog/post_list.html) is a convention that prevents name collisions between apps — do not skip it.

DEBUG = True in production. The development server and DEBUG = True expose tracebacks to anyone. Set DEBUG = False and configure ALLOWED_HOSTS before deploying.

Next Steps

Django's official documentation at docs.djangoproject.com covers class-based views, forms, authentication, REST APIs with Django REST Framework, and deployment to production — all natural next steps once you are comfortable with the basics covered here.

Was this page helpful?