Skip to content

Top 30 Django Interview Questions and Answers [2026]

October 8, 2026 13 min read
Django interview guide by Second Talent
TL;DR: Django interviews in 2026 test the ORM (lazy QuerySets, N+1 queries, transactions), migrations on live databases, security defaults and what async support really covers. Candidates should know Django 6.1 (August 2026) and 6.0 (December 2025), including the built-in Tasks framework, Content Security Policy support and the move to calendar version numbers from 2028.

Django 6.1, released on August 5, 2026, lets a QuerySet fetch a missing foreign key for every row it loaded at once. That turns the classic 51-query loop into two queries with one method call.

It is also one of the last releases with a dotted version number. After 6.2 in April 2027, Django switches to calendar versions starting with Django 2028.

The questions below run from fundamentals to those changes, and every code sample was run on Django 6.1.1.

Key takeaways
  1. 1Django 6.x supports Python 3.12, 3.13 and 3.14; Django 5.2 LTS is the last series for Python 3.10 and 3.11.
  2. 2Django 6.1 dropped PostgreSQL 14 and MySQL below 8.4; it needs PostgreSQL 15 or MySQL 8.4 and later.
  3. 3The default PBKDF2 password hasher runs 1,500,000 iterations in Django 6.1, up from 1,200,000 in 6.0.
  4. 4Django 5.2 LTS gets security fixes until April 2028, longer than 6.0 (April 2027) or 6.1 (December 2027).

Core Framework

1. What is Django's MTV pattern, and how does it map to MVC?

MTV stands for Model, Template, View. The model defines data and business rules, the template renders output, and the view receives a request and returns a response.

Django's "view" is closer to an MVC controller, and the template is the MVC view.

The URL dispatcher and middleware do the routing that some MVC frameworks put in the controller. The naming question matters less than knowing where code belongs.

Queries and domain rules in models and managers, request handling in views, presentation in templates or serializers.

2. In what order does middleware run on the request and the response?

Top to bottom on the way in, bottom to top on the way out. The middleware docs describe it as an onion: each class in MIDDLEWARE wraps the next, with the view at the center.

Five steps of a Django request: the WSGI or ASGI handler, middleware from top to bottom, the URL resolver, the view, then middleware from bottom to top on the response.

Order therefore matters. SessionMiddleware must come before AuthenticationMiddleware, which reads the session to set request.user. Any middleware can return a response early without calling the next layer.

That is how CSRF rejections and redirects to login work, and the layers above it still process that response on the way out.

import time

class RequestTimer:
    def __init__(self, get_response):
        self.get_response = get_response      # runs once, at startup

    def __call__(self, request):
        start = time.perf_counter()           # before the view
        response = self.get_response(request)
        response["Server-Timing"] = f"app;dur={(time.perf_counter() - start) * 1000:.0f}"
        return response                       # after the view

3. What is the difference between a project and an app, and what is AppConfig.ready() for?

A project is the whole site: settings, root URLconf and the WSGI or ASGI entry point. An app is a Python package with models, views and migrations that the project lists in INSTALLED_APPS.

Apps should be split by business area, such as billing or catalog, not by technical layer.

AppConfig.ready() runs once when the app registry is fully loaded. It is the place to connect signal receivers and run startup checks.

It must not query the database, because it also runs for management commands such as migrate, before tables exist.

4. Function-based or class-based views?

Both are fine; the choice is about reuse. A function-based view is explicit and easy to read top to bottom. Class-based views pay off when you reuse behavior.

Generic views such as ListView and UpdateView cover common cases in a few lines, and mixins such as LoginRequiredMixin add behavior across many views.

The cost of class-based views is indirection: a bug may live in a method four classes up the inheritance chain.

A good answer mentions reading the method resolution order, or a reference such as Classy Class-Based Views, when overriding get_queryset() or form_valid().

5. Which settings must change before a Django site goes to production?

At minimum: DEBUG = False, a real ALLOWED_HOSTS, a SECRET_KEY read from the environment or a secret store, plus HTTPS settings. Those include SECURE_HSTS_SECONDS, SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE.

Static files need collectstatic and a server or CDN in front.

