Django Interview Questions and Answers for Experienced Developers

Django interview questions for experienced professionals in 2026: MVT lifecycle, ORM, migrations, middleware, DRF, security, Celery, async Django, and senior production scenario answers.

Published

Updated

Tech reviewed byDeepak Prasad

Django Interview Questions and Answers for Experienced Developers

Django interview questions for experienced candidates go well past "what is MVT?" Hiring managers want proof you have operated production systems—fixed N+1 queries hiding inside DRF serializers, rolled out migrations without locking a million-row table, and understood why transaction.on_commit matters before enqueueing Celery work. Loops for experienced professionals and Python Django backend roles in 2026 blend ORM depth, Django REST Framework design, security defaults, and operational judgment (caching, workers, ASGI limits).

Below are 45 questions with elaborate answers; technical sections include a strong answer sample you can say aloud. Pair this guide with Python developer interview questions for language fundamentals, PostgreSQL interview questions for Django's default database engine depth, full stack developer interviews for React-to-Django integration, SQL technical interviews when query plans matter, and Flask SQLAlchemy if interviewers compare lightweight Flask patterns.

NOTE
Prep target: Experienced loops favor scenario depth—trace a request through middleware, defend select_related vs prefetch_related, explain object-level permissions, and describe a zero-downtime migration or incident you resolved with query logging.

Tested on: Ubuntu 25.04 (Plucky Puffin); kernel 6.14.0-37-generic; Python 3.13.3.


Interview context and how to prepare

What do Django interviews test for experienced developers?

Django interviews for experienced roles test whether you can build and operate web applications—not recite admin registration steps from memory.

Layer What interviewers probe
Request pipeline URLs, middleware, views, templates/serializers
ORM Relationships, query counts, transactions, constraints
Migrations Safe rollout, squashing, backward compatibility
Security CSRF, XSS, auth, object-level permissions
APIs (DRF) Serializers, validation, throttling, versioning
Production Caching, Celery, settings split, check --deploy
Architecture Service layer, app boundaries, when not to use signals
Experience What changes
Mid (3–5 yrs) CRUD, ORM basics, DRF serializers
Senior (5–8 yrs) N+1, transactions, permissions, testing strategy
Staff / 8+ yrs Large-project structure, migration ops, multi-tenant patterns
Django developer vs Python developer — what is the extra bar?

A Python developer loop may stay language-heavy: decorators, GIL, asyncio, general algorithms.

A Django developer loop assumes solid Python and adds framework production depth:

Topic Python-only loop Django experienced loop
Web Maybe Flask/FastAPI sketch Full MVT/DRF lifecycle
Data SQL strings or generic ORM Migrations, managers, F()/Q()
Concurrency asyncio basics ASGI limits, Celery, on_commit
Security Generic OWASP awareness CSRF middleware, ALLOWED_HOSTS, DRF auth
Testing pytest pytest-django, assertNumQueries

See Python developer interviews for language prep; return here for Django-specific depth.

What is a typical Django interview loop for experienced professionals?
Round Duration Focus
Recruiter / HM 30 min Projects, stack (DRF, Celery, Postgres), team size
Python fundamentals 45 min Often overlaps Python prep
Django deep dive 60–90 min ORM, middleware, security, DRF
Live exercise 45–90 min Write view/serializer, fix N+1, design models
System design 45–60 min Monolith modules, workers, caching—senior roles
Behavioral 30–45 min Incidents, code review, mentoring

Take-home tasks may ask for a small DRF API with tests, pagination, and a README explaining trade-offs.

What is a realistic 4–6 week prep plan?
Week Focus Output
1 Request pipeline — URLs, middleware, settings Diagram request → response; list your project's middleware
2 ORM — relationships, select_related, prefetch_related Fix one N+1; use assertNumQueries in a test
3 DRF — serializers, permissions, pagination Build CRUD API with object-level checks
4 Migrations & transactions — atomic, on_commit, select_for_update Document one safe migration rollout
5 Security & deployment — check --deploy, secrets, caching Harden settings checklist for a sample project
6 Scenarios + STAR stories Rehearse slow API debug and failed Celery recovery

Keep one reference Django project (blog, orders API, or internal tool) you can whiteboard.


MVT architecture and the request lifecycle

Explain MVT architecture. How is it different from MVC?

Django follows MVT: Model–View–Template.

Layer Role
Model Data layer: database tables, fields, relationships, ORM queries
View Request handler: receives request, applies business logic, returns response
Template Presentation layer: renders HTML shown to the user

The confusing part is the word View.

In many MVC frameworks, the Controller handles the request. In Django, that controller-like responsibility is handled by the view.

MVC term Rough Django equivalent
Model Model
View Template
Controller View

Typical flow:

text
URL pattern
→ Django view
→ model/service/ORM
→ template or JSON response

Example:

python
# urls.py
urlpatterns = [
    path("orders/<int:pk>/", views.order_detail, name="order-detail"),
]
python
# views.py
def order_detail(request, pk):
    order = Order.objects.get(pk=pk)
    return render(request, "orders/detail.html", {"order": order})

For API-only Django apps, templates may not be used much. A Django REST Framework app may use serializers and Response objects instead of HTML templates, but the high-level pipeline is still request → URL → view → response.

