Django Beginner

Getting Started with Django: Your First Web App

Learn how to set up Django, create your first project, and build a simple web application from scratch. Perfect for beginners.

KC Ramo · DjangoZen Team Mar 29, 2026 19 min read 546 views

Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. In this tutorial, you'll build your first Django web application from scratch — a simple task manager — while learning the core concepts that make Django one of the most popular frameworks in the world.

Prerequisites

  • Python 3.10+ installed on your system
  • Basic understanding of Python syntax
  • A code editor (VS Code, PyCharm, or similar)
  • Command line / terminal basics

What is Django?

Django was created in 2003 by web developers at the Lawrence Journal-World newspaper. It was released publicly in 2005 and has since become one of the most widely-used web frameworks. Companies like Instagram, Mozilla and Disqus have run Django in production.

Django follows the MVT (Model-View-Template) architectural pattern:

  • Model — Defines your data structure (database tables)
  • View — Contains the business logic (what happens when a URL is visited)
  • Template — The HTML presentation layer (what the user sees)
Why Django? Django includes an ORM, admin panel, authentication system, URL routing, template engine, and security features out of the box. This "batteries-included" philosophy means you spend less time configuring and more time building.

Step 1: Set Up Your Environment

First, create a virtual environment and install Django:

# Create a project directory
mkdir my_first_django_app
cd my_first_django_app

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

# Install Django
pip install django

# Verify installation
python -m django --version
Tip: Always use a virtual environment for Python projects. It keeps your project dependencies isolated and prevents version conflicts between projects.

Pin the version you are learning with

This tutorial targets Django 5.2 LTS (security fixes until April 2028) on Python 3.12 or newer. A bare pip install django installs whatever is newest on the day you run it, so pin the release series and record it in a requirements.txt file:

pip install "Django>=5.2,<6.0"

# requirements.txt
Django>=5.2,<6.0

# on another machine:
pip install -r requirements.txt

Keep the quotes: without them most shells treat > as "redirect output to a file".

Common setup errors

What you seeFix
running scripts is disabled on this system (PowerShell)Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned once, or activate from cmd.exe with venv\Scripts\activate.bat.
No module named djangoThe virtual environment is not active. Activate it again — you must do this in every new terminal. The (venv) prefix in your prompt confirms it.
error: externally-managed-environmentYou ran pip outside a virtual environment on a modern Linux system. Create and activate the venv first.

Step 2: Create a Django Project

A Django project is the overall container for your web application. It holds configuration, settings, and one or more apps.

# Create the project
django-admin startproject taskmanager .

# The dot (.) tells Django to create files in the current directory

This creates the following structure:

my_first_django_app/
├── manage.py              # Command-line utility
├── taskmanager/
│   ├── __init__.py        # Python package marker
│   ├── settings.py        # Project configuration
│   ├── urls.py            # URL declarations
│   ├── asgi.py            # ASGI entry point
│   └── wsgi.py            # WSGI entry point
└── venv/                  # Virtual environment

Let's test that everything works:

python manage.py runserver

Open http://127.0.0.1:8000/ in your browser. You should see the Django welcome page with a rocket ship. Congratulations — your first Django project is running!

What each generated file does

  • manage.py — runs Django commands (runserver, migrate, shell, test) with your settings already selected. You rarely edit it.
  • settings.py — all configuration: DEBUG, INSTALLED_APPS, DATABASES (SQLite in db.sqlite3 by default) and SECRET_KEY, which signs cookies and must stay private in production.
  • urls.py — the root URL list; every request starts here.
  • asgi.py / wsgi.py — entry points for production servers such as Gunicorn. Ignore them until deployment.

The terminal may also print "You have N unapplied migration(s)". That is not an error: the built-in apps (users, sessions) have tables that do not exist yet. Step 4's migrate creates them. If you see Error: That port is already in use., stop the other server with Ctrl+C or run python manage.py runserver 8001.

Step 3: Create Your First App

In Django, an app is a module that handles a specific piece of functionality. A project can contain multiple apps.

python manage.py startapp tasks

Register the app in settings.py:

# taskmanager/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tasks',  # Add your app here
]

startapp created models.py (data), views.py (request handling), admin.py (admin registration), tests.py and a migrations/ folder. It does not create urls.py or templates/; you add those in Steps 7 and 8.

