ORM & QuerySets • Models & Migrations • Views & Forms • Auth & Security • REST Framework • 2026

Django Interview Questions

31 questions What each one tests, an answer frame, a spoken answer 36 min read

This page is for anyone facing a Django round, from a first backend job to a senior role. Most interviews open with how a request moves through middleware, URLs and views, then spend a long time on the ORM: lazy QuerySets, N+1 queries, transactions and migrations. After that come forms, authentication, CSRF and XSS, Django REST Framework, caching and how you deploy. Senior rounds add a production story and a judgement call. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer to say out loud. Practise saying them, then swap in your own stories.

Search all questions by round, difficulty and level, or save the ones you want to practise.

Core Concepts 5 questions

Easy Technical round Fresher Practice question

1. Django calls itself an MVT framework. What are the model, view and template, and how does that line up with MVC?

What the interviewer is really testing:
Whether you know where each kind of code belongs in a Django project, not just what the three letters stand for.
Answer frame:

Model: Python classes that describe the data; the ORM turns them into tables and queries.

View: a function or class that takes a request and returns a response; this is where the logic lives.

Template: the HTML with placeholders; presentation only. The URL dispatcher and framework play the controller's part.

Sample spoken answer:

"The model is my data layer: a Python class per table, and the ORM handles the SQL. The view is a function or class that receives the request, talks to the models, and returns a response. The template is the HTML with placeholders that the view fills in. The naming confuses people because Django's view does what MVC calls the controller, and Django's template does what MVC calls the view. The remaining controller work, deciding which code handles which URL, is done by the framework itself through the URL configuration. In practice I keep templates free of logic, keep views thin, and put business rules on the models or in a small service module, so they can be tested without a request."

Red flag to avoid:

Saying the Django view is the same as the MVC view, or putting queries and business rules inside templates.

They may ask next:
  • Where would you put a business rule like 'an order can't be cancelled after it ships'?
  • Does a Django view have to render a template at all?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

2. A browser requests a page from your Django site. Walk me through everything that happens until the response goes back.

What the interviewer is really testing:
Whether you can trace a request through the server, middleware, URL routing and view, which is what you need to debug anything in Django.
Answer frame:

Server: a WSGI or ASGI server receives the request and hands Django an HttpRequest.

Middleware in: each middleware runs in the order listed in MIDDLEWARE.

Routing and view: the URL resolver matches the path to a view, which builds an HttpResponse.

Middleware out: the response passes back through middleware in reverse order.

Sample spoken answer:

"The request first reaches an application server like Gunicorn or Uvicorn, which calls Django through WSGI or ASGI. Django wraps it in an HttpRequest object. It then runs through the middleware stack top to bottom, so things like security headers, sessions, authentication and CSRF checks happen before my code. Next the URL resolver walks the urlpatterns in ROOT_URLCONF and picks the first pattern that matches, capturing any path parameters. The view runs with the request and those parameters, usually queries the database through the ORM, maybe renders a template, and returns an HttpResponse. That response travels back up through the middleware in reverse order, where things like compression or extra headers get applied, and the server sends it to the browser. If the view raises an exception, middleware can handle it, otherwise Django turns it into an error page."

Red flag to avoid:

Skipping middleware entirely, or thinking the template is loaded before the view runs.

They may ask next:
  • What does request.user depend on, and what happens if the middleware that sets it is missing?
  • Where exactly does a 404 come from when no URL pattern matches?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

3. What is middleware in Django? Write one that logs how long each request takes, and tell me why the order in MIDDLEWARE matters.

What the interviewer is really testing:
Whether you can write the modern callable middleware correctly and understand that order creates dependencies between layers.
Answer frame:

What it is: a layer that wraps every request and response, like an onion around the view.

Shape: a class that stores get_response once at startup and runs code before and after calling it.

Order: requests pass top to bottom, responses bottom to top; some middleware depends on others above it.

Sample spoken answer:

"Middleware is code that wraps every request. Django builds a chain at startup: each middleware gets a get_response callable, which is the next layer down, and eventually the view. In my timing example, the code before calling get_response runs on the way in, and the code after runs on the way out, so I measure the gap and add it as a header and a log line. Order matters because each layer sees what the layers above it did. The authentication middleware reads the user from the session, so the session middleware has to come before it, otherwise request.user can't be set. Security middleware usually goes near the top so its redirects and headers apply to everything. I keep middleware light, because it runs on every single request, including static and health check hits if they go through Django."

Code:
import logging
import time

logger = logging.getLogger(__name__)


class RequestTimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response  # called once, at startup

    def __call__(self, request):
        start = time.monotonic()
        response = self.get_response(request)  # the rest of the chain and the view
        elapsed_ms = (time.monotonic() - start) * 1000
        response["X-Response-Time-Ms"] = f"{elapsed_ms:.0f}"
        logger.info("%s %s took %.0f ms", request.method, request.path, elapsed_ms)
        return response
Red flag to avoid:

Writing heavy database work inside middleware, or not knowing why session middleware must come before authentication.

They may ask next:
  • How would you skip this middleware for health check URLs?
  • What are process_view and process_exception for, and when would you use them?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

4. What are Django signals, when are they the right tool, and why do many teams keep business logic out of them?

What the interviewer is really testing:
Whether you know how signals really run and can weigh decoupling against hidden control flow.
Answer frame:

What they are: hooks like pre_save, post_save and post_delete that call receivers when something happens.

Good use: reacting to events from code you don't own, like a third-party app's model.

Costs: they run synchronously inside the same transaction, are easy to miss when reading code, and don't fire for update() or bulk_create().

Sample spoken answer:

"Signals let one piece of code react when something happens elsewhere. For example, a post_save receiver runs every time a model is saved. I connect receivers in the app's AppConfig ready method so they're registered once. They're useful when I need to react to a model I don't own, like a third-party app's model, where I can't just edit the save method. But I'm careful with them for our own business logic. A receiver runs synchronously, in the same request and the same database transaction, so a slow receiver slows the save and an exception in it bubbles up to whoever called save. Someone reading the view has no idea extra code runs. And QuerySet.update() and bulk_create() don't send save signals at all, so logic in a receiver can silently be skipped. For our own models, I'd rather call a clear service function that does the save and the follow-up steps in one visible place."