Important interview nuance:

  • Models should not become a dumping ground for all business logic
  • Views should not become huge “God functions”
  • Complex business workflows often belong in service/domain layers
  • Templates should avoid heavy database work

A strong answer is:

“Django’s MVT maps closely to MVC, but Django’s view acts like the controller. Models represent data, views handle request logic, and templates render presentation. For APIs, serializers often replace templates as the output-shaping layer.”

Walk through the Django request lifecycle step by step.

A weak answer says “URL goes to a view.” A strong answer traces the full request path.

Typical Django request lifecycle:

  1. Web server receives the HTTP request
  2. WSGI or ASGI server passes it to Django
  3. Request enters middleware in the order listed in settings.MIDDLEWARE
  4. URL resolver matches the path using ROOT_URLCONF
  5. Django calls the matched view
  6. View runs application logic, ORM queries, services, permissions, or forms
  7. View returns an HttpResponse, JsonResponse, redirect, or DRF Response
  8. Response travels back through middleware in reverse order
  9. Server sends the final HTTP response to the client

Flow:

text
Client
→ web server
→ WSGI/ASGI server
→ middleware request phase
→ URL resolver
→ view
→ response object
→ middleware response phase
→ client

Example URL route:

python
urlpatterns = [
    path("orders/<int:pk>/", OrderDetailView.as_view(), name="order-detail"),
]

Important middleware order example:

python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
]

Why order matters:

Area Why order matters
Sessions Auth middleware depends on session middleware
Authentication request.user is attached before views need it
CSRF Unsafe methods can be rejected before view logic
Security headers Responses can be modified globally
Custom tenant middleware Tenant may need to be resolved before view/service logic

Debugging examples:

Symptom Check
request.user is missing/wrong AuthenticationMiddleware, session middleware order
CSRF failure before view CsrfViewMiddleware, token, trusted origins
View not called URL pattern, middleware short-circuit, permissions
Wrong tenant/database Tenant middleware order
Response header missing Middleware ordering or early return

A strong answer is:

“A request enters Django through WSGI or ASGI, passes through middleware, gets routed by URLconf to a view, and the response unwinds through middleware in reverse order. Middleware order matters for sessions, auth, CSRF, security, and custom cross-cutting logic.”

What is middleware and what are common production uses?

Middleware is Django’s global request/response hook system.

Each middleware wraps the view layer. It can:

  • Inspect or modify the request before the view runs
  • Return a response early without calling the view
  • Inspect or modify the response after the view runs
  • Handle cross-cutting concerns consistently

Common built-in middleware:

Middleware Purpose
SecurityMiddleware Security-related headers and SSL redirects
SessionMiddleware Adds session support
AuthenticationMiddleware Adds request.user
CsrfViewMiddleware Protects unsafe requests from CSRF
CommonMiddleware Common URL handling behavior
GZipMiddleware Compresses responses when appropriate

Custom production middleware examples:

Use case Example
Request tracing Add X-Request-ID or correlation ID
Metrics Measure request duration/status codes
Multi-tenancy Resolve tenant from host/header/path
Feature flags Attach rollout flags to request
Security Add custom headers or block suspicious requests
Auditing Log user/IP/action metadata
Maintenance mode Short-circuit with 503 response

Simple middleware shape:

python
class RequestIDMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        request.request_id = generate_request_id()

        response = self.get_response(request)

        response["X-Request-ID"] = request.request_id
        return response

Middleware behaves like an onion:

text
middleware A request phase
  middleware B request phase
    view
  middleware B response phase
middleware A response phase

Common mistakes:

  • Putting middleware in the wrong order
  • Doing heavy database queries for every request
  • Swallowing exceptions without logging
  • Adding tenant/auth logic too late
  • Using middleware for feature-specific business logic
  • Forgetting async compatibility in ASGI-heavy apps

Middleware should be used for cross-cutting concerns, not normal view-specific logic.

A strong answer is:

“Middleware wraps Django’s request/response cycle. I use it for cross-cutting concerns like security, sessions, auth, tracing, metrics, tenant resolution, and request IDs, while keeping feature-specific business logic in views or services.”

WSGI vs ASGI — when does Django use each?

WSGI and ASGI are server interfaces used to run Django applications.

Interface Model Typical server Best for
WSGI Synchronous request/response Gunicorn, uWSGI Traditional Django apps
ASGI Async-capable protocol interface Uvicorn, Daphne, Hypercorn Async views, websockets, long-lived connections

WSGI is enough for many Django apps because most traditional Django work is database-heavy and synchronous.

ASGI is useful when the app needs:

  • Async views
  • WebSockets
  • Long-polling
  • Server-sent events
  • Many concurrent external API calls
  • Django Channels
  • Mixed HTTP and async protocols

Example async view:

python
async def health_check(request):
    data = await call_external_service()
    return JsonResponse({"status": data})

Important ORM warning:

Django supports async views, but you must be careful with synchronous ORM/database work inside async code.

Risky pattern:

python
async def user_detail(request, pk):
    user = User.objects.get(pk=pk)  # unsafe in async context
    return JsonResponse({"name": user.name})

Safer options:

  • Keep ORM-heavy views synchronous
  • Use async ORM methods where supported
  • Use sync_to_async() carefully for sync ORM work
  • Avoid wrapping huge DB-heavy code in async just for trendiness

Example bridge:

python
from asgiref.sync import sync_to_async