Common error: if 'tasks' is missing from INSTALLED_APPS, Django ignores the app completely: makemigrations tasks says the app cannot be found and its templates raise TemplateDoesNotExist. Whenever your app seems "invisible", check this list first.

Step 4: Define Your Model

Models define your database structure. Each model maps to a database table, and each field maps to a column.

# tasks/models.py
from django.db import models
from django.utils import timezone


class Task(models.Model):
    PRIORITY_CHOICES = [
        ('low', 'Low'),
        ('medium', 'Medium'),
        ('high', 'High'),
    ]

    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    completed = models.BooleanField(default=False)
    priority = models.CharField(
        max_length=10,
        choices=PRIORITY_CHOICES,
        default='medium'
    )
    created_at = models.DateTimeField(default=timezone.now)
    due_date = models.DateField(null=True, blank=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return self.title

Now create and apply the migration:

# Create migration file
python manage.py makemigrations tasks

# Apply migration to database
python manage.py migrate
What are migrations? Migrations are Django's way of propagating changes you make to your models into the database schema. Think of them as version control for your database.

Details in the model that trip beginners up

  • default=timezone.now passes the function, so it runs for every new task. Writing timezone.now() would freeze a single timestamp when the module loads.
  • blank=True affects forms (the field may be empty); null=True affects the database (the column may be NULL). Optional dates need both; optional text fields need only blank=True.
  • You never declared an id, but Django adds an auto-incrementing primary key. That is the pk the views use.

Remember the cycle: edit models.py → makemigrations → migrate. Skipping migrate causes OperationalError: no such table: tasks_task. Run python manage.py showmigrations to see which migrations are applied.

Try the model in the shell

In Django 5.2, manage.py shell imports your models automatically:

python manage.py shell
>>> Task.objects.create(title="Buy milk", priority="high")
<Task: Buy milk>
>>> Task.objects.filter(completed=False).count()
1
>>> Task.objects.get(title="Buy milk").get_priority_display()
'High'

The shell shows <Task: Buy milk> thanks to your __str__ method; without it you would see Task object (1).

Step 5: Set Up the Admin Panel

Django's built-in admin panel gives you a full CRUD interface for free:

# tasks/admin.py
from django.contrib import admin
from .models import Task


@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    list_display = ('title', 'priority', 'completed', 'due_date', 'created_at')
    list_filter = ('priority', 'completed')
    search_fields = ('title', 'description')
    list_editable = ('completed',)

Create a superuser account:

python manage.py createsuperuser

Visit http://127.0.0.1:8000/admin/ and log in. You can now add, edit, and delete tasks through the admin interface.

The password you type in createsuperuser is hidden on screen — that is normal. If you forget it, run python manage.py changepassword <username>. A "no such table" error on login means migrate has not been run.

Step 6: Create Views

Views handle the logic of your application. Let's create views for listing, creating, and toggling tasks:

# tasks/views.py
from django.shortcuts import render, redirect, get_object_or_404
from .models import Task


def task_list(request):
    tasks = Task.objects.all()
    return render(request, 'tasks/task_list.html', {'tasks': tasks})


def task_create(request):
    if request.method == 'POST':
        title = request.POST.get('title')
        description = request.POST.get('description', '')
        priority = request.POST.get('priority', 'medium')
        if title:
            Task.objects.create(
                title=title,
                description=description,
                priority=priority
            )
        return redirect('task_list')
    return render(request, 'tasks/task_create.html')


def task_toggle(request, pk):
    task = get_object_or_404(Task, pk=pk)
    task.completed = not task.completed
    task.save()
    return redirect('task_list')


def task_delete(request, pk):
    task = get_object_or_404(Task, pk=pk)
    if request.method == 'POST':
        task.delete()
    return redirect('task_list')

How these views work

Each view receives a request and must return a response; forgetting return produces "didn't return an HttpResponse object". render() fills a template with the context dictionary, whose keys become template variable names. request.POST holds submitted form values, keyed by each input's name. get_object_or_404 turns a missing task into a clean 404 page instead of a crash.

After a successful POST, every view returns redirect('task_list'). This Post/Redirect/Get pattern means pressing refresh repeats a harmless GET rather than resubmitting the form and creating a duplicate task.

The create template

task_create renders tasks/task_create.html; without it, clicking "+ Add Task" raises TemplateDoesNotExist. Add it next to the list template in Step 8:

<!-- tasks/templates/tasks/task_create.html -->
<h1>New Task</h1>
<form method="post">
    {% csrf_token %}
    <input type="text" name="title" maxlength="200" required>
    <textarea name="description"></textarea>
    <select name="priority">
        <option value="low">Low</option>
        <option value="medium" selected>Medium</option>
        <option value="high">High</option>
    </select>
    <button type="submit">Save</button>
</form>

The name attributes must match what the view reads. A typo such as name="titel" raises no error: request.POST.get('title') returns None, nothing is saved, and you are silently redirected. When a form "does nothing", compare the names first.

Edge case: browser attributes like required are not security. Anyone can POST priority=urgent or a huge title, and objects.create() does not check choices or max_length. Django's ModelForm validates this on the server, which is why forms are the first item in What's Next.

Step 7: Configure URLs

URLs map web addresses to views:

# tasks/urls.py (create this file)
from django.urls import path
from . import views

urlpatterns = [
    path('', views.task_list, name='task_list'),
    path('create/', views.task_create, name='task_create'),
    path('toggle/<int:pk>/', views.task_toggle, name='task_toggle'),
    path('delete/<int:pk>/', views.task_delete, name='task_delete'),
]
# taskmanager/urls.py
from django.contrib import admin
from django.urls import path, include

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

For /toggle/3/, the root urls.py hands the path to tasks/urls.py, where <int:pk> converts "3" to an integer and Django calls task_toggle(request, pk=3) — so the parameter must be named pk. Templates and views refer to URLs by name, so you can change a path later without editing them.

Common URL errors

ErrorTypical cause
Page not found (404)No pattern matched. With DEBUG = True the page lists every pattern tried; look for a missing trailing slash or typo.
NoReverseMatchA misspelled URL name, or a missing argument: {% url 'task_toggle' %} instead of {% url 'task_toggle' task.pk %}.
No module named 'tasks.urls'tasks/urls.py has not been created yet.

Step 8: Create the Template

Create the directory tasks/templates/tasks/ and add the list template:

<!-- tasks/templates/tasks/task_list.html -->
<h1>My Tasks</h1>
<a href="{% url 'task_create' %}">+ Add Task</a>

{% for task in tasks %}
<div class="task {% if task.completed %}completed{% endif %}">
    <strong>{{ task.title }}</strong>
    <small>({{ task.get_priority_display }})</small>
    <form method="post" action="{% url 'task_toggle' task.pk %}">
        {% csrf_token %}
        <button>{% if task.completed %}Undo{% else %}Done{% endif %}</button>
    </form>
</div>
{% empty %}
<p>No tasks yet!</p>
{% endfor %}

Why tasks/templates/tasks/? Django searches the templates/ folder of every installed app, and the inner tasks/ folder keeps your file names from colliding with other apps. If you see TemplateDoesNotExist, the error page's "Template-loader postmortem" lists every path Django checked — compare it with where your file actually is.

Template syntax comes in three kinds: {{ variable }} for values (methods like get_priority_display are called without parentheses), {% tag %} for logic, and {{ value|filter }} for formatting, e.g. {{ task.created_at|date:"M j, Y" }}. Misspelled variables render as empty strings rather than errors. Values are auto-escaped, so a task titled <script> is shown as text, not executed.

Adding the delete button

The list has no delete control yet. Add this form inside the loop, after the toggle form — it must be POST, because task_delete ignores GET on purpose:

<form method="post" action="{% url 'task_delete' task.pk %}">
    {% csrf_token %}
    <button type="submit">Delete</button>
</form>

Step 9: Understanding CSRF Protection

Notice the {% csrf_token %} tag in forms above. Django requires this token in every POST form to prevent Cross-Site Request Forgery attacks. If you forget it, you'll get a 403 error.

Security Note: Never remove CSRF protection. It's one of Django's built-in security features that protects your users from malicious form submissions.

If you see "403 Forbidden — CSRF verification failed", with DEBUG = True the page states the reason. The usual causes: the form lacks {% csrf_token %}; the template was rendered without the request (always use render(request, ...)); or the browser blocked or lost the csrftoken cookie — reload the form and try again. Never "fix" it with @csrf_exempt.

Step 10: Run and Test

python manage.py runserver

Visit http://127.0.0.1:8000/ — you now have a working task manager with:

  • Task listing with priority indicators
  • Create new tasks with title, description, and priority
  • Toggle tasks complete/incomplete
  • Delete tasks
  • Admin panel for full management

Testing your app automatically

Django's test client acts like a browser without a running server. Put this in tasks/tests.py:

from django.test import TestCase
from django.urls import reverse

from .models import Task


class TaskViewTests(TestCase):
    def test_create_task(self):
        response = self.client.post(reverse("task_create"), {"title": "Write tests"})
        self.assertRedirects(response, reverse("task_list"))
        self.assertTrue(Task.objects.filter(title="Write tests").exists())

    def test_empty_title_is_ignored(self):
        self.client.post(reverse("task_create"), {"title": ""})
        self.assertEqual(Task.objects.count(), 0)

    def test_delete_ignores_get(self):
        task = Task.objects.create(title="Keep me")
        self.client.get(reverse("task_delete", args=[task.pk]))
        self.assertTrue(Task.objects.filter(pk=task.pk).exists())

Run python manage.py test tasks. Django creates a separate, empty test database, so your real tasks are never touched, and each test starts clean. Method names must begin with test or they are skipped. reverse() is the Python version of {% url %}. The test client skips CSRF checks by default — a convenience, not a sign that protection is off.

Practice exercises

  1. Filter: make /?show=open list only unfinished tasks using request.GET.get('show') and Task.objects.filter(completed=False).
  2. Detail page: add task_detail(request, pk) at <int:pk>/ using get_object_or_404, and link each title to it. Confirm /9999/ returns 404.
  3. Overdue: add an is_overdue() method returning True when due_date is before timezone.localdate() and the task is not completed. Use it in the template and test a task with no due date.
  4. Toggle test: write a test that toggles a task twice and asserts it ends up not completed.

Why Django, and what it gives you

Django is a batteries-included web framework, meaning it provides out of the box nearly everything a web application needs — an ORM for the database, an admin interface, authentication, form handling, security protections, and a templating system. This is its great strength for getting started: rather than assembling a stack from many pieces, you get a coherent, well-integrated set of tools designed to work together, with sensible defaults and strong security built in. Understanding that Django aims to handle the common needs of web development for you, so you can focus on what makes your application unique, sets the right expectations for why it is such a productive starting point.

How a Django project is structured

A Django project is organized into apps — self-contained modules each handling a piece of functionality — within an overall project that holds configuration. This structure encourages separating concerns: one app for accounts, another for a blog, another for a shop, each with its own models, views, and templates. Understanding this organization early helps you build maintainable applications, because it gives you a natural way to divide your code as it grows. Grasping the project-and-apps structure, and how Django's settings, URLs, and apps fit together, is the foundation that makes the rest of the framework comprehensible rather than mysterious.

The request-response flow

At the core of how Django works is a simple flow: a request arrives at a URL, Django's URL configuration routes it to a view, the view does the work — often querying models and rendering a template — and returns a response. Understanding this path from URL to view to template to response is the mental model that makes everything else click, because every page in your application follows it. Once you see how a request flows through URLs, views, models, and templates to produce what the user sees, the pieces of Django stop being isolated concepts and become a coherent pipeline you can reason about and build within confidently.

Where to go after your first app

Building your first app teaches the fundamentals, but Django has much more depth to grow into. After the basics, the natural next steps include the admin interface for managing data, forms for handling user input safely, authentication for user accounts, and the deployment process for getting your app online. Each builds on the foundation of models, views, URLs, and templates you started with. Knowing that these capabilities are there to learn next — and that they follow naturally from the fundamentals — turns a first app from an endpoint into a starting point, giving you a clear path from a working first project toward building real, complete applications.

What's Next?

You've built a functional Django application! Here are some next steps to continue learning:

TopicWhat You'll Learn
Django FormsUse Django's form classes for validation and rendering
User AuthenticationAdd login, registration, and permissions
Static FilesServe CSS, JavaScript, and images properly
Class-Based ViewsUse Django's powerful generic views for less code
Django REST FrameworkBuild APIs for frontend frameworks or mobile apps
DeploymentPut your app online with Gunicorn and Nginx
Pro Tip: Read the official Django documentation at docs.djangoproject.com — it's one of the best docs in any framework. The official tutorial ("Writing your first Django app") goes even deeper into polls app concepts.