Red flag to avoid:

Using signals for core business rules without knowing that bulk operations skip them or that they run inside the save.

They may ask next:
  • What's the difference between pre_save and post_save, and what does the created argument tell a receiver?
  • How would you stop a receiver from firing while you load test data?
Say it in 60 seconds
Hard Situational round Mid-level, Senior Practice question

5. A teammate sends the welcome email from a post_save signal on User. Sometimes people get the email but their account doesn't exist. What's going on, and what do you suggest?

What the interviewer is really testing:
Whether you can link a strange production symptom to transactions and signals, and suggest a fix that is both correct and clear.
Answer frame:

Cause: post_save fires inside the transaction; if something later fails, the user row rolls back but the email already left.

Fix: send it with transaction.on_commit, ideally queuing a background task with the user's id.

Clarity: move the call into the sign-up service function rather than a hidden signal.

Sample spoken answer:

"The clue is that the email exists but the account doesn't. post_save runs straight after the INSERT, but still inside the transaction. If the sign-up view wraps several steps in atomic, or ATOMIC_REQUESTS is on, and a later step fails, the user row rolls back. The email, though, has already gone out, and you can't roll back an email. It also means the request waits on the mail server. I'd suggest two changes. First, wrap the send in transaction.on_commit, so it only happens once the data really commits, and have it queue a background task that takes the user's id and loads the user fresh, so the request stays fast and retries are possible. Second, I'd move it out of the signal into the function that registers users. Then anyone reading the sign-up code sees that it sends an email, and a bulk import or a test won't trigger emails by surprise. I'd pair with the teammate on it rather than just rewriting it."

Red flag to avoid:

Suggesting a sleep or a retry loop, or blaming the email provider without looking at the transaction.

They may ask next:
  • If the background task starts before the commit is visible, what goes wrong, and how does on_commit prevent it?
  • How would you write a test that proves no email is sent when sign-up fails?
Say it in 60 seconds

ORM & Queries 7 questions

Medium Technical round Fresher, Mid-level Practice question

6. People say Django QuerySets are lazy. What does that mean, and at what points does a QuerySet actually run a query?

What the interviewer is really testing:
Whether you can predict when the database is hit, which is the root of most Django performance surprises.
Answer frame:

Lazy: filter, exclude, order_by and chaining only build the query; nothing runs yet.

Evaluation: iterating, list(), len(), bool(), printing, or slicing with a step runs it.

Cache: an evaluated QuerySet keeps its results; a new filter or .all() makes a fresh query.

Sample spoken answer:

"Lazy means that building a QuerySet costs nothing. When I write Book.objects.filter(published=True).order_by('title'), Django just builds up a description of the SQL. The query only runs when I actually need the rows: when I loop over it, call list() or len() on it, test it in an if, print it in the shell, or slice it with a step. A plain slice like [:10] stays lazy and becomes a LIMIT. Once a QuerySet has been evaluated, it caches its results, so looping over the same object twice only queries once. But if I call .filter() or .all() again I get a new QuerySet and a new query. That's why I store the QuerySet in a variable when I'll reuse it, use exists() when I only need a yes or no, and use count() when I only need the number and won't load the rows anyway."

Red flag to avoid:

Believing filter() runs a query straight away, or calling list() on a large QuerySet just to check if it's empty.

They may ask next:
  • Does qs[0] hit the database every time you call it?
  • When would len(qs) be better than qs.count(), and when worse?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

7. What's the difference between select_related and prefetch_related, and how do you choose between them?

What the interviewer is really testing:
Whether you know how each one fetches related data, so you can fix N+1 queries with the right tool.
Answer frame:

select_related: one query with a SQL JOIN; for ForeignKey and OneToOne relations.

prefetch_related: one extra query per relation, stitched together in Python; for many-to-many and reverse foreign keys.

Why it matters: without them, touching a relation inside a loop runs one query per row.

Sample spoken answer:

"Both solve the same problem: touching a related object inside a loop, which otherwise fires one query per row, the classic N+1. select_related follows ForeignKey and OneToOne fields using a JOIN, so I get the books and their authors in a single query. It only works where each row has at most one related object. For the many side, like an author's books or a many-to-many of tags, a JOIN would multiply rows, so prefetch_related runs a second query with an IN clause for all the related rows and matches them up in Python. So the rule of thumb is: a single related object, select_related; a collection, prefetch_related. I can combine them, and when I need to filter or order the prefetched rows I use a Prefetch object with its own QuerySet."

Code:
# One query: books JOIN authors
books = Book.objects.select_related("author")
for book in books:
    print(book.title, book.author.name)  # no extra query

# Two queries: authors, then all their books with IN (...)
# (assumes Book.author has related_name="books")
authors = Author.objects.prefetch_related("books")
for author in authors:
    print(author.name, [b.title for b in author.books.all()])
Red flag to avoid:

Mixing them up, or saying prefetch_related does a JOIN.

They may ask next:
  • If you call .filter() on a prefetched relation inside the loop, does the prefetch still help?
  • How would you prove in a test that a view doesn't have an N+1 problem?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

8. What are F expressions and Q objects used for? Show me a safe way to increment a view counter when many requests hit at once.

What the interviewer is really testing:
Whether you know how to push work into the database to avoid race conditions, and how to build OR and NOT conditions.
Answer frame:

F expressions: refers to a column's value inside the database, so the update happens in SQL, not in Python.

Race: reading a value, adding one in Python and saving loses updates when two requests overlap.

Q objects: wraps conditions so you can combine them with OR, AND and NOT.

Sample spoken answer:

"If I load a post, add one to views in Python and save it, two requests that overlap can both read the same number and both write the same result, so one view is lost. With an F expression the update becomes a single SQL statement, views equals views plus one, and the database applies each one in turn, so nothing is lost. I use update() on a filtered QuerySet for this, which also skips loading the object. One catch: if I set an F expression on an instance and call save, the attribute holds the expression until I call refresh_from_db. Q objects are for conditions that keyword arguments can't express. Keyword arguments to filter are always joined with AND, so for OR or NOT I wrap the conditions in Q and combine them with the pipe, the ampersand, or the tilde."

Code:
from django.db.models import F, Q

# Increment in the database: safe when requests overlap
Post.objects.filter(pk=post_id).update(views=F("views") + 1)