async def user_detail(request, pk):
    user = await sync_to_async(User.objects.get)(pk=pk)
    return JsonResponse({"name": user.name})

Interview nuance:

Scenario Better choice
Normal CRUD app with ORM-heavy views WSGI or sync views
API calls multiple external services concurrently ASGI + async views can help
WebSockets/chat/live notifications ASGI + Channels
Heavy CPU work Background worker, not async view
Long-running job Celery/RQ/background task

Do not say “ASGI is always faster.” It helps when you actually have non-blocking I/O. If your view mostly blocks on the synchronous ORM, async may add complexity without improving throughput.

A strong answer is:

“I use WSGI or sync views for typical ORM-heavy Django apps. I use ASGI when I need async I/O, WebSockets, or long-lived connections. I avoid calling blocking ORM code directly from async views unless I use the proper async APIs or bridging.”


Models, ORM, and migrations

How does the Django ORM and QuerySet lazy evaluation work?

Django models map Python classes to database tables, and the ORM lets you build SQL queries using Python objects.

A QuerySet is lazy. It represents a database query, but it usually does not hit the database until it is evaluated.

Example:

python
qs = Order.objects.filter(status="PAID").order_by("-created_at")

At this point, Django has built a query, but it has not necessarily executed SQL yet.

Common QuerySet evaluation triggers:

Action Effect
Iteration Executes query
list(qs) Executes query and loads all rows
len(qs) Evaluates results; use count() for count query
bool(qs) Evaluates existence; use exists() for existence check
qs[0] Executes query with limit/offset
repr(qs) in shell May evaluate for display
Template loop Executes query while rendering
Serialization Often evaluates the QuerySet

Example:

python
# Lazy
orders = Order.objects.filter(status="PAID")

# Evaluates
for order in orders:
    print(order.id)

QuerySets are chainable:

python
orders = (
    Order.objects
    .filter(status="PAID")
    .filter(total__gte=100)
    .select_related("customer")
)

Important caching behavior:

  • The same evaluated QuerySet caches its results
  • A new QuerySet clone runs a new query
  • Calling .all() creates a new QuerySet
  • Related manager calls can trigger extra queries
  • Lazy evaluation can hide N+1 query bugs

Better count/existence patterns:

python
Order.objects.filter(status="PAID").count()
Order.objects.filter(status="PAID").exists()

Instead of:

python
len(Order.objects.filter(status="PAID"))
bool(Order.objects.filter(status="PAID"))

Interviewers like candidates who can connect QuerySet laziness to performance debugging.

Useful tools:

  • Django Debug Toolbar
  • connection.queries in development
  • assertNumQueries() in tests
  • Database logs
  • QuerySet.explain() for query plans

A strong answer is:

“A QuerySet is a lazy, chainable SQL query description. It hits the database only when evaluated, so I pay attention to loops, serializers, templates, len(), bool(), and related-object access that can trigger hidden queries.”

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

null=True and blank=True work at different layers.

Option Layer Meaning
null=True Database Column can store SQL NULL
blank=True Validation/forms Field is allowed to be empty in forms/model validation

Example:

python
class Profile(models.Model):
    bio = models.TextField(blank=True)
    birth_date = models.DateField(null=True, blank=True)

Common patterns:

Field Common choice Why
Required string default null=False, blank=False Must be provided
Optional string blank=True, default="" Avoid two empty values
Optional date null=True, blank=True No date is naturally NULL
Optional FK null=True, blank=True Relationship may be missing
Boolean Avoid nullable unless tri-state needed True/False/Unknown only when required

Why avoid null=True on strings?

Because you create two ways to represent “empty”:

text
NULL
""

That can complicate filters, uniqueness, validation, and API behavior.

Example problem:

python
Customer.objects.filter(nickname="")
Customer.objects.filter(nickname__isnull=True)

Now both may mean “no nickname.”

Use blank=True when form/admin/serializer validation should accept an empty value.

Use null=True when the database needs to represent missing value as SQL NULL.

Important nuance: Django REST Framework serializers have their own options such as allow_blank and allow_null, but the same idea applies: blank string and null are different states.

A strong answer is:

null=True controls database NULL storage, while blank=True controls validation. I usually avoid null=True on string fields so I do not end up with both NULL and empty string representing the same thing.”

How do F() and Q() objects help in Django queries?

F() and Q() help push more logic into SQL instead of doing it in Python loops.

F() references a model field in the database.

Use it for:

  • Atomic increments/decrements
  • Comparing two columns
  • Updating one field based on another
  • Avoiding race-prone read-modify-write code

Example stock update:

python
from django.db.models import F

Product.objects.filter(pk=product_id, stock__gt=0).update(
    stock=F("stock") - 1
)

This is safer than:

python
product = Product.objects.get(pk=product_id)
product.stock -= 1
product.save()

The second version can lose updates under concurrency.

Q() builds complex boolean conditions.

Use it for:

  • OR conditions
  • NOT conditions
  • Dynamic filters
  • Combining optional search filters

Example:

python
from django.db.models import Q

orders = Order.objects.filter(
    Q(status="PAID") | Q(status="SHIPPED"),
    total__gte=100,
)

Negation:

python
Order.objects.filter(~Q(status="CANCELLED"))

Dynamic filter example:

python
query = Q()

if status:
    query &= Q(status=status)