The strong answer names the tool: python manage.py check --deploy runs the checks from the deployment checklist and warns about each missing setting. Running it in CI stops a debug setting from reaching production.

6. What are signals, and when should you avoid them?

Signals let decoupled code react to events such as post_save or user_logged_in. They fit when a third-party or reusable app needs to react to your models without you editing that app.

Inside your own project, prefer an explicit function call. The Django docs say it directly: "Signals are implicit function calls which make debugging harder."

They also skip cases people assume they cover: QuerySet.update(), bulk_create(), raw SQL, and the new database-level on_delete options in 6.1.

ORM and Queries

7. Why are QuerySets lazy, and when do they hit the database?

Building a QuerySet with filter(), exclude() or order_by() runs no SQL; it builds a query. SQL runs when the QuerySet is evaluated.

That means iterating, slicing with a step, len(), list(), bool(), or calling get(), count(), exists() or first().

Laziness lets you compose queries across functions.

It also causes two common bugs: evaluating the same QuerySet twice (two queries), and calling len(qs) just to count, which loads every row. qs.count() and qs.exists() push the work to the database.

Once evaluated, a QuerySet caches its results, so reusing the same variable does not query again.

8. How do you find and fix N+1 queries in Django?

Load related objects in bulk. select_related() follows foreign keys and one-to-one fields with a SQL join. prefetch_related() runs one extra query per relation and joins the results in Python, which also works for many-to-many and reverse foreign keys.

Django 6.1 adds a third option, fetch_mode(models.FETCH_PEERS). It fetches a missing field for every instance from the same QuerySet the first time one of them needs it.

Column chart of queries needed to list 50 books with their authors in Django 6.1.1: 51 with a plain loop, 1 with select_related, 2 with prefetch_related and 2 with the FETCH_PEERS fetch mode.
select_related
  • One query with a SQL JOIN
  • ForeignKey and OneToOne only
  • Wide rows if the join fans out
prefetch_related
  • One extra query per relation
  • Also many-to-many and reverse FKs
  • Joined in Python; accepts a Prefetch() queryset

To find N+1 queries before users do, assert query counts in tests with assertNumQueries, use django-debug-toolbar locally. Also look for ladders of identical queries in traces.

FETCH_RAISE turns any unplanned lazy load into a FieldFetchBlocked exception, which makes a good guard in performance-critical code and tests.

books = Book.objects.fetch_mode(models.FETCH_RAISE)
for book in books:
    book.author.name      # raises FieldFetchBlocked: add select_related("author")

9. What problem do F() expressions solve?

Race conditions in read-modify-write code. This loses updates when two requests run at once, because both read the same value:

book = Book.objects.get(pk=pk)
book.views += 1
book.save()

F("views") refers to the column in the database, so the increment happens in SQL: Book.objects.filter(pk=pk).update(views=F("views") + 1). It is atomic and needs no read.

F expressions also compare columns in filters, such as filter(stock__lt=F("reorder_level")). After an update through F, refresh the instance with refresh_from_db() before reading the field.

10. What is the difference between annotate() and aggregate()?

aggregate() returns one dictionary for the whole QuerySet: Book.objects.aggregate(total=Sum("stock")) gives {"total": 1225}. annotate() adds a computed value to each row: Author.objects.annotate(n=Count("books")) gives every author a n attribute, which can then be filtered or ordered.

The classic trap is combining two annotations across different joins, such as counting both books and reviews per author. The joins multiply rows and inflate both counts. Use Count(..., distinct=True) or subqueries for each count.

11. How do you use transactions correctly in Django?

Wrap multi-step writes in transaction.atomic(). Lock rows you will change with select_for_update(). Schedule side effects with transaction.on_commit() so they run only if the transaction commits.

with transaction.atomic():
    book = Book.objects.select_for_update().get(pk=pk)
    Book.objects.filter(pk=book.pk, stock__gt=0).update(stock=F("stock") - 1)
    transaction.on_commit(lambda: send_receipt.enqueue(order_id=order.id))

Without on_commit, a background job can start before the transaction commits and read data that does not exist yet. It can also send an email for an order that was rolled back.

ATOMIC_REQUESTS = True wraps every view in a transaction, which is simple but holds locks for the whole request; many teams prefer explicit blocks.

12. When do you use only(), defer() and values()?

To load less data. only("id", "title") and defer("body") still return model instances but skip columns, useful when a table has a large text or JSON field. values() and values_list() return dictionaries or tuples, skipping model instantiation, which is faster for exports and reports.

The catch: accessing a deferred field later triggers one query per instance, the same N+1 pattern as a foreign key.

The Django 6.1 fetch modes cover deferred fields as well as foreign keys, so FETCH_PEERS or FETCH_RAISE apply to deferred fields too.

13. How do you write reusable query logic?

Put it on a custom QuerySet and expose it as the manager, so the methods chain:

class BookQuerySet(models.QuerySet):
    def in_stock(self):
        return self.filter(stock__gt=0)

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

Book.objects.in_stock().filter(author__name="Ann")

This keeps query rules in one place instead of repeating filter(stock__gt=0) across views. Methods on a plain Manager do not chain after filter(), which is why as_manager() or Manager.from_queryset() is the better pattern.

14. When is raw SQL appropriate, and how do you keep it safe?

When the ORM cannot express the query cleanly or efficiently: window functions it does not cover, recursive CTEs, database-specific features. Use Model.objects.raw() to get model instances, or connection.cursor() for anything else.

Always pass values as parameters, never through string formatting: cursor.execute("SELECT ... WHERE id = %s", [pk]). The same applies to RawSQL() expressions and to extra(), which the docs call "an old API that we aim to deprecate".

Table and column names cannot be parameters, so map user input to an allowlist.

Models and Migrations

15. How do migrations work, and what is a data migration?

makemigrations compares your models with the state built from existing migration files and writes new operations. migrate applies unapplied migrations and records them in the django_migrations table.

A data migration changes rows rather than schema, using RunPython. It must use the historical model from apps.get_model(), not an import, because the real model may have changed since.

Give it a reverse function (or RunPython.noop) so the migration can be rolled back, and process large tables in batches.

16. How do you add a required column to a large table without downtime?

In steps that keep old and new code working. Add the column as nullable or with a database default. Deploy code that writes it. Backfill existing rows in batches. Then make it non-null in a later migration.

Since Django 5.0, db_default sets a default in the database itself, so rows inserted by old code or by other systems get the value. On PostgreSQL, adding a column with a constant default does not rewrite the table.

Candidates should also mention sqlmigrate to read the SQL before running it, and separating index creation into its own migration, using AddIndexConcurrently on PostgreSQL.

17. Two branches both added a migration to the same app. How do you fix it?

migrate refuses to run when an app has two leaf migrations. makemigrations --merge creates a merge migration that depends on both; if the two changes touch different fields, that is all it takes.

If they conflict, one branch must rewrite its migration.

Over time, long migration histories slow down test setup. squashmigrations combines a range into one file; after every environment has applied the squashed migration, the old files can be deleted.

18. Why set a custom user model at the start of a project?

Because changing AUTH_USER_MODEL after tables exist is a painful migration. The Django docs recommend subclassing AbstractUser for every new project. Point AUTH_USER_MODEL at it before the first migrate, even if the class body is only pass.

Code should then refer to the user model through get_user_model() or settings.AUTH_USER_MODEL, never by importing django.contrib.auth.models.User. Profile data that other apps own can still live in a separate one-to-one model.

Security

19. How does Django's CSRF protection work?

CsrfViewMiddleware checks every unsafe request (POST, PUT, PATCH, DELETE). The browser holds a secret in the csrftoken cookie. Forms include a masked token through {% csrf_token %}, or JavaScript sends it in the X-CSRFToken header.

Another site can make the browser send the cookie but cannot read it to supply the matching token.

Per the CSRF reference, the middleware also checks the Origin header against the host and CSRF_TRUSTED_ORIGINS. For HTTPS requests without an Origin header it checks the referer.

APIs that authenticate with tokens in a header rather than cookies do not need CSRF protection; DRF's SessionAuthentication enforces it because it uses cookies.

20. How does Django prevent XSS and SQL injection, and how do developers break it?

Templates escape variables by default, and the ORM parameterizes every query. Developers break both through the escape hatches. For XSS that means mark_safe(), the |safe filter and {% autoescape off %} on user content.

For SQL it means string-built SQL in raw(), extra() or RawSQL.

Other gaps templates cannot close: user input placed in a JavaScript context or an unquoted HTML attribute, and href values that accept javascript: URLs. The json_script filter is the safe way to pass data to JavaScript.

Django 6.0's CSP support (question 29) limits the damage if something slips through.

21. How does Django store passwords?

As a salted hash, by default PBKDF2 with SHA-256 at 1,500,000 iterations in Django 6.1. The stored string records the algorithm, iterations and salt.

Django can therefore verify old hashes and upgrade them to the current settings on the next successful login.

Argon2 is supported with the argon2-cffi package: put Argon2PasswordHasher first in PASSWORD_HASHERS and keep the others listed so existing hashes still verify.

A good answer also mentions AUTH_PASSWORD_VALIDATORS for minimum length and common-password checks, and never implementing a custom hasher without a strong reason.

Performance and Async

22. What caching does Django provide?

Four levels, all on the CACHES backends (Redis, Memcached, database, local memory):

  • Per-site through cache middleware, rarely right for logged-in users.
  • Per-view with @cache_page(60 * 15).
  • Template fragments with {% cache 500 sidebar request.user.id %}.
  • Low-level with cache.get_or_set(key, callable, timeout) for computed values.

The hard part is invalidation. Include every variable the output depends on in the key (user, language, filters) and delete or version keys when the underlying data changes.

23. What does Django's async support give you, and what are its limits?

Async views, async middleware, and a-prefixed ORM methods such as aget(), acreate() and async for over QuerySets.

Under ASGI, an async view can call several external APIs concurrently or hold long-lived connections without a thread per request.

The limits, from the async docs: "Transactions do not yet work in async mode". Transactional code therefore runs as a sync function wrapped in sync_to_async().

Persistent connections (CONN_MAX_AGE) should be disabled in async mode in favor of a connection pool. And mixing modes costs a context switch per call, so a loop that crosses between sync and async on every row should cross once instead.

24. How do you run background work in Django 6?

Define it as a task and hand it to a worker. Django 6.0 added a built-in Tasks framework: decorate a function with @task and call .enqueue().

Django handles defining, validating and queuing tasks, but it "does not provide a worker mechanism". The two built-in backends are meant for development and testing.

from django.tasks import task

@task
def email_users(emails, subject, message):
    return send_mail(subject, message, None, emails)

email_users.enqueue(emails=["user@example.com"], subject="Hi", message="Hello")

In production you still need a backend and workers from a third-party package, or Celery. The value of the framework is a standard API: application code enqueues the same way whichever backend runs it.

Enqueue from transaction.on_commit() when the task reads data the request just wrote.

25. How do you keep a Django test suite fast and reliable?

Use TestCase, which wraps each test in a transaction and rolls it back, instead of TransactionTestCase, which truncates tables. Create shared data once in setUpTestData().

Use RequestFactory to test a view function without the full middleware stack, and the test Client when you want the whole request path.

Add assertNumQueries on list endpoints to catch N+1 regressions. Run tests in parallel with --parallel, and build test data with factories instead of fixtures that drift from the models.

Code that uses on_commit needs captureOnCommitCallbacks() inside TestCase, because the test transaction never commits.

26. How do DRF serializers validate data, and what goes wrong with nested writes?

A ModelSerializer builds fields and validators from the model. Validation runs field by field (validate_<field>), then across fields in validate(), and serializer.save() calls create() or update() with validated_data.

The current Django REST framework release is 3.18.1, from September 2026.

Nested serializers are read-only for writes by default. DRF raises an error unless you write create() and update() yourself, and those methods decide how to match, create or delete child rows. Candidates should also mention performance.

A nested serializer on a list endpoint is an N+1 query unless the view's queryset uses select_related or prefetch_related.

What Changed Recently

Dec 3, 2025
Django 6.0: Tasks framework, CSP, template partials, BigAutoField default
Aug 5, 2026
Django 6.1: fetch modes, database-level on_delete, MAILERS
Apr 2027 (planned)
Django 6.2 LTS, the last A.B release
Jan 2028 (planned)
Django 2028, first calendar-versioned release

27. What is changing about Django's version numbers and support?