# OR and NOT need Q objects
visible = Post.objects.filter(
    Q(status="published") | Q(author=request.user),
    ~Q(is_deleted=True),
)
Red flag to avoid:

Incrementing in Python and calling save under concurrency, or not knowing how to write an OR condition.

They may ask next:
  • Does update() call the model's save method or send save signals?
  • How would you compare two fields on the same row, like finding tasks whose done date is after their due date?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

9. Using the ORM, return the five authors with the most published books, along with that count. How is annotate different from aggregate?

What the interviewer is really testing:
Whether you can do grouping and counting in the database instead of in Python loops, and know what each method returns.
Answer frame:

annotate: adds a computed value to every row, which means a GROUP BY per object.

aggregate: collapses the whole QuerySet into one dictionary of totals.

Filtered count: Count with a filter argument counts only the matching related rows.

Sample spoken answer:

"I'd annotate each author with a Count over their books, using the filter argument so only published books are counted, then order by that count descending and slice the first five. That's one SQL query with a GROUP BY, a LIMIT and the count computed by the database, instead of loading every book into Python. The difference between the two methods is what comes back. annotate returns a QuerySet where every object gets an extra attribute, here published, so I can keep filtering and ordering on it. aggregate returns a plain dictionary with totals for the whole QuerySet, for example the total number of books, and ends the chain. One thing I watch for is combining two annotations over different multi-valued relations, because the joins multiply rows and the counts come out inflated."

Code:
from django.db.models import Count, Q

top_authors = (
    Author.objects
    .annotate(published=Count("books", filter=Q(books__status="published")))
    .filter(published__gt=0)
    .order_by("-published")[:5]
)
for author in top_authors:
    print(author.name, author.published)

# aggregate returns a dict, not a QuerySet
totals = Book.objects.aggregate(total=Count("id"))  # {"total": ...}
Red flag to avoid:

Looping over authors in Python and calling len() on each author's books.

They may ask next:
  • Why can two Count annotations over different relations give wrong numbers, and how do you fix it?
  • How would you get the count per status in one query, using values() and annotate()?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

10. How does Django handle database transactions? Walk me through atomic and select_for_update with an example like reserving stock.

What the interviewer is really testing:
Whether you can keep data correct under concurrency, and know the rules around atomic blocks, row locks and errors.
Answer frame:

Default: autocommit; each query commits on its own unless you open a transaction.

atomic: a decorator or context manager; everything inside commits together or rolls back; nesting uses savepoints.

Locks: select_for_update locks the selected rows until the transaction ends, so a second request waits.

Gotchas: catch errors outside the atomic block; side effects go in transaction.on_commit.

Sample spoken answer:

"By default Django runs in autocommit, so every query commits immediately. When several writes must succeed or fail together, I wrap them in transaction.atomic. If an exception escapes the block, everything inside rolls back. Nested atomic blocks become savepoints. For stock, atomic alone isn't enough, because two requests could both read five items left and both sell the last one. So inside the atomic block I load the product with select_for_update, which locks that row. The second request waits until the first commits, then reads the updated stock. select_for_update has to be inside a transaction, or Django raises an error. Two more rules: I don't catch a database error inside the atomic block and carry on, because the transaction is broken at that point; I catch it outside. And anything that talks to the outside world, like sending an email, goes in transaction.on_commit so it only happens if the data really committed."

Code:
from django.db import transaction


def reserve(product_id, qty, user):
    with transaction.atomic():
        product = Product.objects.select_for_update().get(pk=product_id)
        if product.stock < qty:
            raise OutOfStock()
        product.stock -= qty
        product.save(update_fields=["stock"])
        order = Order.objects.create(product=product, quantity=qty, user=user)
        transaction.on_commit(lambda: send_order_email.delay(order.pk))
    return order
Red flag to avoid:

Thinking atomic alone prevents two requests from overselling, or sending emails inside the transaction.

They may ask next:
  • If a view locks two rows, how do you avoid a deadlock with another request locking them in the opposite order?
  • What does ATOMIC_REQUESTS do, and why might a team turn it off?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

11. What is a model manager in Django? When would you write a custom manager or QuerySet, and what's the risk of filtering the default one?

What the interviewer is really testing:
Whether you can keep query logic reusable and in one place, and know how the default manager affects the admin and other code.
Answer frame:

Manager: the interface to the table; objects is the one Django adds by default.

Custom QuerySet: put reusable filters as methods so they chain; expose them with as_manager().

Risk: a filtered default manager hides rows from the admin and anywhere else that uses the default.

Sample spoken answer:

"A manager is how a model talks to its table. Post.objects is the default one Django adds. When the same filter appears in many views, like 'published and not in the future', I don't copy it around. I write a custom QuerySet class with methods like published() and by_author(), and attach it with as_manager(). Because they're QuerySet methods, they chain: Post.objects.published().by_author(user). That keeps the rule in one place, so if the definition of published changes, I change one method. The risk is overriding get_queryset on the default manager to hide rows, like soft-deleted ones. The admin and other tools use the default manager, so those rows vanish and nobody can see or restore them. If I want that, I keep objects unfiltered and add a second manager, say active, for the filtered view."

Code:
from django.db import models
from django.utils import timezone


class PostQuerySet(models.QuerySet):
    def published(self):
        return self.filter(status="published", published_at__lte=timezone.now())

    def by_author(self, user):
        return self.filter(author=user)


class Post(models.Model):
    # ... fields ...
    objects = PostQuerySet.as_manager()

# Post.objects.published().by_author(request.user)
Red flag to avoid:

Repeating the same filter in dozens of views, or filtering the default manager without realising the admin uses it.

They may ask next:
  • How is a custom manager method different from a classmethod on the model?
  • How would you implement soft delete without surprising the rest of the team?
Say it in 60 seconds
Hard Situational round Mid-level, Senior Practice question

12. An export endpoint loops over 200,000 rows in Python to build a CSV and now times out. The product team needs it this week. What do you do?

What the interviewer is really testing:
Whether you can fix a real performance problem in steps, pushing work to the database and out of the request, under time pressure.
Answer frame:

Measure: find whether the time is in queries, Python work or the response size.

Quick wins: fetch only needed columns, avoid per-row queries, aggregate in SQL, and stream with iterator().