if search:
    query &= Q(customer__name__icontains=search) | Q(reference__icontains=search)

orders = Order.objects.filter(query)

Comparison:

Tool Use
F() Field references and atomic DB-side updates
Q() Complex WHERE conditions
annotate() Add computed values to rows
Case/When Conditional SQL expressions
select_for_update() Lock rows during a transaction

Important concurrency note:

F() helps avoid lost updates for simple field updates, but complex business transactions may still need transaction.atomic() and select_for_update().

A strong answer is:

F() lets the database update or compare fields atomically without pulling values into Python. Q() lets me build OR, NOT, and dynamic filters cleanly. Both help keep filtering and updates in SQL.”

Explain transaction.atomic, on_commit, and select_for_update.

Transactions make multi-step database operations safe.

transaction.atomic() creates an all-or-nothing block:

python
from django.db import transaction

with transaction.atomic():
    order = Order.objects.create(customer=customer)
    Payment.objects.create(order=order, amount=order.total)

If an exception occurs inside the block, Django rolls back the transaction.

Use transactions for:

  • Money transfer
  • Inventory decrement + order creation
  • Multi-table writes
  • State transitions
  • Operations that must not be partially saved

select_for_update() locks selected rows until the transaction finishes.

Example:

python
@transaction.atomic
def reserve_stock(product_id, quantity):
    product = (
        Product.objects
        .select_for_update()
        .get(pk=product_id)
    )

    if product.stock < quantity:
        raise OutOfStock()

    product.stock -= quantity
    product.save(update_fields=["stock"])

This prevents two requests from reading the same stock value and overselling.

transaction.on_commit() runs a callback only after the outer transaction successfully commits.

Use it for side effects:

  • Send email
  • Enqueue Celery task
  • Invalidate cache
  • Publish event
  • Call external service

Example:

python
def create_order(request):
    with transaction.atomic():
        order = Order.objects.create(...)

        transaction.on_commit(
            lambda: send_receipt.delay(order.id)
        )

Why this matters:

If you enqueue a Celery task before commit, the worker may run before the row is committed and fail to find the object.

Bad pattern:

python
with transaction.atomic():
    order = Order.objects.create(...)
    send_receipt.delay(order.id)  # worker may run too early

Good pattern:

python
with transaction.atomic():
    order = Order.objects.create(...)
    transaction.on_commit(lambda: send_receipt.delay(order.id))

Important interview nuance:

  • Keep transactions short
  • Avoid slow network calls inside transactions
  • Lock rows in a consistent order to reduce deadlocks
  • Use database constraints too, not only application checks
  • select_for_update() behavior depends on database support and isolation level
  • Catch database exceptions outside the atomic block when possible

A strong answer is:

“I use atomic() for all-or-nothing writes, select_for_update() to lock rows when concurrent updates can race, and on_commit() for side effects like Celery tasks so workers only see committed data.”

How do Django migrations work and how do you deploy them safely?

Django migrations are versioned database schema changes.

They are generated from model changes and stored as Python files in each app’s migrations/ directory.

Common commands:

Command Use
makemigrations Create migration files from model changes
migrate Apply unapplied migrations to the database
showmigrations Show migration status
sqlmigrate Show SQL for a migration
migrate --plan Preview migration plan
makemigrations --check --dry-run CI check for missing migrations

Example:

bash
python manage.py makemigrations
python manage.py migrate
python manage.py showmigrations
python manage.py migrate --plan

Migrations are like version control for schema. They should be reviewed, tested, and deployed carefully.

Safe production migration principles:

Practice Why
Backward-compatible changes Old and new code may run during deploy
Expand-contract pattern Avoid breaking rolling deployments
Avoid long locks Large tables can block writes/reads
Backfill in batches Prevent long transactions and load spikes
Add indexes carefully Large indexes can lock or slow production
Separate schema and data migration Easier rollback and timing control
Test on production-sized data Small staging DB may hide lock problems
Keep migrations deterministic Avoid environment-specific surprises

Expand-contract example:

text
1. Add nullable column
2. Deploy code that writes both old and new fields
3. Backfill old rows in batches
4. Deploy code that reads new field
5. Add NOT NULL/unique constraint later
6. Remove old column after safe window

Risky migration examples:

  • Add non-null field with default on huge table
  • Rename column while old code still reads old name
  • Drop column before all app versions stop using it
  • Run large data migration in one transaction
  • Add index during peak traffic
  • Run migration without backup/rollback plan

Useful CI checks:

bash
python manage.py makemigrations --check --dry-run
python manage.py migrate --plan
python manage.py test

Operational advice:

  • Run migrations before or during deploy based on compatibility
  • Make migrations safe for rolling deploys
  • Coordinate app and worker releases
  • Check migration duration in staging
  • Monitor DB locks, errors, and latency
  • Keep backups before risky schema changes

Common interview mistake:

“Just run migrate before deployment.”

For small apps, that may be fine. For large production tables, safe migration design matters more than the command.

A strong answer is:

“Django migrations are versioned schema changes. I deploy them safely with backward-compatible steps, expand-contract changes, batched backfills, lock-aware operations, and CI checks like makemigrations --check --dry-run and migrate --plan.”


Views, URLs, and templates

