Django Roadmap

Django is a Python web framework that includes most of what a site needs: an ORM, an admin interface, forms, authentication, templating and security protections. It is used for content sites, internal tools, marketplaces and APIs, and companies that build on Python hire for it steadily.

This roadmap takes you from creating your first project to deploying one. Each stage lists topics in learning order, explains each one in plain terms and gives you one exercise. After the stages come projects, interview questions with model answers, common mistakes and a readiness checklist.

By the end you should be able to design models and migrations, write views and templates, secure a site with authentication and permissions, build a REST API, find slow queries, test your work and run it in production.

Level
Beginner to advanced
Time
12-16 weeks at about 8 hours a week
Scope
57 topics in 9 stages
Before you start
Python Roadmap fundamentals: functions, classes, modules and virtual environments; Basic HTML and an understanding of HTTP requests and responses; Basic SQL ideas such as tables, keys and joins; see SQL Roadmap; Optional: Git and GitHub Roadmap for the projects

What you will be able to do

  • Create a Django project, split it into apps and configure it safely for several environments
  • Design models, relationships and migrations, and change a schema without losing data
  • Query data with the ORM, remove N+1 problems and keep related writes consistent with transactions
  • Build pages with views, templates, forms and the admin, including login and permissions
  • Expose a documented JSON API with serializers, viewsets, authentication and pagination
  • Write automated tests, debug with a toolbar and a debugger, and add useful logging
  • Move slow work to Celery tasks and deploy with a production-ready configuration

The roadmap at a glance

  1. Stage 1Web Basics and Project SetupBeginner · 1 week · 8 topics
  2. Stage 2URLs and ViewsBeginner · 1-2 weeks · 8 topics
  3. Stage 3Templates and Static FilesBeginner · 1-2 weeks · 6 topics
  4. Stage 4Models and MigrationsIntermediate · 2 weeks · 7 topics
  5. Stage 5The Django ORM in DepthIntermediate · 2 weeks · 6 topics
  6. Stage 6Forms, Admin and AuthenticationIntermediate · 2 weeks · 7 topics
  7. Stage 7Built-in Features and APIsIntermediate · 3-4 weeks · 8 topics
  8. Stage 8Testing, Debugging and LoggingIntermediate · 1-2 weeks · 4 topics
  9. Stage 9Asynchronous Work and DeploymentAdvanced · 2 weeks · 3 topics

Stage 1 Web Basics and Project Setup

Beginner1 week8 topics

Goal. Explain how a request reaches Django, and create and run a project in an isolated environment.

How the Web Works Core

A browser sends an HTTP request made of a method (GET, POST and so on), a path, headers and sometimes a body. A server answers with a status code, headers and a body, usually HTML or JSON. HTTP keeps no memory between requests, so sessions and cookies exist to recognise the same visitor again.

Before the request, the browser turns a domain name into an address through DNS, then opens a connection, usually encrypted with TLS. Your Django code only ever sees the final step: a request object comes in, and a response goes out. Everything else is handled by the browser, the network and a web server in front of Django.

Understanding status codes (200, 301, 404, 500) and the difference between GET (read) and POST (change) makes the rest of the roadmap much easier.

Practice. Open the browser network tab on any site and write down the method, status and two headers of three requests.

Web Frameworks and Introduction to Django Core

A web framework handles the repeated parts of a site: routing URLs, reading requests, talking to a database, rendering pages and keeping things secure. Django, first released in 2005, is a high-level Python framework often described as "batteries included". The Introduction to Django is short: it ships with an ORM, an admin interface, forms, authentication, sessions, templating, caching hooks and security protections for common attacks.

Flask and FastAPI are lighter alternatives where you choose each part yourself. Django suits content-heavy sites, internal tools and APIs where speed of delivery and consistency matter. Instagram, Pinterest and many news sites have used it. If you need Python first, follow Python Roadmap and come back.

Practice. Write down three features Django gives you that you would have to assemble yourself in Flask.

MVT: how Django arranges code Core

Many web frameworks use the Model-View-Controller pattern, sometimes mistyped as "The MCV Model". Django has its own flavour called MVT: Model, View, Template.

  • The model describes data and talks to the database.
  • The view receives a request, asks models for data and returns a response. It does the job that a controller does elsewhere.
  • The template is the HTML with placeholders that the view fills in.

Django itself plays the controller role by matching the URL to a view, so you never write that part. The practical rule: keep business logic in models or service functions, keep views thin, and keep templates free of anything beyond display decisions.

Practice. Draw the path of a request to /books/ through URL, view, model and template on a sheet of paper.

Virtual Environments Core

A virtual environment is a private folder of Python packages for one project, so that different projects can use different versions without conflict. Create one with the standard tool and activate it before installing anything.

python -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install django
pip freeze > requirements.txt

Never install project packages into the system Python. Record dependencies in requirements.txt, or use a tool such as Poetry, uv or pip-tools that also locks exact versions, and keep .venv out of version control. A new teammate should be able to recreate the environment with one command.

Practice. Create two environments with different Django versions and show that django-admin --version differs in each.

Installing Django Core

Install with pip install django inside the environment, then check with python -m django --version. Pick a version deliberately. Django releases a feature version about every eight months, and some of them are long-term support (LTS) releases that receive security fixes for around three years. Use an LTS or the latest release, and read the deprecation notes before upgrading, because features are deprecated for two releases before removal.

Check the supported Python versions for your Django release in its documentation. Pin the version in requirements.txt, such as Django>=5.2,<5.3, so that an install next month does not change behaviour. Install a database driver only when you need it; SQLite works with no extra package.

Practice. Install Django in a fresh environment and read the release notes for the version you installed.

Projects and Apps Core

In Django a project is the whole site with its settings and root URLs, and an app is a self-contained feature, such as blog or accounts. One project holds many apps, and a good app could in principle be reused elsewhere.

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

After creating an app, add it to INSTALLED_APPS in the settings, or Django will not find its models, templates or management commands. Keep apps small and focused. A common mistake is putting everything in one app, which makes a tangle; another is creating dozens of tiny apps with circular imports. Aim for apps that match a clear domain concept.

Practice. Create a project with two apps and register both in INSTALLED_APPS.

manage.py and Running Your Django Project Core

manage.py is a thin wrapper around django-admin that sets the project's settings module. It is how you run most commands: runserver, makemigrations, migrate, createsuperuser, shell, test, collectstatic and check.

Running Your Django Project locally is python manage.py runserver, which serves on port 8000 and reloads when code changes. Visit the address and you see a welcome page when DEBUG is true.

You can write your own commands by placing a module under an app's management/commands/ folder. They are ideal for imports, clean-up jobs and one-off fixes, because they have full access to models and settings.

Practice. Write a custom management command that prints the number of rows in one model.

settings.py Core

settings.py holds the configuration for the whole project: INSTALLED_APPS, MIDDLEWARE, DATABASES, TEMPLATES, STATIC_URL, ALLOWED_HOSTS, SECRET_KEY, DEBUG, time zone, language and more.