Bigger fix: move the export to a background job that emails or links the finished file.

Sample spoken answer:

"First I'd measure, because the fix depends on where the time goes. Usually it's a few things together. There are often per-row queries from related fields, which select_related fixes. The code loads full model objects when it needs five columns, so values_list with just those fields cuts memory a lot. Any totals computed in Python can become an annotate in the database. And the QuerySet caches every row, so I'd use iterator() to process them in chunks, and send the file with a StreamingHttpResponse so the first bytes go out right away. That alone often gets it under the timeout this week. But a request that grows with the data will time out again, so I'd also propose the proper fix: a background job that builds the file and sends the user a download link when it's ready. I'd tell the product team which part ships now and which follows."

Red flag to avoid:

Just raising the server timeout, or rewriting it all in raw SQL without measuring first.

They may ask next:
  • What problem can a streaming response hit with proxies or load balancer timeouts?
  • How would you stop two users from starting the same heavy export at the same time?
Say it in 60 seconds

Models & Migrations 4 questions

Easy Technical round Fresher Practice question

13. How do Django migrations work? What do makemigrations and migrate each do, and how does Django know what's already applied?

What the interviewer is really testing:
Whether you understand migrations as versioned files that belong in the repository, not a magic sync step.
Answer frame:

makemigrations: compares your models to the state recorded in existing migration files and writes new files.

migrate: applies unapplied migrations to the database in dependency order.

Tracking: applied migrations are recorded in the django_migrations table.

Sample spoken answer:

"Migrations are version control for the database schema. When I change a model, makemigrations compares my models with the state described by the migration files already in the app and writes a new file describing the difference, like adding a field. It doesn't touch the database at all. migrate is what changes the database: it applies each migration that hasn't run yet, following the dependencies between files, and records each one in the django_migrations table. That table is how Django knows what's done. Because of that, migration files are code: I commit them, review them in pull requests, and never edit one that has already run somewhere else. If I want to see the actual SQL before running it, sqlmigrate prints it, and showmigrations lists what's applied."

Red flag to avoid:

Saying makemigrations changes the database, or deleting migration files to fix a problem on a shared project.

They may ask next:
  • Two developers both create migration 0015 in the same app on different branches. What happens, and how do you fix it?
  • What is a data migration, and how is it different from a schema migration?
Say it in 60 seconds
Hard Technical round Senior Practice question

14. You need to add a required column to a table with millions of rows, while the site stays up. How do you plan the migrations?

What the interviewer is really testing:
Whether you know that schema changes can lock tables and break old code during a rolling deploy, and can split the change safely.
Answer frame:

Expand: add the column as nullable first, which is cheap, and deploy code that writes it.

Backfill: fill existing rows in batches, outside one giant transaction.

Contract: only then make it non-null, once every row and every running version is ready.

Check: read the SQL with sqlmigrate, and build big indexes concurrently where the database supports it.

Sample spoken answer:

"I wouldn't do it in one migration. Adding a non-null column means every existing row needs a value, and on a big table that can rewrite or lock it while live traffic waits. And during a deploy, old and new code run side by side, so the schema has to work for both; old code that inserts rows without the new value would start failing. So I split it. First, a migration adds the column as nullable, which is quick, and the code starts writing the value for new rows. Second, I backfill old rows in small batches with a script or a data migration, so no single transaction holds locks for long. Third, once every row has a value, a final migration adds the not-null constraint. I run sqlmigrate on each step to see the real SQL. If I also need an index on PostgreSQL, I'd use AddIndexConcurrently in a non-atomic migration so writes aren't blocked. Renames follow the same idea: add, copy, switch, then drop."

Red flag to avoid:

Adding a non-null field with a default in one step on a huge table and hoping, with no thought for old code still running.

They may ask next:
  • Why is renaming a column with a plain migration dangerous during a rolling deploy?
  • How would you backfill ten million rows without overloading the database?
Say it in 60 seconds
Easy Technical round Fresher Practice question

15. On a model field, what's the difference between null=True and blank=True? Why is null=True on a CharField usually discouraged?

What the interviewer is really testing:
Whether you separate database rules from validation rules, a small detail that shows real modelling experience.
Answer frame:

null: a database setting; the column may store NULL.

blank: a validation setting; forms and full_clean allow the field to be empty.

Text fields: use an empty string for 'no value' so there aren't two kinds of empty.

Sample spoken answer:

"null is about the database: null=True lets the column store NULL. blank is about validation: blank=True lets forms, the admin and full_clean accept an empty value. They're independent, so I often need both, for example an optional date field needs null=True so the database can hold nothing and blank=True so the form allows it. For CharField and TextField, the Django convention is to leave null off and store an empty string, because otherwise 'no value' can be either NULL or an empty string and every query has to check both. The exception is a text field that's unique but optional: then I do use null=True, because several empty strings would clash with the unique constraint, while several NULLs don't."

Red flag to avoid:

Treating the two as the same setting, or not knowing that blank only affects validation.

They may ask next:
  • What happens if a field has blank=True but not null=True, and the form leaves it empty?
  • Does model.save() run the blank check?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

16. Django has abstract base classes, multi-table inheritance and proxy models. How do they differ, and when would you use each?

What the interviewer is really testing:
Whether you know what each style does to the database, and avoid the hidden joins of multi-table inheritance.
Answer frame:

Abstract: no table of its own; its fields are copied into each child's table.

Multi-table: parent and child each get a table, linked by a one-to-one key; reads need a join.

Proxy: same table as the parent; only Python behaviour changes, like methods, ordering or a manager.

Sample spoken answer:

"An abstract base class, with abstract set to True in Meta, has no table. I use it to share common fields like created_at and updated_at, and each child gets those columns in its own table. It's the one I reach for most. Multi-table inheritance is when the parent is a normal model: both get tables, the child's table has a one-to-one link to the parent, and loading a child means a join. Querying the parent returns parent objects, not the specific child types. It's useful when I genuinely need to query all the kinds together, but the joins can get costly, so I use it sparingly. A proxy model shares the parent's table exactly and can't add fields. I use it to give the same data different behaviour, for example a separate admin page for a subset of users, with its own manager and ordering."

Red flag to avoid:

Not knowing that multi-table inheritance creates two tables and a join, or trying to add fields to a proxy model.

They may ask next:
  • If you query a multi-table parent, how would you get back the specific child object?
  • Why might you choose a plain ForeignKey instead of multi-table inheritance?
Say it in 60 seconds

Views & Forms 2 questions

Easy Coding round Fresher, Mid-level Practice question

17. When do you write a function-based view and when a class-based view? Show a list view that only shows the logged-in user's own orders.

What the interviewer is really testing:
Whether you can choose between explicit and reusable code, and know which method to override on a generic view.
Answer frame:

Function views: explicit and easy to read; good for one-off or unusual logic.

Class views: reuse through inheritance and mixins; generic views cover list, detail, create, update and delete.

Customising: override get_queryset or get_context_data; put mixins like LoginRequiredMixin first.

Sample spoken answer:

"A function view is just a function that takes a request and returns a response. Everything is visible top to bottom, so I use it when the logic is unusual or when it handles a form in a way the generic views don't fit. Class-based views shine for standard patterns. ListView, DetailView, CreateView and the rest already handle pagination, templates and form processing, and I only override the parts that differ. For 'my orders', I'd subclass ListView, add LoginRequiredMixin so anonymous users are sent to the login page, and override get_queryset to filter by request.user. The mixin has to come first in the class list so it runs before the view. The trade-off with class views is that the behaviour is spread across parent classes, so a newcomer has to know which method to override."

Code:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView

from .models import Order


class MyOrderListView(LoginRequiredMixin, ListView):
    model = Order
    paginate_by = 20
    template_name = "orders/my_orders.html"

    def get_queryset(self):
        return (Order.objects
                .filter(user=self.request.user)
                .order_by("-created_at"))
Red flag to avoid:

Filtering the orders in the template instead of the query, or leaving the list open to other users.

They may ask next:
  • What does as_view() actually return, and why do you call it in urls.py?
  • How would you add a permission check that isn't just 'logged in', for example staff only?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

18. How does form validation work in Django? Where do you put a check on one field, and where do you check two fields against each other?

What the interviewer is really testing:
Whether you know the order Django validates in, so rules go in the right hook and errors show next to the right field.
Answer frame:

is_valid: triggers cleaning; errors land in form.errors and good values in cleaned_data.

One field: clean_<fieldname> runs after the field's own checks and must return the value.

Cross-field: clean() runs last and sees every cleaned field; add_error attaches a message to one field.

ModelForm: also validates against the model, including unique constraints.

Sample spoken answer:

"When I call form.is_valid(), Django cleans each field in turn. First the field's own checks, like whether an EmailField holds a real email, then any method named clean_ plus the field name. That's where a single-field rule goes, like checking the email isn't already registered, and the method must return the cleaned value. After all the fields, Django calls the form's clean() method, which is where I compare fields, like password and confirm. There I use add_error so the message shows next to the right field rather than at the top. Valid values end up in cleaned_data and errors in form.errors. A ModelForm also runs the model's validation, including unique checks. One thing people miss: calling save() on a model directly doesn't run full_clean, so the validation only happens if something calls it."

Code:
from django import forms
from django.contrib.auth import get_user_model

User = get_user_model()


class SignupForm(forms.Form):
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    confirm = forms.CharField(widget=forms.PasswordInput)

    def clean_email(self):
        email = self.cleaned_data["email"].lower()
        if User.objects.filter(email__iexact=email).exists():
            raise forms.ValidationError("That email is already registered.")
        return email

    def clean(self):
        cleaned = super().clean()
        if cleaned.get("password") != cleaned.get("confirm"):
            self.add_error("confirm", "Passwords don't match.")
        return cleaned
Red flag to avoid:

Validating in the view with manual if statements, or reading request.POST directly after is_valid.

They may ask next:
  • Why use cleaned_data.get() rather than square brackets inside clean()?
  • How would you reuse the same validation in a form and in an API serializer?
Say it in 60 seconds

Auth & Security 5 questions

Hard Technical round Mid-level, Senior Practice question

19. Why do people say you should set up a custom user model at the start of a Django project? What if the project already has live users?

What the interviewer is really testing:
Whether you know how deeply the user model is wired into a project, and have a realistic plan when it's too late to swap it.
Answer frame:

Why early: AUTH_USER_MODEL is referenced by foreign keys and migrations; changing it later is painful.

How: subclass AbstractUser to add fields, or AbstractBaseUser for full control, like logging in by email.

References: use settings.AUTH_USER_MODEL in models and get_user_model() in code.

Too late: add a profile model with a one-to-one link, or plan a careful manual migration.

Sample spoken answer:

"The user model sits under almost everything: foreign keys from orders, comments, permissions, the admin, and many migrations point at it. If I start with the built-in User and later need, say, email as the login, swapping AUTH_USER_MODEL means rewriting those relations and migrations by hand. So on a new project I create a custom user straight away, usually subclassing AbstractUser, even if it has no extra fields yet. If I need email login, I'd use AbstractBaseUser with PermissionsMixin, set USERNAME_FIELD, and write a manager with create_user and create_superuser. Everywhere else I refer to settings.AUTH_USER_MODEL in models and get_user_model() in code, never the User class directly. If the project already has live users, I'd usually add a profile model with a one-to-one link for the extra data. A full swap is possible but it's a planned, tested, manual migration, not a quick change."

Red flag to avoid:

Suggesting you just change AUTH_USER_MODEL on a live project and run makemigrations.

They may ask next:
  • Why shouldn't a reusable app import User directly?
  • How would you let users log in with either username or email?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

20. How do Django's built-in permissions and groups work? How would you let editors publish any article but let authors edit only their own drafts?

What the interviewer is really testing:
Whether you know what the permission system gives you out of the box and where it stops, since it checks access per model, not per row.
Answer frame:

Defaults: migrate creates add, change, delete and view permissions for every model; Meta.permissions adds custom ones.

Assigning: give permissions to groups like Editors, then put users in groups instead of granting one by one.

Checking: user.has_perm, the permission_required decorator or PermissionRequiredMixin, and perms in templates.

Ownership: the built-in backend has no per-row rules, so filter by owner in the query.

Sample spoken answer:

"When I run migrate, Django creates four permissions for every model: add, change, delete and view. I can add my own in the model's Meta, like publish_article. I don't usually give permissions to people one by one. I create a group such as Editors, give the group the permissions, and add users to it, so a role change is one click in the admin. In code I check with user.has_perm using the app label and codename, or protect a view with PermissionRequiredMixin, and templates get a perms variable for hiding buttons. Superusers pass every check. The catch is that these permissions are per model, not per row. The default backend can say whether someone may change articles, not whether they may change this article. So publishing is a custom permission for the Editors group, and for authors editing their own drafts I filter the query by author and status, so another person's draft simply returns 404."

Code:
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import models
from django.shortcuts import get_object_or_404
from django.views.generic import UpdateView


class Article(models.Model):
    # ... fields ...
    class Meta:
        permissions = [("publish_article", "Can publish articles")]


class PublishArticleView(PermissionRequiredMixin, UpdateView):
    permission_required = "news.publish_article"
    # ...


@login_required
def edit_draft(request, pk):
    article = get_object_or_404(
        Article, pk=pk, author=request.user, status="draft"
    )
    # ... handle the form ...
Red flag to avoid:

Checking is_staff or a hard-coded username for access, or assuming the change permission alone stops people editing each other's drafts.

They may ask next:
  • You add a user to the Editors group, but has_perm still returns False in the same request. Why?
  • When would you reach for a package that stores permissions per object instead of filtering by owner?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

21. How does Django's CSRF protection actually work, and when is it reasonable to exempt a view from it?

What the interviewer is really testing:
Whether you understand the attack and the token check, instead of reaching for csrf_exempt whenever a POST fails.
Answer frame:

Attack: another site makes the victim's browser send a request that carries their cookies.

Check: on unsafe methods the middleware compares a token from the form or the X-CSRFToken header with the secret in the cookie.

Exempt only: when the caller isn't a browser session, like a webhook that proves itself another way.

Sample spoken answer:

"CSRF is when a malicious page makes your browser send a POST to a site you're logged into. The browser attaches your session cookie, so without protection the request looks like you. Django's CSRF middleware blocks that. It sets a secret in a cookie, and every form includes a matching token through the csrf_token template tag, or JavaScript sends it in an X-CSRFToken header. On POST, PUT, PATCH and DELETE, the middleware checks the token matches the secret. Another site can make the browser send the cookie, but it can't read the token, so it can't forge the request. Django also checks the Origin header when the browser sends one, and on HTTPS it checks the Referer. I only exempt a view when it isn't used by a logged-in browser at all, like a payment webhook, and then it must verify the sender another way, such as checking a signature. And GET views must never change data, because CSRF protection doesn't cover them."

Red flag to avoid:

Adding csrf_exempt to fix a 403 without understanding the cause, or changing data on a GET request.

They may ask next:
  • Why doesn't an API that uses token authentication in a header need CSRF protection?
  • Your single-page app's POSTs return 403. What do you check before touching csrf_exempt?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

22. How does Django protect you from XSS and SQL injection by default, and what are the common ways developers accidentally switch that off?

What the interviewer is really testing:
Whether you know what the framework does for you and exactly where that protection ends.
Answer frame:

XSS: templates escape every variable by default; |safe, mark_safe and autoescape off remove that.

Other contexts: escaping for HTML isn't enough inside JavaScript; use the json_script filter.

SQL: the ORM sends values as parameters; raw(), extra() and cursor.execute with string formatting bypass it.

Sample spoken answer:

"For XSS, Django templates escape every variable by default, so a name containing a script tag is shown as text rather than run. The protection goes away when someone marks content safe: the safe filter, mark_safe in Python, or turning autoescape off. I only do that for HTML I built myself, and I use format_html to build it so the inserted values still get escaped. Escaping for HTML also isn't enough inside a script block, so to pass data to JavaScript I use the json_script filter. For SQL injection, the ORM always sends values as query parameters, never pasted into the SQL text. The danger is raw SQL: calling raw() or cursor.execute with an f-string that includes user input. The fix is to pass the values as a separate params list, which the database driver handles safely. In code review, any f-string near SQL gets a second look from me."

Code:
# Unsafe: user input becomes part of the SQL text
Book.objects.raw(f"SELECT * FROM shop_book WHERE title = '{q}'")

# Safe: the driver sends q as a separate parameter
Book.objects.raw("SELECT * FROM shop_book WHERE title = %s", [q])
Red flag to avoid:

Saying the ORM makes SQL injection impossible even with raw SQL, or using the safe filter on user content.

They may ask next:
  • A user can choose the sort column for a list. Parameters can't hold column names, so how do you keep it safe?
  • Where else might user content end up unescaped, for example in emails or admin list pages?
Say it in 60 seconds
Easy Situational round Fresher, Mid-level Practice question

23. You're about to put a Django app on the public internet for the first time. What do you check in the settings before launch?

What the interviewer is really testing:
Whether you know the handful of settings that turn a development project into a safe production one.
Answer frame:

Must change: DEBUG off, SECRET_KEY from the environment, ALLOWED_HOSTS set to the real domains.

HTTPS: redirect to HTTPS, secure session and CSRF cookies, then HSTS once HTTPS is stable.

Check: run manage.py check --deploy, set up error reporting, and protect the admin.

Sample spoken answer:

"First, DEBUG must be off. With it on, any error page shows a full traceback with settings and local variables to anyone. Second, SECRET_KEY comes from an environment variable or secret store, never the repository, since it signs sessions and tokens. Third, ALLOWED_HOSTS lists the real domains, so requests with a forged Host header are rejected. Then HTTPS: SECURE_SSL_REDIRECT, and SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE so cookies never travel over plain HTTP. I'd add HSTS with a short duration first and raise it once I'm sure HTTPS works everywhere, because browsers remember it. Then I run manage.py check --deploy, which flags most of these. Finally, I make sure errors reach us through logging or an error tracker now that the debug page is gone, and I protect the admin with strong passwords and ideally two-factor sign-in."

Red flag to avoid:

Launching with DEBUG on, or keeping the secret key in the settings file in the repository.

They may ask next:
  • Why is it risky to turn on HSTS with a very long duration on day one?
  • What happens with DEBUG off if ALLOWED_HOSTS is empty?
Say it in 60 seconds

REST Framework 2 questions

Easy Technical round Fresher, Mid-level Practice question