Function-based views vs class-based views — when do you use each?
Style Pros Cons
FBV Explicit, easy to read, great for small endpoints Repetition without decorators
CBV Reuse via mixins (ListView, CreateView) Harder to trace for juniors
DRF ViewSets Router registration, consistent CRUD Magic if team does not know DRF

Experienced guidance:

  • FBV for one-off admin tools or complex branching
  • CBV/ViewSets for consistent CRUD with permissions and pagination
  • Keep views thin—move rules to services or selectors
python
class OrderListView(ListView):
    model = Order
    paginate_by = 25

A strong answer is:

I pick FBVs for clarity on small endpoints and DRF ViewSets for API resources, but business logic lives in services—not in view methods regardless of style.

How does URL routing work with path, include, and namespaces?

ROOT_URLCONF points to your root urls.py. Patterns map paths to views:

python
from django.urls import path, include

urlpatterns = [
    path("api/v1/orders/", include("orders.urls", namespace="orders")),
    path("health/", health_check),
]
Feature Use
path() / re_path() Match URL to view
include() Delegate to app urls
namespace + app_name Reverse URLs without collision — reverse("orders:detail", kwargs={"pk": 1})
Named groups Capture typed parameters

API versioning: URL prefix (/api/v1/), Accept header, or subdomain—pick one and document for clients.

A strong answer is:

URLs are explicit maps from paths to callables; I use include and namespaces so apps stay modular and reverse() stays stable when paths change.

How do Django templates work and how do you avoid XSS?

Django templates use auto-escaping by default—HTML from variables is escaped unless marked safe.

Construct Role
{{ variable }} Output escaped HTML
{% for %}, {% if %} Control flow
{% url %} Reverse named routes
{% csrf_token %} CSRF token in forms
{% extends %} / {% block %} Layout inheritance

XSS risk returns when you use |safe or mark_safe() on user content—interviewers want you to justify every safe mark.

For SPAs, templates matter less; DRF serializers become the presentation layer—validation and field exposure rules apply there instead.

A strong answer is:

Templates auto-escape output; I never mark user-controlled strings safe, and I keep presentation in templates or serializers—not mixed into ORM models.

What is the service layer pattern in large Django projects?

As projects grow, fat views and fat models become hard to test. A service layer (or selectors for reads) centralizes business rules:

text
views.py      → HTTP, permissions, input/output
services.py   → business transactions, orchestration
selectors.py  → complex read queries (optional)
models.py     → fields, constraints, small methods

Benefits:

  • One place for transaction.atomic() and domain rules
  • Reuse from management commands, Celery tasks, and admin actions
  • Unit test without HTTP request factory

Anti-pattern: hiding business logic in signals—hard to trace; prefer explicit service calls.

A strong answer is:

Views stay thin; services own transactions and domain rules so the same logic runs from APIs, tasks, and management commands with one test suite.


Middleware, security, and deployment settings

