Django interviews focus on the framework's conventions and the decisions that matter in production. Expect questions on the request lifecycle and middleware ordering, the ORM and the N+1 problem, migrations on large tables, forms and validation, the authentication and permission system, and caching. Security is a recurring theme, particularly what Django protects you from by default and what remains your responsibility. The questions below cover both the framework mechanics and the operational judgement interviewers look for.
Behavioural Questions
1. Tell me about a Django project you have worked on. What was your role and what was the hardest part?
Note: Interviewers are trying to work out whether you have shipped Django or only followed a tutorial. Details about deployment, migrations, and data volume are what separate the two.
Structure it in four parts:
- What the application did, and its scale. Number of users, size of the largest table, request volume at peak. One line is enough, but the numbers matter.
- Your actual scope. Whether you owned models and migrations, the API layer, background tasks with Celery, or the deployment. Be precise — inflating this is easy to catch with a follow-up question.
- The genuinely hard problem. Strong candidates: a migration on a large table that could not lock it, N+1 queries discovered under load, a permissions model that grew beyond what Django's built-in system handled, or moving slow work into a task queue.
- The outcome and what you would redo.
2. How do you decide between using a third-party Django package and writing the functionality yourself?
Treat it as a cost decision. A Django package is not free — it constrains your upgrade path, and an abandoned package can hold you on an old Django version for years.
Use a package when: the problem is well-defined, widely solved, and dangerous to get wrong. Django REST Framework, django-allauth for social login, Celery for task queues, and django-storages for object storage all qualify — you would not want to reimplement any of them.
Write it yourself when: you need a fraction of what it does, or when the package imposes an abstraction that fights your models.
What to check before adopting:
- Does it declare support for your Django and Python versions, and how quickly did it support the last release?
- How many open issues, and when was the last commit?
- Does it add migrations or middleware to your project — those are hard to back out of.
Note: Django's own contrib apps should be your first stop. Candidates regularly reach for a package for something the framework already does.
3. Describe a time you had to deploy a change to a live Django application. How did you manage the risk?
The interesting part of this question is migrations, so lead with them.
- Separate schema changes from code changes. The safe pattern is additive first: add the new nullable column, deploy code that writes to both old and new, backfill, then deploy code that reads from the new one, and only then drop the old column. Each step is independently reversible.
- Know what locks. Adding a nullable column is cheap on modern PostgreSQL; adding one with a default, or adding an index without
CONCURRENTLY, can lock a large table for minutes. - Have a rollback plan for both code and schema. A migration that cannot be reversed needs to be flagged before it runs, not after.
The rest of the risk management: deploying behind a feature flag, running python manage.py check --deploy, taking a database backup immediately before, and watching error rates for the first fifteen minutes rather than walking away.
Note: If you have ever had to roll back a migration in production, tell that story. It is far more convincing than describing an ideal process.
4. How do you approach testing in a Django project, and how much testing is enough?
Give a hierarchy rather than a coverage number, because coverage percentage is a weak proxy and interviewers know it.
- Model and business logic tests are the highest value per line. They are fast, they do not need the HTTP layer, and they catch the bugs that corrupt data.
- View and API tests using Django's test client or DRF's
APIClient, focused on permissions and status codes. Authorisation bugs are the ones that actually hurt, so test that the wrong user gets a 403. - Integration tests for the few flows that must never break — signup, checkout, payment.
- End-to-end browser tests sparingly. They are slow and flaky, so reserve them for a handful of critical journeys.
On "how much": enough that you can deploy on a Friday. Concretely, that means every bug fix arrives with a regression test, anything involving money or permissions is covered, and the suite runs fast enough that people actually run it.
Note: Mention factory_boy over fixtures, and pytest-django if you use it. Both signal practical experience.
5. How do you handle working on a legacy Django codebase with old patterns or an outdated version?
Show that you can upgrade incrementally rather than proposing a rewrite, since the rewrite answer is what most interviewers are screening against.
- Get the tests and the pipeline working first. You cannot safely change anything without a way to know you broke it. If there are no tests, add characterisation tests around the areas you must touch.
- Upgrade one minor version at a time. Run with
-W error::DeprecationWarningto surface what the next version will remove, fix those, then step forward. Jumping several versions at once turns a series of small problems into one unsolvable one. - Prioritise by risk, not by ugliness. An unsupported Django version with open security advisories is urgent; a fat views file is not.
- Improve code you are already touching. Refactoring in the course of feature work is far easier to justify than a standalone cleanup ticket.
Note: Be explicit that you would document the upgrade path and its blockers before starting. Legacy work fails most often through lack of a plan, not lack of skill.
Technical Questions
1. Explain Django's MTV architecture and how a request flows through it.
Django calls its pattern MTV — Model, Template, View — which maps onto MVC with the names shifted. Django's View is MVC's controller, and Django's Template is MVC's view. The framework itself plays the controller role.
The flow of a request:
- The WSGI or ASGI server hands the request to Django, which wraps it in an
HttpRequest. - Middleware runs top to bottom. Each layer can inspect or modify the request, or short-circuit it entirely — this is where sessions, authentication, and CSRF checks happen.
- URL resolution matches the path against
urlpatternsand extracts any captured arguments. - The view runs. It talks to models through the ORM, applies business logic, and returns an
HttpResponse. - If it rendered a template, the template engine produces the HTML.
- Middleware runs again in reverse order on the way out, then the response is returned.
Note: The reverse ordering of middleware on the response is a common follow-up. It matters because a middleware that sets a header on the way out sees the response after everything registered below it.
2. What is the Django ORM, and what is the N+1 query problem?
The ORM maps Python classes to database tables so you write Book.objects.filter(author__name='X') instead of SQL. QuerySets are lazy — nothing hits the database until you iterate, slice, or call something like len() — and they are cached once evaluated.
The N+1 problem is the most common Django performance bug. This code runs one query for the books and then one more per book:
for book in Book.objects.all(): # 1 query
print(book.author.name) # 1 query each timeA hundred books means a hundred and one queries.
The two fixes:
select_related('author')for forward ForeignKey and OneToOne relations. It performs a SQL JOIN and returns everything in one query.prefetch_related('tags')for ManyToMany and reverse ForeignKey relations. It runs a second query and joins the results in Python, because a JOIN would multiply rows.
Note: Say how you would find it, not just how to fix it. django-debug-toolbar shows the query count per page, and assertNumQueries in a test stops it coming back.
3. What are Django migrations, and how do you handle a migration that would lock a large table?
Migrations are versioned, ordered Python files describing schema changes. makemigrations compares your models to the existing migration state and generates the difference; migrate applies it and records the result in django_migrations.
The commands worth knowing: sqlmigrate shows the SQL a migration will run — always read it before applying anything to a large table. showmigrations lists what has been applied. --fake marks a migration applied without running it, for when the schema is already correct.
For a large table, the danger is the lock. The safe approach:
- Add columns as nullable with no default. On PostgreSQL a nullable column with no default is a metadata-only change. Adding a NOT NULL column with a default rewrites the whole table on older versions.
- Backfill in batches in a separate data migration or management command, not inside the schema migration.
- Create indexes concurrently. Use
AddIndexConcurrentlyfromdjango.contrib.postgres.operationswithatomic = Falseon the migration. - Split add-and-populate into separate deploys so old and new code can both run against the schema during the rollout.
Note: Setting a lock_timeout so a migration fails fast rather than queueing behind traffic is a strong detail to mention.
4. What is the difference between a Django project and an app, and how should you structure a large codebase?
A project is the deployable unit — settings, root URL configuration, WSGI and ASGI entry points. An app is a self-contained module of functionality with its own models, views, and migrations. One project contains many apps, and a well-designed app is reusable across projects.
How to draw app boundaries: by domain, not by layer. An app called orders containing its own models, views, and services is right; apps called models, views, and forms are wrong — that is just the framework's structure repeated at a larger scale.
What changes as a codebase grows:
- Split settings into
base,development, andproduction, with secrets from the environment rather than the repository. - Move logic out of views. Fat views are the most common structural problem in Django. Push business rules into model methods, managers, or a
services.pyso they are testable without HTTP. - Use custom managers and QuerySets so query logic lives in one place —
Order.objects.pending()beats the same filter repeated in nine views. - Watch for circular imports between apps. If two apps import each other's models, the boundary is wrong.
5. How does Django's authentication system work, and what is the difference between authentication and authorisation?
Authentication establishes who you are; authorisation decides what you may do. Django provides both.
Authentication: authenticate() runs the configured backends and returns a user or None. login() puts the user's id in the session. AuthenticationMiddleware then attaches request.user on every subsequent request, falling back to AnonymousUser. Passwords are stored using PBKDF2 by default, salted and iterated, and the hasher is upgraded transparently on the next successful login.
Authorisation:
- Permissions — Django creates add, change, delete, and view permissions per model automatically. Check them with
user.has_perm('app.change_order'). - Groups — named collections of permissions, so you assign a role rather than individual rights.
- Enforcement — the
@login_requiredand@permission_requireddecorators, orLoginRequiredMixinandPermissionRequiredMixinon class-based views.
Note: Django's model-level permissions do not cover object-level rules such as "only the author may edit this post". Say that you would implement that in the view or with a package like django-guardian — knowing the limitation matters more than knowing the API.
6. What is middleware in Django, and how would you write your own?
Middleware is a chain of hooks that wraps every request and response. Each one receives the request, may act on it, calls the next layer, and then may act on the response coming back.
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response # runs once at startup
def __call__(self, request):
start = time.monotonic()
response = self.get_response(request) # everything below runs here
response['X-Duration-Ms'] = f"{(time.monotonic() - start) * 1000:.1f}"
return responseAdd the dotted path to MIDDLEWARE in settings.
The rules:
- Order matters, and it is asymmetric. Requests pass through the list top to bottom; responses come back bottom to top.
- Returning a response without calling
get_responseshort-circuits everything below — which is exactly how authentication redirects and rate limiters work. - Placement has consequences.
SessionMiddlewaremust come beforeAuthenticationMiddleware, because authentication reads the session.
Note: Good uses are cross-cutting concerns: request logging, correlation ids, timing, locale, and enforcing a maintenance mode. Business logic does not belong here, because it runs on every single request including static files and health checks.
7. What is the difference between a Django Form and a ModelForm, and how does validation work?
A Form defines its fields explicitly and is not tied to a model — a search box or a contact form. A ModelForm generates its fields from a model, and gains a save() method that creates or updates the instance.
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['title', 'author', 'published_on']Note: Always list fields explicitly. Using __all__ means that any field added to the model later becomes editable through this form, which is a real and frequently exploited security hole.
Validation runs in a fixed order when you call is_valid():
to_pythonconverts the raw string to the right Python type.validateapplies field-level rules such as required and choices.- Validators attached to the field run next.
clean_<fieldname>— your per-field logic. It must return the cleaned value.clean()— form-wide logic, the only place you can compare two fields, such as checking that an end date is after a start date. RaiseValidationErrorhere for cross-field errors.
Errors collect in form.errors rather than raising, so all problems are shown to the user at once.
8. How do you handle background or long-running tasks in Django?
Never do them in the request-response cycle. Sending an email, generating a report, or calling a slow third-party API inside a view holds a worker process hostage and eventually times out for the user.
The standard answer is Celery with a broker such as Redis or RabbitMQ:
@shared_task(bind=True, max_retries=3)
def send_invoice(self, order_id):
order = Order.objects.get(pk=order_id)
try:
mailer.send(order.email, render_invoice(order))
except TransientError as exc:
raise self.retry(exc=exc, countdown=60)The view calls send_invoice.delay(order.id) and returns immediately.
The practices that matter:
- Pass ids, not objects. Serialising a model instance means the worker acts on a stale copy.
- Make tasks idempotent. A task can be delivered twice; running it twice must not send two invoices.
- Set retries with backoff, and a time limit, so a hung task does not occupy a worker forever.
- Dispatch after the transaction commits —
transaction.on_commit(lambda: task.delay(id))— or the worker may look up a row that has not been written yet. This is a classic race that is hard to reproduce.
Note: For simpler needs, mention django-q or a management command run by cron. Celery is powerful but it is real operational overhead.
9. How does caching work in Django, and what are the different caching levels?
Django exposes several granularities, and choosing the right one matters more than choosing the backend.
- Per-site caching —
UpdateCacheMiddlewareandFetchFromCacheMiddlewarecache entire pages. Simple, but wrong for anything personalised. - Per-view caching —
@cache_page(60 * 15)on a specific view. Good for public pages such as a listing or a blog post. - Template fragment caching —
{% cache 300 sidebar request.user.id %}around an expensive block, keyed by whatever makes it vary. This is often the best trade-off for a page that is mostly dynamic with one costly component. - The low-level API —
cache.get,cache.set, andcache.get_or_setfor caching a specific computation or query result. The most control and usually the right answer.
Backends: Redis or Memcached in production, local-memory for development, and dummy for tests.
The hard part is invalidation. Options are a short timeout and tolerating staleness, deleting the key in a post_save signal, or including a version marker in the cache key so a new value simply produces a new key and the old one expires on its own. The last approach avoids the race conditions the other two have.
10. What security protections does Django provide out of the box, and what must you still do yourself?
What Django gives you by default:
- SQL injection — the ORM parameterises every query. You lose that protection the moment you use
raw()orextra()with string formatting. - XSS — the template engine autoescapes variables. You lose it with the
|safefilter ormark_safe. - CSRF —
CsrfViewMiddlewareplus{% csrf_token %}in every POST form. - Clickjacking —
XFrameOptionsMiddlewaresetsX-Frame-Options: DENY. - Password storage — PBKDF2 with salting, and configurable validators.
What is still your job:
DEBUG = Falsein production, andALLOWED_HOSTSset. Leaving DEBUG on exposes settings, environment variables, and a full traceback with source.- Keep
SECRET_KEYout of the repository. - Set
SECURE_SSL_REDIRECT,SESSION_COOKIE_SECURE,CSRF_COOKIE_SECURE, and HSTS. - Object-level authorisation. Django checks that a user may edit some order, not this order. Every candidate should say this — it is where real breaches happen.
- Rate limit login endpoints, validate uploaded files, and keep dependencies patched.
Note: Run python manage.py check --deploy. It audits most of the settings above and is an easy, concrete thing to cite.