24. In Django REST Framework, what does a serializer do? How do you add validation and a read-only field to a ModelSerializer?

What the interviewer is really testing:
Whether you see serializers as both output formatting and input validation, the two jobs they do in every API.
Answer frame:

Out: turn model instances into plain Python data that is rendered as JSON.

In: validate incoming data into validated_data; save() then calls create() or update().

ModelSerializer: builds fields from the model; validate_<field> for one field, validate() for several.

Sample spoken answer:

"A serializer works in both directions. Going out, it turns model instances or QuerySets into plain Python dictionaries, which DRF renders as JSON. Coming in, it takes the request data, validates it, and gives me validated_data. Calling save() then calls create() or update() depending on whether I passed an instance. A ModelSerializer saves typing by building the fields from the model, including its validators, and I list the fields explicitly rather than using all, so I don't leak a new column by accident. For a single field I add a method named validate_ plus the field name, and for rules across fields I override validate(). Read-only data, like the id or an author name pulled through a relation with source, I mark read_only so clients can see it but not set it."

Code:
from rest_framework import serializers

from .models import Book


class BookSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source="author.name", read_only=True)

    class Meta:
        model = Book
        fields = ["id", "title", "isbn", "author", "author_name"]
        read_only_fields = ["id"]

    def validate_isbn(self, value):
        if len(value) not in (10, 13):
            raise serializers.ValidationError("ISBN must be 10 or 13 characters.")
        return value
Red flag to avoid:

Describing a serializer only as a JSON converter and forgetting it validates input.

They may ask next:
  • Why is fields = '__all__' risky on a public API?
  • How would you accept an author id on write but return the full author object on read?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

25. Build a DRF endpoint for notes where users can only see and edit their own. Explain viewsets, routers and how permission classes are checked.

What the interviewer is really testing:
Whether you can wire a ModelViewSet correctly and know that object permissions don't protect list endpoints on their own.
Answer frame:

Viewset: one class for list, create, retrieve, update and delete; a router generates the URLs.

Scope data: filter get_queryset by request.user so other users' rows are never reachable.

Permissions: has_permission runs on every request; has_object_permission only when get_object() is called.

Ownership: set the owner in perform_create, never from client input.

Sample spoken answer:

"A ModelViewSet gives me list, create, retrieve, update, partial update and delete in one class, and a router turns it into URLs. The important part is how access is enforced. First, I override get_queryset to return only the current user's notes, so another user's note isn't in the list and a request for it by id returns 404. Second, I add a custom permission with has_object_permission checking the owner. That's a second line of defence if someone later widens the queryset. But it's important to know that object permissions are only checked when the view calls get_object, which list views don't do, so the queryset filter is what actually protects the list. Finally, I set the owner in perform_create from request.user, so a client can't create notes on someone else's behalf. Because there's no queryset attribute on the class, the router needs a basename."

Code:
from rest_framework import permissions, routers, viewsets


class IsOwner(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.owner == request.user


class NoteViewSet(viewsets.ModelViewSet):
    serializer_class = NoteSerializer
    permission_classes = [permissions.IsAuthenticated, IsOwner]

    def get_queryset(self):
        return Note.objects.filter(owner=self.request.user)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)


router = routers.DefaultRouter()
router.register("notes", NoteViewSet, basename="note")
# urlpatterns = router.urls
Red flag to avoid:

Relying only on has_object_permission and leaving the list endpoint returning every user's notes.

They may ask next:
  • Would you rather return 404 or 403 when someone requests another user's note, and why?
  • How would you add rate limiting to this endpoint in DRF?
Say it in 60 seconds

Caching & Deployment 3 questions

Hard Technical round Mid-level, Senior Practice question

26. What levels of caching does Django give you, and how would you cache an expensive dashboard without showing people stale or wrong data?

What the interviewer is really testing:
Whether you can pick the right cache level and have a real plan for invalidation and per-user data.
Answer frame:

Levels: whole site via middleware, per view with cache_page, template fragments, and the low-level cache API.

Backend: a shared store like Redis or Memcached in production; the default local memory cache is per process.

Invalidation: a timeout as a safety net, plus deleting the key when the data changes.

Per user: include the user or team in the key; never cache personalised pages under a shared key.

Sample spoken answer:

"Django has four levels. The site-wide cache middleware caches whole pages, cache_page does the same for one view, the cache template tag caches a fragment, and the low-level API lets me cache any value with get and set. For a dashboard, whole-page caching is dangerous because the page is personal, so I'd use the low-level API on the expensive part, the computed stats, with the team id in the key. I'd set a timeout so it can't be stale forever, and delete the key when the underlying data changes, from the service function that writes it. In production the backend has to be shared, like Redis or Memcached, because the default local memory cache lives inside each worker process, so workers would disagree and invalidation wouldn't reach them all. I'd also think about a burst of requests all missing at once after expiry, and measure the hit rate to confirm it helps."

Code:
from django.core.cache import cache


def get_dashboard_stats(team_id):
    key = f"dashboard-stats:{team_id}"
    stats = cache.get(key)
    if stats is None:
        stats = compute_stats(team_id)  # the slow part
        cache.set(key, stats, timeout=300)
    return stats


def invalidate_dashboard_stats(team_id):
    cache.delete(f"dashboard-stats:{team_id}")
Red flag to avoid:

Caching a personalised page under one key, or having no plan for invalidation beyond 'it expires eventually'.

They may ask next:
  • Why is cache_page risky on a page that shows the logged-in user's name?
  • How would you stop many requests from recomputing the same expired value at the same moment?
Say it in 60 seconds
Easy Technical round Fresher Practice question

27. What's the difference between static files and media files in Django, and how is each one served in production?

What the interviewer is really testing:
Whether you know that Django's development server hides how files are really served, and treat user uploads as untrusted.
Answer frame:

Static: CSS, JavaScript and images that ship with the code; collectstatic gathers them into STATIC_ROOT.

Media: files users upload through FileField or ImageField; stored under MEDIA_ROOT or in object storage.

Production: a web server, CDN or storage service serves both; Django's dev server helpers are for development only.

Sample spoken answer:

"Static files are part of the code: stylesheets, scripts, logos. Each app can have a static folder, and running collectstatic copies everything into STATIC_ROOT, one place a web server or CDN can serve from. With a hashed file storage, filenames include a content hash so browsers can cache them for a long time. Media files are what users upload, like profile pictures, stored through FileField or ImageField into MEDIA_ROOT or, more often in production, an object storage bucket through a storage backend. The development server serves static files for convenience when DEBUG is on, which hides the real setup. In production Django shouldn't be serving either type itself; Nginx, a CDN, a library like WhiteNoise for static, or the storage service does it. For media I also treat every upload as untrusted: check type and size, and never trust the uploaded filename."

Red flag to avoid:

Mixing uploads into the static folder, or not knowing that collectstatic exists.

They may ask next:
  • Why shouldn't user uploads be served from the same domain as your app without care?
  • Your CSS works locally but is missing in production. What do you check first?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

28. How would you deploy a Django app to production? Talk me through the servers, where migrations run, and how settings differ per environment.

What the interviewer is really testing:
Whether you have actually run Django in production beyond runserver, and can avoid common deploy mistakes.
Answer frame:

Serving: runserver is for development only; use Gunicorn behind WSGI, or an ASGI server if you need async.

In front: a reverse proxy or load balancer handles TLS, static files and slow clients.

Release steps: build once, collectstatic, run migrate once per release, then start or roll the workers.

Settings: read secrets and per-environment values from environment variables; DEBUG off.

Sample spoken answer:

"In production I never use runserver. The app runs under an application server, usually Gunicorn with a few worker processes through WSGI, or an ASGI server like Uvicorn if we use async views or websockets. In front sits Nginx or a cloud load balancer that terminates HTTPS and serves static files, and if it terminates TLS I set SECURE_PROXY_SSL_HEADER so Django knows the request was secure. The release pipeline builds the image once, runs collectstatic, then runs migrate as a single step before the new workers take traffic, not on every worker start, where they'd race each other. Settings come from environment variables, so the same code runs in staging and production with different databases, keys and hosts, and secrets never live in the repository. I add a health check URL, send errors to a tracker, and write logs to standard output so the platform collects them."

Red flag to avoid:

Running runserver in production, or committing the production SECRET_KEY to the repository.

They may ask next:
  • Why is it a problem if every container runs migrate as it starts up?
  • How do you decide how many Gunicorn workers to run?
Say it in 60 seconds

Real Work 3 questions

Medium Behavioral round Mid-level, Senior Practice question

29. Tell me about a Django page or API endpoint you made much faster. How did you find out where the time was going?

What the interviewer is really testing:
Whether you measure before changing anything, and know the usual Django causes of slowness.
Answer frame:

Symptom: what was slow, and how users or monitoring noticed.

Measure: query counts and timings with a profiler, debug toolbar, or logs.

Fix and guard: the change, the result, and a test that stops it coming back.

Sample spoken answer:

"At my last company the order history page in our customer portal took several seconds for our bigger customers. Before changing anything, I opened it locally with the debug toolbar against a copy of realistic data, and saw over four hundred queries. The template was showing each order's customer and its line items, and each of those was a separate query per order, the classic N+1. I added select_related for the customer and prefetch_related for the line items, which brought it down to three queries. The page was still a bit slow, and EXPLAIN showed the main query sorting a big table without an index, so I added a composite index on customer and created date. The page went from seconds to well under half a second. To stop it coming back, I added a test using assertNumQueries on that view, so a new template field that sneaks in a query fails the build."

Red flag to avoid:

A story about adding caching straight away without ever measuring what was slow.

They may ask next:
  • What would you do if the query count was fine but the page was still slow?
  • How do you find slow endpoints in production, not just on your laptop?
Say it in 60 seconds
Hard Behavioral round Mid-level, Senior Practice question

30. Tell me about a Django migration that caused a problem, on your team's branches or in production. What did you change afterwards?

What the interviewer is really testing:
Whether you've felt the real risks of schema changes and turned the lesson into a process, not just a fix.
Answer frame:

What happened: the migration, the symptom, and who noticed.

Recovery: how you got the site healthy again, calmly.

Prevention: the review step or rule you added so it wouldn't recur.

Sample spoken answer:

"In my last role I added an index to support a new search filter. It was a normal AddIndex migration on our events table, which had tens of millions of rows. It passed review and ran fine on staging, which had much less data. In production, building the index blocked writes to that table for several minutes during the deploy, and some requests timed out. Cancelling would only have rolled it back and left the release half done, so we let it finish, posted an update to the team, and watched the error rate fall back. Afterwards I rewrote our approach: on PostgreSQL we use AddIndexConcurrently in a non-atomic migration for big tables, and I added a pull request checklist item to paste the sqlmigrate output for any migration touching a large table. We also started refreshing staging with production-sized, anonymised data so a slow migration shows up before release."

Red flag to avoid:

Blaming the database or a teammate, or a story where nothing about the process changed.

They may ask next:
  • How would you roll back a migration that has already run in production?
  • How do you handle two branches that each add a migration to the same app?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

31. Tell me about upgrading a project to a newer Django version. How did you plan it and keep it from breaking things?

What the interviewer is really testing:
Whether you upgrade in safe, planned steps, rather than bumping the version and hoping the tests catch everything.
Answer frame:

Prepare: read the release notes and clear deprecation warnings on the current version first.

Dependencies: check that third-party apps support the target version.

Roll out: move one version step at a time, run the full test suite, and test on staging.

Sample spoken answer:

"At my last company we were two major versions behind, and security fixes for our version were about to stop. I started by running the test suite with deprecation warnings turned on, because most breaking changes are announced as warnings in earlier versions. We fixed those while still on the old version, which made the actual upgrade much smaller. Then I listed our third-party packages and checked each one's supported Django versions; two needed upgrades and one was abandoned, so we replaced it. Rather than jumping straight to the latest, we moved one version at a time, each on its own branch, running the tests and reading the release notes for anything our tests didn't cover, like changed defaults. We deployed each step to staging and did a manual pass on login, payments and the admin. The final production release was uneventful, which was the goal."

Red flag to avoid:

Upgrading several major versions in one jump with no plan, or skipping the release notes.

They may ask next:
  • What would you do if a critical third-party app didn't support the new version?
  • How do you convince a team to spend time on an upgrade when nothing is visibly broken?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card