From 2028, Django releases once a year, every January, with the year as the version. Per DEP 20 and the Django download page, the releases once planned as 7.0 and 7.1 become Django 2028 and Django 2029.

Every feature release will then get the same three-year support period, instead of only designated LTS releases.

Django 5.2 LTSLatest 5.2.17
Apr 2028
End of security fixes
Django 6.0Latest 6.0.8
Apr 2027
End of security fixes
Django 6.1Latest 6.1.1
Dec 2027
End of security fixes
Django 6.2 LTSPlanned Apr 2027
Apr 2030
Planned end of security fixes
Source: djangoproject.com/download, read September 28, 2026.

Django 6.2 in April 2027 is the last release under the old scheme and the last LTS, supported until April 2030. For planning: 5.2 LTS gets fixes until April 2028, so a team on 5.2 can move to 6.2 LTS or wait for 2028.

Deprecation warnings follow the new names already: RemovedInDjango70Warning became RemovedInDjango2028Warning in 6.1.

28. What are the database-level on_delete options in Django 6.1?

DB_CASCADE, DB_SET_NULL and DB_SET_DEFAULT put the delete rule into the SQL ON DELETE clause, so the database does the work.

The existing CASCADE option runs in Python: Django loads the related objects, sends signals and deletes them itself.

The database version is faster for large relations, because nothing is loaded. The trade-off, stated in the 6.1 release notes, is that DB_CASCADE "does not trigger the pre_delete or post_delete signals".

In a test on Django 6.1.1, deleting an author removed their DB_CASCADE posts with no pre_delete signal for the posts. Meanwhile a Python-level CASCADE relation on the same author still sent one.

Code that relies on those signals for cleanup, such as deleting files, must move that logic before switching.

29. How does the built-in Content Security Policy support work?

Django 6.0 added ContentSecurityPolicyMiddleware, configured with Python dictionaries in SECURE_CSP (enforced) and SECURE_CSP_REPORT_ONLY (report only). A context processor exposes a per-request nonce for inline scripts.

From the 6.0 release notes:

from django.utils.csp import CSP

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "img-src": [CSP.SELF, "https:"],
}

A sensible rollout starts with SECURE_CSP_REPORT_ONLY, collects violation reports for a few weeks, then enforces. Before 6.0, most projects used the third-party django-csp package; the built-in version removes that dependency.

30. What are template partials, and why were they added?

Named fragments inside a template, defined with {% partialdef %} and rendered with {% partial %}. They can also be loaded on their own with the template_name#partial_name syntax.

That makes them useful with htmx-style front ends, because the full page and the fragment a button click swaps in come from the same file.

{% partialdef row %}<li>{{ order.id }}: {{ order.status }}</li>{% endpartialdef %}
<ul>{% for order in orders %}{% partial row %}{% endfor %}</ul>

get_template("orders.html#row").render({"order": order})   # just the <li>

The feature came from the django-template-partials package, and the 6.0 release notes include a migration guide for projects that used it.

The same release also changed the DEFAULT_AUTO_FIELD default to BigAutoField, which new projects already set explicitly since Django 3.2.

Signs of a Strong Answer

  • They say when a QuerySet runs SQL, and reach for count() and exists() over len().
  • They name how they would catch N+1 queries in CI, not only how to fix one.
  • They put side effects in transaction.on_commit() without being prompted.
  • They plan migrations for a table with live traffic in several deploys.
  • They know which operations skip signals, including update() and the new DB_CASCADE.
  • They can say which Django version they run, when its support ends, and what 6.2 LTS and DEP 20 mean for their upgrade plan.

Hiring Django Developers

Django rewards engineers who understand the database under the ORM, and a short live exercise on a list endpoint shows that quickly.

Second Talent matches companies with pre-vetted Python and Django developers from Asia, screened with questions like these. Our Django developer cost guide shows current rates.

Tell us the stack and we send a shortlist within 24 hours. Start hiring, or see our Python and FastAPI interview guides.

Hiring developers in Southeast Asia?

Get Cost Guide

How would you like to talk?

WhatsApp us Prefer texting at your own pace? Just hit us up on WhatsApp. We promise no spam and a hassle-free experience.

Loading available times…