Production safety depends on a few of them. Keep SECRET_KEY and database passwords out of the repository by reading them from environment variables. Set DEBUG = False in production, since debug mode shows source code and settings in error pages. Set ALLOWED_HOSTS to the real domains. For several environments, split settings into a base module and small overrides, or use a package such as django-environ.

Run python manage.py check --deploy to receive a list of security problems with the current settings.

Practice. Move SECRET_KEY, DEBUG and the database URL to environment variables and run check --deploy.

Stage 2 URLs and Views

Beginner1-2 weeks8 topics

Goal. Route requests to views, write function-based and class-based views, and understand the full request and response cycle.

Views Core

A view is a Python callable that takes an HttpRequest and returns an HttpResponse. It can return HTML, JSON, a redirect, a file or an error. The request object carries the method, GET and POST data, uploaded files, cookies, the session and, once authentication runs, request.user.

from django.http import HttpResponse
def hello(request):
    return HttpResponse("Hello, " + request.GET.get("name", "world"))

Useful shortcuts live in django.shortcuts: render combines a template and context, redirect returns a redirect, and get_object_or_404 loads an object or raises a 404. Views should stay short: read input, call model or service code, choose a response. Anything reusable belongs elsewhere.

Practice. Write a view that returns JSON with the current time and the name taken from a query parameter.

Function-Based Views Core

Function-Based Views are plain functions. They are the clearest way to start because the flow is visible from top to bottom.

def book_detail(request, pk):
    book = get_object_or_404(Book, pk=pk)
    if request.method == "POST":
        form = ReviewForm(request.POST)
        if form.is_valid():
            form.save(book)
            return redirect("book-detail", pk=pk)
    else:
        form = ReviewForm()
    return render(request, "books/detail.html", {"book": book, "form": form})

Decorators add behaviour without cluttering the body: @login_required, @require_POST, @cache_page(60). The weakness is repetition: list, create, edit and delete views for many models look alike, which is where class-based views help.

Practice. Write list, detail and create views for a Book model as functions.

URL Configuration in Django Core

URL Configuration maps paths to views. The root ROOT_URLCONF module holds a list called urlpatterns, and each app usually has its own urls.py, included from the root with include().

# mysite/urls.py
from django.urls import path, include
urlpatterns = [
    path("admin/", admin.site.urls),
    path("books/", include("books.urls")),
]
# books/urls.py
urlpatterns = [
    path("", views.book_list, name="book-list"),
    path("<int:pk>/", views.book_detail, name="book-detail"),
]

Django checks URL patterns in order and uses the first match, so put specific ones before general ones. Using urls.py in Django Apps keeps each app's routes next to its views, and app_name gives them a namespace.

Practice. Create a books app with its own urls.py and include it under /books/ in the root configuration.

URL patterns, Path Converters and Regex Paths Core

path() patterns use converters to capture parts of the URL and pass them to the view as arguments. Built-in Path Converters are str (anything but a slash), int, slug, uuid and path (includes slashes). You can register a custom converter class with register_converter for formats such as a date.

For complex matching, re_path() accepts a regular expression, which is the older style and the one used by the removed url() function. Regex Paths look like this: re_path(r"^archive/(?P<year>[0-9]{4})/$", views.archive).

Prefer path() unless you really need a regular expression. Keep URLs readable and stable, since they are public: lowercase words, hyphens, nouns, and a trailing slash chosen consistently.

Practice. Add a route that accepts a four-digit year and month and returns the matching archive, using a custom converter.

Named URLs and Reverse URL Core

Never write URL strings by hand in code or templates. Give each pattern a name and generate the address from it. This is Named URLs, and it means you can change the path in one place.

In Python, reverse("book-detail", kwargs={"pk": 3}) returns /books/3/. Reverse URL in templates is {% url "book-detail" pk=book.pk %}. With app_name set in the app's urls.py, use the namespace: books:book-detail. For models, define get_absolute_url() and call it wherever you link to an object; the admin and generic views use it too.

reverse_lazy delays evaluation until it is needed and is required for class attributes such as success_url, which are evaluated when the module is imported, before URLs are loaded.

Practice. Replace every hard-coded link in a small project with url tags and a get_absolute_url method.

Request-Response Flow in Django Core

Knowing the full journey helps with debugging. A web server (Gunicorn, Uvicorn) hands the request to Django through WSGI or ASGI. Django builds an HttpRequest, then passes it through the middleware stack from top to bottom. The URL resolver picks a view. The view runs and returns a response, which passes back up through the middleware in reverse order, and is sent to the client.

Along the way: sessions and authentication middleware attach request.session and request.user, the CSRF middleware checks tokens on unsafe methods, and an exception in the view is turned into a 404 or 500 response, possibly rendered by your error templates.

A middleware can short-circuit the chain by returning a response early, which is how redirects for missing login or maintenance pages work.

Practice. Add a middleware that prints the path on the way in and the status on the way out, then trace one request.

Class-Based Views Core

A class-based view groups behaviour as methods. View dispatches by HTTP method to get(), post() and so on, and as_view() turns the class into a callable for the URL configuration.

class AboutView(View):
    def get(self, request):
        return render(request, "about.html")
# urls.py:  path("about/", AboutView.as_view())

The main benefit is reuse through inheritance and mixins, such as LoginRequiredMixin and PermissionRequiredMixin, which must come first in the base class list. Customizing Views means overriding hook methods such as get_queryset(), get_context_data(), form_valid() and get_success_url(), rather than rewriting the whole flow. The cost is indirection: to see what a class does you may need to read several parents. The Classy Class-Based Views website is a common reference for the method call order.

Practice. Convert one of your function-based views to a class and add LoginRequiredMixin.

Generic Views Recommended

Generic Views are ready-made classes for common jobs, so a typical page needs only a model and a template name.

  • ListView shows a list of objects, with pagination built in.
  • DetailView shows one object, found by pk or slug.
  • CreateView shows a form for a new object and saves it on valid input.
  • UpdateView edits an existing object.
  • DeleteView asks for confirmation, then deletes.
class BookList(ListView):
    model = Book
    paginate_by = 20
class BookCreate(CreateView):
    model = Book
    fields = ["title", "author"]
    success_url = reverse_lazy("book-list")

By default templates are named book_list.html, book_detail.html and book_form.html inside the app's templates folder, and the context holds object_list or object.

Practice. Build full CRUD for one model using only the five generic views and three templates.

Stage 3 Templates and Static Files

Beginner1-2 weeks6 topics

Goal. Render pages with the Django template language and serve static and uploaded files correctly.

Templates Core

Templates are text files, usually HTML, with placeholders and a little logic. The Django template language is deliberately limited, so that designers can edit it and logic stays in Python. A view passes a dictionary called the context, and render(request, "page.html", context) produces the response.

Django looks for templates in each app's templates/ folder when APP_DIRS is on, and in any folders listed in DIRS. Put app templates in a subfolder named after the app, books/templates/books/list.html, so that names from different apps never collide.

DTL Syntax has three parts: variables in double braces, tags in brace-percent pairs, and filters applied with a pipe. Output is escaped for HTML by default, which protects against cross-site scripting.

Practice. Create a template that displays a page title and a list of three items passed from a view.

Variables, Conditionals and Loops in templates Core

Variables in Django Templates are written {{ book.title }}. The dot looks up a dictionary key, an attribute, a method call without arguments or a list index, in that order. A missing value renders as an empty string, which hides typos, so check output carefully.

Conditional Logic in Django Templates uses if, elif, else with and, or, not, in and comparison operators. The For Loop in Django Templates is for with an optional empty block and helper variables such as forloop.counter and forloop.last.

{% for book in books %}
  <li>{{ forloop.counter }}. {{ book.title }}</li>
{% empty %}
  <li>No books yet.</li>
{% endfor %}

Complex conditions belong in the view or a model method, not in the template.

Practice. Render a book list with row numbers, an empty message and a different style for books that are out of stock.

Template Inheritance Core

Template Inheritance lets pages share a layout. A base template defines blocks, and child templates fill them.

{# base.html #}
<title>{% block title %}My site{% endblock %}</title>
<main>{% block content %}{% endblock %}</main>

{# list.html #}
{% extends "base.html" %}
{% block title %}Books{% endblock %}
{% block content %}...{% endblock %}

extends must be the first tag. Use {{ block.super }} to add to a parent block instead of replacing it, and include to insert a reusable fragment such as a card. Comments in Django Templates are written with the single-line {# ... #} form or the multi-line comment tag; they never reach the browser, unlike HTML comments. Keep one base layout per major area of the site.

Practice. Create a base layout with a navigation block and make two pages extend it.

Filters and Custom Filters Recommended

Filters transform a value for display: {{ name|upper }}, {{ price|floatformat:2 }}, {{ created|date:"d M Y" }}, {{ items|length }}, {{ text|truncatewords:20 }} and {{ value|default:"none" }}.

Filters & Custom Filters: when no built-in fits, write your own in a templatetags package inside an app, which needs an __init__.py file.

from django import template
register = template.Library()

@register.filter
def initials(full_name):
    return "".join(p[0] for p in full_name.split()).upper()

Load it in a template with {% load my_filters %}. Restart the development server the first time you add the package. Mark output as safe only if you built the HTML yourself from escaped parts, because |safe turns off protection against script injection.

Practice. Write a custom filter that formats a number of pages as "4 h 20 min" reading time and use it in a template.

Static Files Core

Static files are CSS, JavaScript and images that do not change per request. In development, runserver serves them from each app's static/ folder and from STATICFILES_DIRS when django.contrib.staticfiles is installed. In templates use {% load static %} then {% static "css/site.css" %} so that the path follows STATIC_URL.

For production set STATIC_ROOT and run python manage.py collectstatic, which copies every static file into one folder that a web server or CDN can serve. Whitenoise is a middleware that lets the Django process itself serve those files efficiently with compression and cache-friendly hashed names, which keeps small deployments simple.

A common failure: styles work locally and vanish in production because collectstatic was not run or the files are not served.

Practice. Add a stylesheet and an image to a project, then run collectstatic and serve the result with Whitenoise.

Media Folder Recommended

Media files are files that users upload, such as avatars and documents, as opposed to static files that you ship with the code. Configure MEDIA_ROOT (a folder on disk) and MEDIA_URL (the public prefix). A model uses ImageField or FileField with an upload_to path, and forms need enctype="multipart/form-data".

In development you can serve the Media Folder by adding static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) to the URL patterns. Never do this in production; use the web server or object storage, such as an S3-compatible service with django-storages.

Treat uploads as untrusted: limit size, check the file type by content and not just the name, store them outside the code folder, and do not serve user-uploaded HTML from your main domain.

Practice. Add an avatar ImageField to a profile model and display the uploaded image in a template.

Stage 4 Models and Migrations

Intermediate2 weeks7 topics

Goal. Design models, configure a database and evolve the schema safely.

Database Setup in Django Core

The DATABASES setting says where data lives. SQLite is the default: one file, zero setup, ideal for learning and tests. For real projects use PostgreSQL, which Django supports best, or MySQL or MariaDB.

  • SQLite in Django needs nothing but the file name.
  • PostgreSQL in Django needs psycopg and gives access to extras such as JSON fields, arrays and full-text search in django.contrib.postgres.
  • MySQL in Django and MariaDB in Django use mysqlclient or a compatible driver; both work with the django.db.backends.mysql engine.
DATABASES = {"default": {
    "ENGINE": "django.db.backends.postgresql",
    "NAME": "shop", "USER": "shop", "HOST": "localhost",
    "PASSWORD": os.environ["DB_PASSWORD"],
}}

Use the same database type in development and production to avoid surprises. SQL Roadmap covers the underlying language.

Practice. Switch a project from SQLite to PostgreSQL in Docker and run the migrations against it.

Models Core

A model is a Python class that describes one database table; each attribute is a column, and each instance is a row. Models live in an app's models.py, which is what Models.py refers to. Django creates the table from the class through migrations, and gives you a query API, validation hooks and admin integration without extra code.

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey("Author", on_delete=models.CASCADE)
    published = models.DateField(null=True, blank=True)

    def __str__(self):
        return self.title

Every model gets an automatic id primary key unless you declare one. Define __str__ so objects read well in the admin and shell. Add a Meta class for ordering, indexes and constraints.

Practice. Write Author and Book models, including __str__ and a Meta ordering.

Model Field Types, Options and Custom Fields Core

Choose the narrowest type that fits: CharField (needs max_length), TextField, IntegerField, DecimalField for money, BooleanField, DateField, DateTimeField, EmailField, URLField, UUIDField, JSONField, FileField and ImageField.

Field Options in Django Models change behaviour. null=True allows a database NULL; blank=True allows an empty form value; they are different, and for text fields you normally want only blank. Others: default, unique, choices, db_index, verbose_name, help_text, validators and editable.

Never store money in a FloatField; use DecimalField.

Custom Fields subclass Field or an existing field and override methods such as from_db_value and get_prep_value to convert between Python and the database. They are rare; often a validator or a property is enough.

Practice. Add a price DecimalField, a status field with choices and a validator that rejects negative prices.

Model Relationships Core

Relationships are expressed with three field types.

  • ForeignKey is many-to-one: many books, one author. It needs on_delete, such as CASCADE, PROTECT or SET_NULL, which decides what happens when the parent goes.
  • ManyToManyField is many-to-many: books and tags. Django creates a hidden join table, or use through= for a custom one that carries extra data such as a date.
  • OneToOneField is one-to-one, often used to extend a user with a profile.

Reverse access is automatic: author.book_set.all(), or a name you choose with related_name="books". Pick related_name deliberately; it makes queries readable and avoids clashes when two keys point to the same model.

on_delete=models.CASCADE can remove far more than you expect. Think about each one.

Practice. Model authors, books, tags and a review table with extra data, and query each relationship in the shell.

Migrations Core

Migrations are version-controlled instructions that bring the database schema in line with your models. makemigrations compares models with the recorded state and writes a file into the app's migrations/ folder; migrate applies unapplied files; showmigrations lists status; sqlmigrate prints the SQL; migrate app 0003 can go back to an earlier state.

Commit migration files to version control. Never edit one that other people have already applied; create a new migration instead. For data changes use RunPython with a forward and reverse function. Adding a non-null column to a big table needs care: add it nullable, backfill, then add the constraint, to avoid long locks.

Squash old migrations occasionally.

Practice. Add a field with a default, write a RunPython data migration that fills it, and reverse both.

Model Methods and managers Recommended

Put behaviour that belongs to a single object in Model Methods, for example is_overdue(), full_name as a property, or a get_absolute_url(). Override save() to add logic that must always run, and call super().save(); remember that bulk operations such as bulk_create and update() skip save() and signals.

For behaviour about a whole table, write a custom manager or queryset: Book.objects.published(). Chain-friendly queryset methods keep views short.

class BookQuerySet(models.QuerySet):
    def published(self):
        return self.filter(published__lte=timezone.now())

class Book(models.Model):
    objects = BookQuerySet.as_manager()

The idea is "fat models, thin views", but keep models focused: heavy workflows spanning several models can live in a services module.

Practice. Add a published() queryset method and a days_since_published property and use both in a view.

Django Shell and Fixtures Recommended

python manage.py shell opens a Python prompt with Django loaded, the quickest way to try queries and model methods. Install IPython and Django uses it for a better prompt. shell_plus from django-extensions imports all models automatically.

Fixtures are serialised data files in JSON or YAML. python manage.py dumpdata books --indent 2 > books.json exports, and loaddata books.json imports. They are useful for initial data, such as countries or categories, and for small test datasets. Place them in an app's fixtures/ folder so that loaddata and tests can find them by name.

For larger or more varied test data, factory libraries such as factory_boy are easier to maintain than static fixtures, because they generate valid objects with sensible defaults.

Practice. Export a model to a fixture, empty the table in the shell and load the fixture back.

Stage 5 The Django ORM in Depth

Intermediate2 weeks6 topics

Goal. Query data efficiently, avoid slow patterns and keep writes consistent.

Django ORM and querying data Core

The Django ORM turns Python calls into SQL. Querying Data with Django ORM starts from a model's manager, Book.objects, which returns lazy QuerySet objects. Nothing runs until you iterate, slice with a step, call len(), list(), bool() or an evaluating method such as count(), first() or exists().

Book.objects.all()
Book.objects.get(pk=1)            # exactly one, else an exception
Book.objects.filter(author__name="Ada").order_by("-published")[:10]
Book.objects.exclude(title__startswith="A").values("id", "title")

Querysets are chainable and cached after evaluation. values() and values_list() return dictionaries or tuples instead of model objects, which is lighter when you need a few columns. Print str(queryset.query) to see the SQL.

Practice. In the shell, build a queryset step by step and print its SQL before and after adding each filter.

Create, Update and Delete Operations in Django ORM Core

Create with Book.objects.create(title="X", author=a) or build an instance and call save(). bulk_create inserts many rows in one query. Update by changing attributes and calling save(), or run Book.objects.filter(...).update(status="old"), which issues one SQL statement but skips save() logic and signals. Use get_or_create and update_or_create for upserts; they handle the race with a unique constraint.

Delete with obj.delete() or queryset.delete(). Deletes follow on_delete rules, so related rows may go too.

For counters use an F expression, Book.objects.filter(pk=1).update(views=F("views") + 1), so that the increment happens in the database and concurrent requests do not lose updates.

Practice. Write a view counter that survives two simultaneous requests by using an F expression.

Filtering and Lookups in Django ORM Core

Filtering and Lookups use double underscores. A field name followed by a lookup chooses the comparison: __exact, __iexact, __contains, __icontains, __in, __gt, __gte, __lt, __lte, __startswith, __range, __isnull, and date parts such as __year. Double underscores also cross relationships: filter(author__country__name="UK").

Combine conditions with Q objects for OR and NOT: filter(Q(title__icontains="x") | Q(author__name__icontains="x")). Compare a column with another column through F("other_field").

Spanning a multi-valued relationship in one filter() call applies all conditions to the same related row, while chaining two filter() calls may match different rows. This subtle point is a classic interview question.

Practice. Implement a search box that matches title or author name, case-insensitively, using Q objects.

Aggregations Recommended

Aggregations compute summaries in the database. aggregate() collapses the whole queryset into one dictionary; annotate() adds a computed value to every row, which is how you do group-by.

from django.db.models import Count, Avg
Book.objects.aggregate(avg_price=Avg("price"))
Author.objects.annotate(n=Count("book")).filter(n__gt=5).order_by("-n")

Available functions include Count, Sum, Avg, Min and Max, plus Case/When for conditional values and Subquery and OuterRef for correlated queries. The order of filter() and annotate() changes the meaning: a filter before annotate limits the rows counted, and a filter after limits the groups returned.

Doing the arithmetic in the database is almost always faster than loading rows to sum them in Python.

Practice. List authors with their number of books and average price, and only those with at least two books.

Query Optimization Core

Most slow Django pages come from too many queries. The classic N+1 problem: a loop touches book.author on each of 100 books and runs 101 queries. Fix it with select_related("author"), which uses a SQL join for foreign keys, and prefetch_related("tags"), which runs one extra query for many-valued relations.

Other habits: select only what you need with only(), defer() or values(); use exists() rather than count() > 0; avoid evaluating a queryset twice; add db_index=True or Meta.indexes for columns used in filters and ordering; paginate lists; and use iterator() for huge sets.

Measure first. Use Django Debug Toolbar, django.db.connection.queries, or assertNumQueries in tests, which fails when a view quietly adds queries.

Practice. Fix a list page that runs one query per row and prove the improvement with assertNumQueries.

Transactions Core

A transaction makes a group of changes succeed or fail as a unit. By default Django runs in autocommit mode: each query commits at once. Wrap related writes in transaction.atomic() so that an exception rolls back all of them.

from django.db import transaction
with transaction.atomic():
    order = Order.objects.create(user=user)
    for item in cart:
        OrderLine.objects.create(order=order, **item)
        Stock.objects.filter(pk=item["stock_id"]).update(qty=F("qty") - item["qty"])

Setting ATOMIC_REQUESTS = True wraps each view in a transaction. Use transaction.on_commit() to send emails or queue tasks only after the data is safely committed. Use select_for_update() inside atomic to lock rows against concurrent changes. Do not catch a database error inside an atomic block and carry on: the transaction is already broken.

Practice. Write an order creation function that rolls back the whole order when stock runs out.

Stage 6 Forms, Admin and Authentication

Intermediate2 weeks7 topics

Goal. Collect and validate user input, manage data in the admin, and add secure login with permissions.

Forms and validation Core

Django forms define fields, render HTML, and check input. A Form class lists fields; ModelForm builds fields from a model. In a view, bind the form to request.POST, call is_valid(), then use cleaned_data.

Django Forms Validation runs in layers: each field type validates itself, extra validators can be attached, a clean_<field>() method checks one field, and clean() checks relationships between fields.

class SignupForm(forms.Form):
    email = forms.EmailField()
    age = forms.IntegerField(min_value=13)
    def clean_email(self):
        email = self.cleaned_data["email"].lower()
        if User.objects.filter(email=email).exists():
            raise forms.ValidationError("Already registered")
        return email

Form Validation errors are available as form.errors and render beside each field. After success, redirect, so that a refresh does not resubmit the form. Every POST form needs the CSRF token tag.

Practice. Build a signup form with field-level and cross-field validation and show errors next to each input.

Model Form Validation Recommended

A ModelForm validates twice: first the form fields, then the model itself through full_clean(), which runs field validators, clean() on the model, and uniqueness checks. Model Form Validation therefore reuses the rules you put on the model, and they also apply in the admin.

Put rules that must always hold on the model, through validators, constraints and clean(). Put rules about one particular screen on the form.

Remember that save() on a model does not call full_clean(). Code that creates objects outside forms, such as imports, must call it explicitly or rely on database constraints. Use Meta.constraints with CheckConstraint and UniqueConstraint for rules the database itself enforces, since they hold even when code bypasses Django.

Practice. Add a CheckConstraint to a model and show the error both in a ModelForm and in the admin.

Django Admin Core

The Django Admin is an automatic back-office built from your models. Enable it by adding django.contrib.admin, running migrate, creating a user with createsuperuser, and visiting /admin/. Register a model in the app's admin.py, which is what Django's admin.py refers to, with admin.site.register(Book) or a decorator.

It is meant for trusted staff, such as editors and support, not as the public interface. Permissions apply per model, with add, change, delete and view rights that you assign to users or groups.

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "published"]
    search_fields = ["title", "author__name"]

A large share of internal tools can be built with the admin alone, which is part of Django's appeal.

Practice. Register three models in the admin and give one group view-only access to one of them.

Admin Customization Recommended

ModelAdmin has many options. list_display, list_filter, search_fields, ordering, date_hierarchy and list_editable shape the list page. fields, fieldsets, readonly_fields, autocomplete_fields and raw_id_fields shape the edit page, and the last two keep forms fast with large related tables. Inlines (TabularInline, StackedInline) edit related rows on the parent page. actions add bulk operations such as "mark as published".

Admin Customization can go further: override templates, add custom views, or computed columns using a method decorated with @admin.display.

Watch performance. A list_display column that follows a relationship causes a query per row unless you set list_select_related. Keep destructive actions behind confirmation and permissions.

Practice. Add an action that marks selected books as published and a computed column showing review counts.

Authentication Core

Authentication answers "who is this?". Django ships with an authentication app, django.contrib.auth, and middleware that attaches request.user. Sessions store the logged-in state in a cookie, and passwords are hashed, using PBKDF2 by default with Argon2 available.

Built-in views and forms cover login, logout, password change and password reset, so wire them with path("accounts/", include("django.contrib.auth.urls")) and supply templates. Protect views with @login_required or LoginRequiredMixin, and set LOGIN_URL and LOGIN_REDIRECT_URL.

Use authenticate() and login() for custom flows. Never write your own password storage. Add rate limiting or lockout on login using a package such as django-axes, and force HTTPS, with secure cookie settings, in production.

Practice. Add login, logout and password reset pages using the built-in views and your own templates.

User model: Built-in and Custom Core

The Built-in User Model has username, password, email, names, and flags such as is_staff and is_active. It is fine for a quick project, but changing it later is painful because every foreign key to users and its migrations depend on it.

So the standard advice is to define a Custom User Model at the very start, even if it is empty: subclass AbstractUser, then set AUTH_USER_MODEL = "accounts.User" before the first migration. Use AbstractBaseUser if you want email as the login field and full control, together with a custom manager.

Always refer to the user through settings.AUTH_USER_MODEL in foreign keys and get_user_model() in code, never by importing django.contrib.auth.models.User. Put extra data in the custom model or a one-to-one profile.

Practice. Create a custom user model with email login in a new project before the first migration.

Authorization: users, groups and permissions Core

Authorization decides what a known user may do. Users & Permissions work like this: every model gets add, change, delete and view permissions; permissions are assigned to users directly or, better, to groups such as "editors"; user.has_perm("books.change_book") checks one. Superusers pass every check.

In views use @permission_required("books.add_book") or PermissionRequiredMixin. In templates the perms variable allows {% if perms.books.add_book %}.

Permissions are model-wide. For object-level rules, such as "only the owner may edit", check in the view or queryset (filter(owner=request.user)), or use a package such as django-guardian. Filtering the queryset is safer than hiding links, because it also blocks guessed URLs. See API Design Roadmap for how the same idea applies to APIs.

Practice. Restrict book editing to the book owner or members of an editors group, and test with two users.

Stage 7 Built-in Features and APIs

Intermediate3-4 weeks8 topics

Goal. Use middleware, CSRF protection, messages, caching and signals as intended, then expose data as a JSON API.

Middleware Core

Middleware is a layer of hooks that wraps every request and response. Settings list it in MIDDLEWARE, and the order matters: the request goes down the list, the response comes back up. Built-in ones handle security headers, sessions, common URL fixes, CSRF, authentication and messages.

Custom Middleware is a callable that takes get_response and returns a function:

class TimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    def __call__(self, request):
        start = time.perf_counter()
        response = self.get_response(request)
        response["X-Time"] = f"{time.perf_counter() - start:.3f}"
        return response

Routing Middleware is a loose name for middleware that inspects the path, host or user to redirect or choose behaviour before the URL resolver runs. Use middleware only for cross-cutting concerns that apply to most requests. Specific logic is better in a view or decorator.

Practice. Write middleware that adds a request id header and logs method, path and duration.

CSRF Core

Cross-site request forgery tricks a logged-in user's browser into sending a state-changing request to your site, because browsers attach cookies automatically. Django defends with a secret token that a page must include in every unsafe request (POST, PUT, PATCH, DELETE). CsrfViewMiddleware checks it.

In templates, put {% csrf_token %} inside each POST form. For JavaScript requests, read the csrftoken cookie and send it in the X-CSRFToken header. Mark a view @csrf_exempt only when another mechanism protects it, such as an API with token authentication and no cookies.

Set CSRF_TRUSTED_ORIGINS when the site is served from several domains or behind a proxy using HTTPS; forgetting this causes confusing 403 errors after deployment. Also use SameSite cookies, which Django sets to Lax by default.

Practice. Submit a form without the token to see the 403, then fix it and send the token from a fetch call.

Caching Recommended

Caching stores the result of expensive work so that the next request can skip it. Configure a backend in CACHES: local memory for development, Redis or Memcached for production, because they are shared across processes. See Redis Roadmap.

Django offers several levels.

  • Per-view: @cache_page(300).
  • Template fragments: {% cache 300 sidebar %}.
  • Low-level: cache.get_or_set("key", compute, 300).
  • Whole site through middleware, which is rarely wise.

The hard part is invalidation. Pick a short timeout, or delete keys when the underlying data changes, and include whatever varies in the key, such as the user or language. Do not cache pages that contain personal data or CSRF tokens without Vary headers. Measure the query count before and after to prove the effect.

Practice. Cache an expensive aggregate for five minutes and clear the cache entry when a book is saved.

Signals Optional

Signals let one part of the code react when something happens elsewhere, without a direct call. Built-in ones include pre_save, post_save, post_delete, m2m_changed and request_finished.

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

Connect receivers in the app config's ready() method so that they load at start-up. Signals are not run by bulk_create or update().

The main drawback is hidden control flow: a developer reading save() cannot see what else runs. Prefer an explicit function call for logic in your own app, and keep signals for decoupling between apps or reacting to third-party models. Never put slow work inside a receiver; hand it to a background task.

Practice. Create a profile automatically when a user is created and verify that bulk_create does not trigger it.

Django REST Framework and Serializers Core

Django REST Framework (DRF) is the standard toolkit for APIs on Django. Serializers convert model instances to JSON and validate incoming data, much as forms do for HTML.

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ["id", "title", "author", "published"]

    def validate_title(self, value):
        if value.isdigit():
            raise serializers.ValidationError("Title cannot be only digits")
        return value

ModelSerializer generates fields from a model; use plain Serializer for shapes that do not match a table. Nested serializers show related objects, but remember to prefetch them or you will create N+1 queries. Expose only the fields clients need, and mark computed or internal ones read_only. See API Design Roadmap for naming and error design.

Practice. Write a ModelSerializer for books with a nested author and a custom validation, and print its output in the shell.

Views, ViewSets and Routers in Django REST Framework Core

DRF offers views at several levels. APIView is like Django's class-based view with request parsing and content negotiation. Generic views such as ListCreateAPIView combine a queryset and a serializer. Views & ViewSets go furthest: a ModelViewSet provides list, retrieve, create, update and delete in one class.

Routers generate the URLs for a viewset, so the Routers in Django REST Framework section is short:

router = DefaultRouter()
router.register("books", BookViewSet)
urlpatterns = router.urls

Override get_queryset() to filter by the current user and perform_create() to set the owner. Add custom actions with @action(detail=True, methods=["post"]). Viewsets are concise for plain CRUD; when behaviour differs per endpoint, explicit generic views or function views can be clearer.

Practice. Expose books through a ModelViewSet and router, and add a custom action that marks a book as read.

API authentication, permissions and pagination Core

DRF configures defaults in the REST_FRAMEWORK setting. Authentication classes include session, basic and token authentication; JWT is available through add-on packages. Permission classes such as IsAuthenticated, IsAdminUser and IsAuthenticatedOrReadOnly allow or deny, and a custom BasePermission with has_object_permission can enforce ownership.

Pagination is mandatory for lists. Pagination classes include PageNumberPagination, LimitOffsetPagination and CursorPagination; cursor style stays fast and stable for large or changing data. Add filtering with the django-filter package and throttling for rate limits.

Set secure defaults globally, such as IsAuthenticated on everything, and loosen them per view. Version the API in the URL or header from the start, and document it with an OpenAPI schema generated by a package such as drf-spectacular.

Practice. Protect your API with token authentication, limit edits to owners and add cursor pagination.

Django Ninja Optional

Django Ninja is a newer API framework for Django inspired by FastAPI. You declare endpoints as functions with Python type hints, and Pydantic schemas validate input and describe output. It generates interactive OpenAPI documentation automatically and supports async views.

from ninja import NinjaAPI, Schema
api = NinjaAPI()

class BookOut(Schema):
    id: int
    title: str

@api.get("/books/{book_id}", response=BookOut)
def get_book(request, book_id: int):
    return Book.objects.get(pk=book_id)

It has less built-in machinery than DRF, so fewer ready-made pieces for permissions and filtering, but code is short and fast to read. Choose DRF for a large ecosystem and existing team knowledge; choose Ninja for small, typed, new APIs.

Practice. Rebuild one DRF endpoint in Django Ninja and compare the code and generated docs.

Stage 8 Testing, Debugging and Logging

Intermediate1-2 weeks4 topics

Goal. Test your code automatically, find problems quickly and record what a running site does.

Django Test Framework Core

Django builds testing on Python's unittest. Each app has a tests.py module or a tests/ package. A TestCase class wraps each test in a transaction that is rolled back, so tests stay isolated and fast, and python manage.py test creates a throwaway test database and runs them.

unittest & TestCase give you assertions such as assertEqual, assertRaises and the Django extras assertContains, assertRedirects, assertTemplateUsed and assertNumQueries. The test Client makes requests without a server.

class BookViewTests(TestCase):
    def test_list_shows_title(self):
        Book.objects.create(title="Dune")
        resp = self.client.get(reverse("book-list"))
        self.assertContains(resp, "Dune")

Test behaviour, such as status codes and stored data, not template markup details. Use setUpTestData for shared objects, and override_settings for configuration changes.

Practice. Write tests for the list, detail and create views of one app, including a login-required redirect.

pytest Recommended

pytest is a popular alternative runner, installed with pytest-django. Tests are plain functions with plain assert statements and informative failure output, plus fixtures for setup, and parametrisation to run one test over many inputs.

import pytest
@pytest.mark.django_db
@pytest.mark.parametrize("title,ok", [("Dune", True), ("", False)])
def test_title_validation(title, ok):
    form = BookForm({"title": title})
    assert form.is_valid() == ok

The django_db mark grants database access, and the client and admin_client fixtures replace the test client. Django's own runner is fine and needs no dependencies; pick pytest if you value less boilerplate and a richer plugin ecosystem. Run tests in CI on every push.

Practice. Convert two test cases to pytest with a parametrised test and a factory for books.

Debugging Django Core

Begin with the error page in development, which shows the traceback and local variables. For deeper inspection use PDB and IPDB: insert breakpoint() where you need to stop, then step with n, enter functions with s, print with p, and continue with c. IPDB adds tab completion and colours.

Django Debug Toolbar overlays a panel in development showing SQL queries with timings, duplicated queries, templates rendered, cache calls and request data; it is the fastest way to spot an N+1. django-silk is a profiler that records requests and queries into a database and shows slow ones, which suits staging and finds problems across many requests.

Never run either in production with DEBUG on. Read the full traceback from the bottom, as the last frame is usually the cause.

Practice. Install the debug toolbar, find a duplicated query on one page and fix it with select_related.

Logging Core

Django uses Python's logging module and configures it through the LOGGING setting. Four parts fit together: Loggers are named channels that your code writes to, logger = logging.getLogger(__name__); Handlers decide where records go, such as the console, a file or an email; Formatters decide how a line looks; Logging Filters decide which records pass.

LOGGING = {
  "version": 1, "disable_existing_loggers": False,
  "handlers": {"console": {"class": "logging.StreamHandler"}},
  "root": {"handlers": ["console"], "level": "INFO"},
}

In containers log to the console and let the platform collect output. Use levels properly, add context such as user and request ids, never log passwords or tokens, and send errors to a tracking service such as Sentry. The built-in django.request logger records server errors.

Practice. Configure logging so that app messages at INFO go to the console in JSON-like lines and errors also go to a file.

Stage 9 Asynchronous Work and Deployment

Advanced2 weeks3 topics

Goal. Move slow work out of requests, use async features where they help, and ship a Django site safely.

Asynchronous Django Recommended

Django supports async def views, middleware and parts of the ORM (await Book.objects.aget(...), async for over querysets) when served through ASGI, for example with Uvicorn or Daphne. It also keeps full WSGI support.

Async helps when a view waits on many slow network calls, such as calling several APIs, or holds open connections such as WebSockets through Channels. It does not make database-heavy pages faster, because the database drivers and much of the ORM still use threads underneath. Calling blocking code from async code stalls the loop; wrap it with sync_to_async, and the reverse with async_to_sync.

Do not convert a project to async without a measured reason. Most sites perform well with synchronous views and several worker processes.

Practice. Write an async view that fetches two URLs concurrently with asyncio.gather and compare it with a sync version.

Background Tasks and Celery Recommended

Anything slow or retry-prone should leave the request: sending emails, resizing images, calling payment APIs, generating reports. Background Tasks run in separate worker processes, fed by a queue. Celery is the most used system. You define a task, @shared_task def send_welcome(user_id), call it with send_welcome.delay(user.id), and workers pick it up from a broker such as Redis or RabbitMQ. Celery beat schedules periodic jobs.

Pass ids, not objects, so the task loads fresh data. Make tasks idempotent, because they can run twice. Set retries with backoff and time limits. Start tasks from transaction.on_commit, so that a worker never looks for a row that has not been committed yet.

Newer Django versions include a tasks framework interface, and lighter options such as Django-Q2 and Huey exist for small sites.

Practice. Send a welcome email from a Celery task with retries, triggered after the user row is committed.

Production Checklist and Deployment Core

Before going live, run python manage.py check --deploy and fix its warnings. The Production Checklist: DEBUG = False, a strong secret key from the environment, correct ALLOWED_HOSTS, HTTPS with SECURE_SSL_REDIRECT, secure and HTTP-only cookies, HSTS, a real database, collectstatic run, error reporting, backups, and migrations applied.

Deployment: run Django with Gunicorn (WSGI) or Uvicorn (ASGI) behind a reverse proxy such as Nginx or a platform router that terminates TLS. Put it in a container with a non-root user, run several worker processes, serve static files through Whitenoise or a CDN, and keep uploads in object storage. Run migrations as a separate release step. Add a health endpoint and structured logs. See Docker Roadmap and Linux Roadmap for the surrounding tools.

Practice. Deploy a small Django app in a container behind Nginx or a platform of your choice and run check --deploy against it.

Projects to build

Build these in order. Each one should end up in a public repository with a short README that explains your decisions.

  1. Personal blogBeginner

    Build a blog with posts, categories and comments. Use class-based views, template inheritance and the admin for writing posts. Add pagination and a simple search.

    Models · Templates · Generic views · Admin

  2. Bookmark manager with accountsBeginner

    Let users register, log in and save bookmarks with tags. Each user sees only their own data. Add forms with validation and a message after each action.

    Authentication · Forms · Relationships · Permissions

  3. Online shop back endIntermediate

    Create products, a cart and orders with stock control. Use transactions so that an order and the stock change succeed together, and optimise the product list with select_related.

    ORM · Transactions · Query optimisation · Custom user model

  4. REST API for a task trackerIntermediate

    Expose projects and tasks through Django REST Framework with token authentication, owner-only permissions, filtering and pagination. Generate OpenAPI documentation and cover it with tests.

    DRF · Serializers · Permissions · Testing

  5. Report generator with background jobsAdvanced

    Let users request a large CSV export. Queue the work with Celery and Redis, report progress, send an email when it is done and store the file in object storage.

    Celery · Redis · Media files · Email

  6. Deployed multi-tenant appAdvanced

    Deploy a Django app in containers with PostgreSQL, Gunicorn, Nginx and HTTPS. Add structured logging, a health check, and a pipeline that runs tests and migrations on each release.

    Deployment · Docker · Logging · Continuous integration

Interview questions

Try to answer each question out loud before you open the answer.

Explain how Django handles a request from start to finish.Junior

The web server passes the request to Django through WSGI or ASGI, which builds an HttpRequest. It travels down the middleware stack, then the URL resolver matches a pattern and calls the view with the request and captured arguments. The view uses models and templates and returns an HttpResponse. The response passes back up through the middleware in reverse order and is sent to the client. Exceptions are turned into error responses along the way.

What is the difference between a project and an app?Junior

A project is the complete site: settings, the root URL configuration and the entry points. An app is a focused package inside it that provides one feature, with its own models, views, templates and tests. A project contains many apps and an app can be reused across projects. Apps become active when listed in INSTALLED_APPS.

What is the difference between null=True and blank=True?Junior

null=True is a database setting: the column may store NULL. blank=True is validation: forms and the admin may accept an empty value. They are independent. For string fields I normally use blank=True without null, so that "no value" has one representation, an empty string. For dates, numbers and foreign keys I use both, because an empty form value must be stored as NULL.

How do migrations work and what do you do about conflicts?Mid

makemigrations compares the models to the state recorded by earlier migration files and writes a new file with the operations needed. migrate applies unapplied files in dependency order and records them in a table. Files are committed with the code. When two branches add migrations to the same app, the graph has two leaves; I run makemigrations --merge or write a merge migration. I never edit a migration that others have applied.

What is the N+1 query problem and how do you fix it in Django?Mid

It happens when code runs one query to fetch a list and then one more per item to fetch a related object, for example reading book.author inside a loop. With 100 books that is 101 queries. select_related uses a SQL join and suits foreign keys and one-to-one. prefetch_related runs one extra query and joins in Python, which suits many-to-many and reverse relations. I find them with the debug toolbar and guard against regressions with assertNumQueries.

Explain QuerySet laziness and caching.Mid

A queryset describes a query but does not run it. Chaining filter, exclude or order_by builds a new queryset without touching the database. The SQL runs when you iterate, slice with a step, call list, len or bool, or use methods such as get, count and exists. After evaluation the results are cached on that object, so iterating again does not re-query. Creating a new queryset each time, for example by calling .all() in a loop, hits the database again.

What is the difference between function-based and class-based views, and when do you use each?Mid

A function-based view is a simple function from request to response, with explicit flow that is easy to read. A class-based view organises behaviour into methods and reuses code through inheritance and mixins. Generic views make standard CRUD pages short. I use generic class-based views for plain list, detail and form pages, and functions when the logic is unusual, because overriding many hooks can be harder to follow than a few explicit lines.

How does Django protect against common web attacks?Mid

Templates escape output by default, preventing most cross-site scripting. The ORM uses parameterised queries, preventing SQL injection. CsrfViewMiddleware requires a token on unsafe requests. Clickjacking is handled by the X-Frame-Options middleware, and settings enable HSTS, secure cookies and content type protection. Passwords are hashed with a slow algorithm. These work only if I avoid bypasses such as the safe filter, raw SQL with string formatting and csrf_exempt without a reason.

Why should you define a custom user model at the start of a project?Mid

The built-in User model is referenced by foreign keys and migrations across the project. Changing AUTH_USER_MODEL after the first migration is hard, because tables and constraints must be rewritten. Starting with a subclass of AbstractUser, even an empty one, costs nothing and leaves room to add fields or switch to email login later. In code I refer to it with get_user_model or settings.AUTH_USER_MODEL.

How do you handle transactions in Django?Senior

Django runs in autocommit mode by default. I wrap multi-step writes in transaction.atomic so that any exception rolls everything back, and I can nest it with savepoints. ATOMIC_REQUESTS wraps entire views. I use select_for_update to lock rows against concurrent changes and transaction.on_commit to trigger emails or tasks only after the commit. I do not swallow database errors inside an atomic block, because the transaction is then unusable.

How do middleware and signals differ, and when would you avoid signals?Senior

Middleware wraps the request and response cycle and affects every request that passes through it, for cross-cutting concerns such as sessions, security headers and timing. Signals notify receivers when an event such as a model save happens. I avoid signals for logic in my own app, because control flow becomes hidden and bulk operations skip them. An explicit service function is clearer, and slow work should go to a background task.

Serializer or form: how do Django REST Framework serializers compare with Django forms?Mid

Both declare fields, validate input and produce cleaned data. Forms deal with HTML form data and rendering, while serializers deal with JSON and other formats, both for input and for output. ModelSerializer and ModelForm both derive fields from a model. Serializers add nested representations and work with viewsets and renderers. I use forms for server-rendered pages and serializers for APIs, and keep shared rules on the model.

How would you design permissions so that users edit only their own records?Mid

Authentication tells me who the user is, and model permissions say which actions are allowed on a type, but ownership is an object-level rule. I filter the queryset by the owner in get_queryset, so other users' records return 404, and I set the owner in perform_create. In DRF I can add a custom permission with has_object_permission. I test it with two users, since hiding a link is not security.

How would you speed up a slow Django page?Senior

I measure first with the debug toolbar or silk to see query count, duplicate queries and time per query. Typical fixes are select_related and prefetch_related, selecting fewer columns, adding indexes for filtered and ordered columns, paginating, and doing aggregation in the database. Then caching: fragments or expensive results with sensible invalidation. Slow external calls move to background tasks. I re-measure to confirm each change helped.

How do you run background work reliably with Celery?Senior

I call tasks with ids and let the worker load fresh data. Tasks are idempotent, because retries and duplicate delivery happen, and have retry with exponential backoff and time limits. I start them from transaction.on_commit so the row exists. The broker, usually Redis or RabbitMQ, must be monitored, and queue length, failures and task age are metrics I alert on. Periodic jobs use Celery beat with a single scheduler.

What do you check before deploying a Django site?Senior

DEBUG is off and the secret key comes from the environment. ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS match the real domains, HTTPS is enforced with secure cookies and HSTS, and manage.py check --deploy is clean. Static files are collected and served by Whitenoise or a CDN, uploads are in object storage, the database is backed up and migrations run as a release step. Logging and error reporting work, and I run the app with Gunicorn behind a reverse proxy.

Common mistakes

  • Starting with the built-in user model and trying to swap it later; define a custom user model before the first migration.
  • Putting all logic in views or templates; move business rules to models, querysets or service functions.
  • Looping over objects and touching related fields, creating hundreds of queries; use select_related and prefetch_related and check with the debug toolbar.
  • Committing secrets and running with DEBUG on in production; read secrets from the environment and run check --deploy.
  • Editing migration files that others have already applied; add a new migration instead.
  • Using null=True on text fields, so there are two kinds of empty; use blank=True and an empty string.
  • Updating counters with read, add one and save, which loses concurrent updates; use an F expression.
  • Hiding a link and calling that authorisation; filter querysets and check permissions on the server for every request.
  • Doing slow work such as sending email inside the request; queue it as a background task.
  • Serving the development server in production; use Gunicorn or Uvicorn behind a reverse proxy.

Ready-for-interview checklist

  • I can create a project and explain what each file generated by startproject and startapp is for.
  • I can trace a request from the URL pattern through the view to the template.
  • I can design models with ForeignKey, ManyToMany and OneToOne relationships and choose on_delete deliberately.
  • I can write, apply, reverse and squash migrations, including a data migration.
  • I can write complex filters with Q and F, annotate and aggregate, and read the SQL a queryset produces.
  • I can find and fix an N+1 problem and prove it with assertNumQueries.
  • I can build a form with field-level and cross-field validation and explain the CSRF token.
  • I can set up a custom user model, login, groups and object-level permissions.
  • I can build a CRUD API with serializers, a viewset, authentication and pagination.
  • I can write unit tests with the test client, and I understand the difference between TestCase and pytest.
  • I can set up logging and use the debug toolbar and a debugger.
  • I can explain how to run Celery tasks safely, and when async views are and are not useful.
  • I can list what must be configured before a Django site goes live.

Related roadmaps

  • Python RoadmapSyntax to packaging: data model, typing, async, testing.
  • SQL RoadmapQueries, joins, window functions, indexes and transactions.
  • API Design RoadmapREST, gRPC, GraphQL, versioning, auth, pagination and rate limits.
  • Docker RoadmapImages, containers, networking, volumes, Compose and security.
  • Redis RoadmapData types, caching patterns, persistence, clustering and streams.
  • System Design RoadmapScalability, caching, queues, consistency and the classic designs.

FAQ

How long does it take to learn Django?

If you already know Python, you can build a small site in two to three weeks. Reaching the level asked for in junior and mid-level interviews, including the ORM, authentication, a REST API, testing and deployment, usually takes twelve to sixteen weeks at about eight hours a week. Building projects matters more than reading.

Should I learn Django or Flask first?

Django is the better first choice if you want a complete, conventional toolkit and the most job listings. Flask is smaller and teaches how the parts fit together, but you assemble authentication, database access and forms yourself. Many developers learn Django first and Flask or FastAPI later.

Is Django still worth learning?

Yes. It is actively maintained with regular releases and long-term support versions, and it is used in production by many companies. For content-heavy sites, internal tools and APIs that need to ship quickly, its built-in features save a great deal of work. FastAPI is popular for pure APIs, but Django remains strong for full applications.

Do I need to learn Django REST Framework?

If you build APIs, yes: it is the most common way to do it with Django, and it appears in many job descriptions. Learn the core Django parts first, namely models, views, forms and authentication, because DRF builds directly on them. Django Ninja is a lighter alternative worth knowing about.

Can Django handle large sites?

Yes, when designed well. Scaling usually comes from database design and indexes, caching, background tasks, several worker processes and horizontal scaling behind a load balancer, not from the framework itself. Several very large sites have been built on Django.

Practise while you learn.Solve interview problems in the browser, with hints and a timer.
CodingSQLSystem design
Next Post Previous Post
No Comment
Add Comment
comment url