How does Django protect against CSRF, XSS, and SQL injection?
Threat Django mechanism
SQL injection ORM parameterizes queries; raw SQL needs careful params
XSS Template auto-escaping; cautious `
CSRF CsrfViewMiddleware + token on unsafe methods
Clickjacking X-Frame-Options middleware
Host header attacks ALLOWED_HOSTS
Session hijacking SESSION_COOKIE_SECURE, HttpOnly, rotation

Deployment checklist:

bash
python manage.py check --deploy

Must-know production settings:

  • DEBUG=False
  • Strong SECRET_KEY from environment
  • SECURE_SSL_REDIRECT, HSTS when behind HTTPS
  • Least-privilege database user

A strong answer is:

Django ships secure defaults, but I still run check --deploy, keep DEBUG off, configure ALLOWED_HOSTS, and never bypass ORM parameterization with string-concatenated raw SQL.

How does CSRF work with Django and a React SPA?

Session-cookie auth from a SPA requires CSRF awareness:

Approach CSRF handling
Cookie session + Django templates {% csrf_token %} in forms
SPA with session cookie Read csrftoken cookie; send X-CSRFToken header on mutations
JWT in Authorization header CSRF not applicable to header; CORS matters instead

Also configure CORS (django-cors-headers) for allowed origins—see full stack interviews.

CSRF_TRUSTED_ORIGINS must include your frontend origin in Django 4+.

A strong answer is:

If the browser sends session cookies, I use CSRF tokens on unsafe methods; for token-in-header APIs I focus on CORS and never store JWTs in cookies without a CSRF strategy.

How do you structure settings for dev, staging, and production?

Split settings modules instead of one giant settings.py:

text
config/
  settings/
    base.py      # INSTALLED_APPS, middleware, shared config
    dev.py       # DEBUG=True, local DB
    staging.py
    production.py  # security flags, logging, caches
python
# manage.py or wsgi.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
Practice Detail
12-factor Secrets from environment / secret manager
Never commit SECRET_KEY, DB passwords
Feature flags Environment-driven toggles
Database Separate DB per environment

A strong answer is:

I inherit from a base settings module and override per environment, loading secrets from the platform—not from git—and I keep DEBUG and ALLOWED_HOSTS strict in production.

What is the difference between authentication and object-level authorization?

Authentication answers who is the user (request.user).

Authorization answers what they may do—at URL level or per object.

Layer Example
Login Session, token, or SSO
View permission IsAuthenticated, IsAdminUser (DRF)
Object permission "May this user edit this order?"

Implement object checks in:

  • DRF has_object_permission on custom permission classes
  • Service layer guards before mutations
  • QuerySet filtering — Order.objects.filter(owner=request.user) so unauthorized rows do not appear

Never rely on hiding UI alone—enforce on the server.

A strong answer is:

Authentication identifies the user; authorization filters QuerySets and checks object permissions before any update—defense in depth, not security by obscurity.

When would you write custom middleware vs a DRF permission or decorator?
Tool Best for
Middleware Every request—logging, request ID, tenant from subdomain
Decorator Single view concerns
DRF permission API authz tied to view/action
Service layer Business rules

Avoid heavy DB work in middleware—it runs on every request including static and health checks.

A strong answer is:

Middleware is for global cross-cutting hooks; permissions and services handle authz and business rules closer to the domain.


Django REST Framework

How do DRF serializers work and what pitfalls hit experienced teams?

Serializers convert between Django models/querysets and JSON (validation, nested relations, write paths).

Type Use
Serializer Explicit fields, non-model input
ModelSerializer CRUD from Meta.model
ListSerializer Bulk operations

Pitfalls:

Pitfall Fix
N+1 in nested serializers select_related / prefetch_related in view get_queryset
Over-exposing fields Explicit fields list; never __all__ in public APIs
Fat create()/update() Delegate to services; keep transactions clear
Validation only in serializer Duplicate critical checks in services for non-HTTP entry points
python
class OrderSerializer(serializers.ModelSerializer):
    class Meta:
        model = Order
        fields = ["id", "status", "total", "customer_id"]
        read_only_fields = ["id"]

A strong answer is:

Serializers define API contracts and validation; I optimize queryset fetching for nested data and keep write paths thin by calling services inside create/update.

Explain ViewSets, routers, and when not to use them.

ViewSets bundle CRUD actions (list, retrieve, create, …). Routers register URL patterns automatically:

python
router = DefaultRouter()
router.register(r"orders", OrderViewSet, basename="order")
Good fit Poor fit
Standard REST resources Non-REST RPC-style endpoints (/orders/123/cancel/)
Consistent permissions per resource Highly heterogeneous handlers

Use @action(detail=True, methods=["post"]) for sub-resource operations on a ViewSet.

A strong answer is:

ViewSets plus routers speed up consistent CRUD APIs; for odd endpoints I use APIView or @action rather than forcing everything into list/retrieve semantics.

How do pagination, filtering, and throttling work in DRF?
Feature Purpose
Pagination PageNumberPagination, CursorPagination for large tables
Filtering django-filter backend — ?status=PAID
Ordering OrderingFilter with allowlist
Throttling Rate limits per user/IP — AnonRateThrottle, UserRateThrottle

Cursor pagination avoids expensive OFFSET on huge tables—aligns with SQL pagination guidance.

Always allowlist filter and order fields—prevent sorting by arbitrary columns (DoS vector).

A strong answer is:

I paginate every list endpoint, allowlist filters and ordering, and throttle public APIs to protect the database from scrape or abuse.

What authentication classes are common in DRF production APIs?
Class Pattern
SessionAuthentication Same-site browser apps with CSRF
TokenAuthentication Simple tokens (consider rotation)
JWTAuthentication Stateless APIs (djangorestframework-simplejwt)
OAuth2 / social Third-party identity

Production notes:

  • Prefer short-lived access + refresh tokens for JWT
  • Store refresh tokens securely; support revocation
  • Combine with permission classes and object checks

A strong answer is:

I pick session auth for same-site apps and JWT for SPA/mobile APIs, always pairing authentication with permissions and object-level checks—not open endpoints after login.

How do you version a Django REST API?

Strategies:

Strategy Example
URL path /api/v1/orders/
Accept header Accept: application/vnd.myapp.v1+json
Query param ?version=1 (less common)

Experienced practice:

  • Deprecate with sunset headers and changelog
  • Maintain serializers per version when shapes diverge
  • Do not break v1 clients when adding v2—run both during migration

A strong answer is:

I version in the URL or Accept header, document deprecation timelines, and keep old serializers until clients migrate—never silently change field meaning.


Authentication, admin, and permissions

When and how do you extend the Django User model?

Django recommends customizing early:

Approach When
AbstractUser Add fields; keep auth machinery
AbstractBaseUser + PermissionsMixin Full control (email as username)
Profile OneToOne Legacy projects only—harder joins
python
class User(AbstractUser):
    department = models.CharField(max_length=64, blank=True)

Set AUTH_USER_MODEL before first migrate—changing later is painful.

A strong answer is:

I use AbstractUser or AbstractBaseUser before the first migration and set AUTH_USER_MODEL so auth and foreign keys stay consistent.

Session authentication vs token/JWT — trade-offs in Django?
Factor Session (cookie) Token / JWT
State Server-side session store Often stateless JWT
Logout Delete session server-side Need blocklist or short TTL
CSRF Required for cookie auth Header tokens avoid CSRF
Mobile / SPA Awkward cross-domain Common fit
Scale Shared session backend (Redis) at scale Fewer server lookups

Hybrid: session for admin, JWT for public API—justify operational cost.

A strong answer is:

Sessions give server-controlled logout for browser apps; JWT suits distributed APIs if I handle expiry, rotation, and revocation explicitly.

How do you customize Django Admin for production teams?

Admin is powerful for internal ops—not a public API.

Customization Use
list_display, search_fields, list_filter Operator efficiency
readonly_fields Audit-sensitive models
inlines Edit related rows together
Custom ModelAdmin actions Bulk operations (with permissions)
has_change_permission Object-level admin authz

Security: restrict admin URL, use staff flag + MFA, never expose admin on public internet without VPN/IP allowlist.

A strong answer is:

Admin is for trusted operators—I customize list views and permissions, lock it behind network controls, and never treat it as the customer-facing API.

How do Django groups and permissions work?

Django's auth model provides permissions (app_label.action_modelname) and groups that bundle permissions.

Layer Scope
user.has_perm('orders.change_order') Model-level
Custom permissions in Meta Business capabilities
DRF DjangoModelPermissions Maps HTTP verbs to model perms

Experienced teams often outgrow default perms and add role tables or external IAM—but know the built-ins for interview baselines.

A strong answer is:

Groups bundle model permissions for RBAC; for fine-grained APIs I combine Django permissions with object-level checks or a dedicated roles model.


Testing, caching, and signals

How do you test Django apps with pytest-django?

pytest-django provides fixtures and database handling:

python
import pytest

@pytest.mark.django_db
def test_create_order(api_client, user):
    api_client.force_authenticate(user=user)
    response = api_client.post("/api/v1/orders/", {"sku": "A1"}, format="json")
    assert response.status_code == 201
Tool Use
@pytest.mark.django_db DB access in test
factory_boy Realistic model factories
assertNumQueries(n) Guard against N+1 regressions
APIClient DRF integration without live server

Prefer fast tests: many unit/service tests, fewer full-stack; use pytest.mark.django_db(transaction=True) when testing on_commit.

A strong answer is:

I use pytest-django with factories for data, APIClient for HTTP contracts, and assertNumQueries on hot list endpoints to catch ORM regressions.

What is the difference between Django Test Client, DRF APIClient, and live server tests?
Client Scope
django.test.Client Django views, templates, forms
rest_framework.test.APIClient DRF views, auth helpers, JSON
LiveServerTestCase / pytest-selenium Real HTTP port—slower, E2E

Most experienced loops want you to test service layer without HTTP when rules are complex—faster and clearer failures.

A strong answer is:

APIClient for HTTP integration, direct service tests for business rules, live server only for true E2E—keep the pyramid mostly fast unit and integration tests.

What caching layers does Django support?
Layer Mechanism
Per-view @cache_page(timeout)
Template fragment {% cache %} tag
Low-level API cache.get/set with Redis/Memcached backend
ORM CachedDB session engine—not general query cache

Invalidation is the hard part:

  • Key by object version or TTL
  • cache.delete on save signals—prefer explicit invalidation in services over scattered signals
python
from django.core.cache import cache

def get_dashboard_stats(user_id):
    key = f"dashboard:{user_id}"
    data = cache.get(key)
    if data is None:
        data = compute_stats(user_id)
        cache.set(key, data, timeout=300)
    return data

A strong answer is:

I cache expensive reads in Redis with explicit keys and TTLs, invalidate on writes in the service layer, and measure hit rate—not cache by default without profiling.

When should you use Django signals — and when should you avoid them?

Signals (post_save, pre_delete, …) decouple side effects from save paths.

Good use Bad use
Audit logging to external system Core business rules
Cache invalidation (careful) Sending email on every save (hidden flow)
Third-party app hooks Replacing service layer

Problems signals cause:

  • Implicit execution order
  • Hard to test and trace
  • Run inside same DB transaction—failures roll back saves unexpectedly

Prefer explicit service methods for critical workflows.

A strong answer is:

Signals are for loose coupling at framework boundaries; I avoid them for business logic and use services plus on_commit for side effects I need to reason about in tests.

What are management commands and how do you use them in production?

Management commands (python manage.py my_command) run admin tasks:

Use case Example
One-off data fix Backfill nullable column
Scheduled job Cron/K8s CronJob invoking command
Imports CSV ingest with batching

Structure with BaseCommand, add_arguments, and idempotent design. Long jobs: log progress, support --dry-run, wrap in transaction.atomic() per batch.

A strong answer is:

Management commands are my tool for batch ops and cron—they are idempotent, log clearly, and batch database work instead of loading everything into memory.


Celery, deployment, and async Django

How does Celery integrate with Django and what production pitfalls appear?

Celery runs async tasks in worker processes—broker (Redis/RabbitMQ) holds the queue.

python
@shared_task(bind=True, max_retries=3)
def send_receipt(self, order_id):
    try:
        order = Order.objects.get(pk=order_id)
        mail.send(order)
    except Exception as exc:
        raise self.retry(exc=exc, countdown=60)
Pitfall Fix
Task before DB commit Use transaction.on_commit
Unbounded retries max_retries, exponential backoff, dead letter
Long tasks blocking workers Chunk work; separate queues
Passing model instances Pass IDs only—stale data

Monitor queue depth, worker memory, and task failure rates.

A strong answer is:

Celery handles async side effects—I enqueue by ID after commit, cap retries with backoff, and monitor queues so email or report jobs never block web workers.

How do you serve static and media files in production?
Type Dev Production
Static (CSS/JS) runserver or collectstatic Whitenoise, S3, or CDN via collectstatic
Media (user uploads) Local MEDIA_ROOT S3/Azure Blob with django-storages

Never serve user uploads from Django workers at scale—use object storage + signed URLs.

collectstatic in CI/CD before deploy; cache-bust with hashed filenames (ManifestStaticFilesStorage).

A strong answer is:

Static assets go to CDN or Whitenoise after collectstatic; user media lives in object storage with signed URLs—not on app server disks.

What are the limits of async Django views in 2026?

Django 5.x continues improving async ORM methods (aget, acreate, async for), but the ORM is not fully non-blocking:

Safe in async view Risky
await httpx.get(...) Order.objects.get() without bridge
await sync_to_async(service)() Long CPU work on event loop
Streaming responses transaction.atomic() in async context

Use sync views + gunicorn workers unless profiling proves async I/O wins.

A strong answer is:

Async views help for external I/O; ORM-heavy endpoints stay sync or use sync_to_async deliberately—I profile before adopting ASGI for CRUD APIs.

How do you deploy Django with Docker and Gunicorn?

Typical container layout:

Process Role
gunicorn / uvicorn WSGI/ASGI workers
migrate job Run once per deploy
Celery worker Separate container
Redis Broker + cache
Postgres Managed database
dockerfile
# Simplified pattern
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]

Health check endpoint (not admin)—wire to load balancer. Run check --deploy in CI.

Pair with Git interview questions for release workflow stories.

A strong answer is:

I containerize web and worker processes separately, run migrations as a deploy step, put Gunicorn behind a reverse proxy, and expose a health check for the load balancer.


Senior scenarios and final prep

Scenario: A DRF list endpoint is slow in production — how do you debug?
Step Action
1 Confirm scope—one endpoint? spike after deploy?
2 APM or logs—latency breakdown (DB vs serialization)
3 Query countdjango-debug-toolbar in staging, assertNumQueries locally, connection.queries in dev
4 Check serializer nesting and SerializerMethodField DB hits
5 EXPLAIN slow SQL — SQL prep
6 Add select_related/prefetch_related or denormalize read model
7 Pagination—ensure not returning thousands of rows
8 Cache stable reads if appropriate

Common root causes: N+1, missing index, unbounded queryset, synchronous external HTTP in serializer.

A strong answer is:

I measure query count and SQL time first, fix ORM fetching and pagination, then cache or denormalize only after profiling proves the bottleneck.

Scenario: Two requests decrement the same inventory — how do you prevent overselling?

Options (often combined):

Approach Detail
select_for_update() Lock row inside atomic() during read-modify-write
F() expression update(stock=F('stock') - 1) with filter(stock__gt=0)
Database constraint CheckConstraint(stock__gte=0)
Idempotent orders Unique key on client request id
python
updated = Product.objects.filter(pk=sku, stock__gt=0).update(stock=F("stock") - 1)
if updated == 0:
    raise OutOfStock()

Avoid application-level read-then-write without locking under concurrency.

A strong answer is:

I use atomic updates with F() or row locks, enforce non-negative stock in the database, and return clear errors when the update count is zero.

How do you structure a large Django codebase?

Common mature layout:

text
project/
  config/           # settings, urls, wsgi
  apps/
    orders/
      models.py
      services.py
      selectors.py
      api/
        serializers.py
        views.py
        urls.py
      tests/
    users/
  common/           # shared utilities, exceptions

Principles:

  • Apps by bounded context—not by technical layer only
  • api/ package per app for DRF surface
  • Avoid circular imports—services call repositories/selectors, not views
  • Shared exception handler for consistent API errors

A strong answer is:

I split by domain apps with services and API packages inside each, keep settings and URLs centralized, and enforce boundaries so imports flow inward—not views importing across domains chaotically.

What should you rehearse the week before a senior Django interview?

Checklist:

  • Draw request lifecycle with middleware order
  • Explain N+1 fix on a real serializer with prefetch_related
  • Walk through transaction.atomic + on_commit + Celery
  • Recite security checklist — CSRF, XSS, DEBUG, ALLOWED_HOSTS, check --deploy
  • DRF story — pagination, auth, object permissions, versioning
  • One migration war story — backward compatible rollout
  • One performance win — query count before/after
  • Python fundamentals refresh — decorators, GIL, asyncio limits
  • SQL — indexes and EXPLAIN

Behavioral: STAR story for production incident (runaway query, failed task queue, bad deploy).

A strong answer is:

I rehearse one Django system end to end—ORM, DRF, workers, deploy—and tie every answer to production experience with metrics, not tutorial definitions.


Pattern cheat sheet (quick reference)

Need Django starting point
Forward FK join select_related()
Reverse / M2M batch prefetch_related()
Complex OR Q() objects
Atomic counter F() + filter(stock__gt=0).update()
Safe async side effect transaction.on_commit(task.delay)
Row lock select_for_update() in atomic()
API CRUD DRF ViewSet + router
Object authz Filter queryset + has_object_permission
Prod security DEBUG=False, check --deploy
Background work Celery + Redis broker
Tests pytest-django + assertNumQueries

References

Official Django documentation

On-site prep


Summary

Experienced Django interviews trace the full request pipeline, efficient ORM usage, DRF authorization, and Celery, caching, and migrations in production. Answer aloud and compare your structure to each section. Pair with Python developer interviews and SQL interviews when list endpoints fail at the database layer.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …