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.
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:
URL pattern
→ Django view
→ model/service/ORM
→ template or JSON responseExample:
# urls.py
urlpatterns = [
path("orders/<int:pk>/", views.order_detail, name="order-detail"),
]# 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:
- Web server receives the HTTP request
- WSGI or ASGI server passes it to Django
- Request enters middleware in the order listed in
settings.MIDDLEWARE - URL resolver matches the path using
ROOT_URLCONF - Django calls the matched view
- View runs application logic, ORM queries, services, permissions, or forms
- View returns an
HttpResponse,JsonResponse, redirect, or DRFResponse - Response travels back through middleware in reverse order
- Server sends the final HTTP response to the client
Flow:
Client
→ web server
→ WSGI/ASGI server
→ middleware request phase
→ URL resolver
→ view
→ response object
→ middleware response phase
→ clientExample URL route:
urlpatterns = [
path("orders/<int:pk>/", OrderDetailView.as_view(), name="order-detail"),
]Important middleware order example:
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:
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 responseMiddleware behaves like an onion:
middleware A request phase
middleware B request phase
view
middleware B response phase
middleware A response phaseCommon 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:
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:
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:
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:
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:
# Lazy
orders = Order.objects.filter(status="PAID")
# Evaluates
for order in orders:
print(order.id)QuerySets are chainable:
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:
Order.objects.filter(status="PAID").count()
Order.objects.filter(status="PAID").exists()Instead of:
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.queriesin developmentassertNumQueries()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:
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”:
NULL
""That can complicate filters, uniqueness, validation, and API behavior.
Example problem:
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=Truecontrols database NULL storage, whileblank=Truecontrols validation. I usually avoidnull=Trueon 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:
from django.db.models import F
Product.objects.filter(pk=product_id, stock__gt=0).update(
stock=F("stock") - 1
)This is safer than:
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:
from django.db.models import Q
orders = Order.objects.filter(
Q(status="PAID") | Q(status="SHIPPED"),
total__gte=100,
)Negation:
Order.objects.filter(~Q(status="CANCELLED"))Dynamic filter example:
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:
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:
@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:
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:
with transaction.atomic():
order = Order.objects.create(...)
send_receipt.delay(order.id) # worker may run too earlyGood pattern:
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, andon_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:
python manage.py makemigrations
python manage.py migrate
python manage.py showmigrations
python manage.py migrate --planMigrations 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:
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 windowRisky 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:
python manage.py makemigrations --check --dry-run
python manage.py migrate --plan
python manage.py testOperational 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-runandmigrate --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
class OrderListView(ListView):
model = Order
paginate_by = 25A 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:
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
includeand namespaces so apps stay modular andreverse()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:
views.py → HTTP, permissions, input/output
services.py → business transactions, orchestration
selectors.py → complex read queries (optional)
models.py → fields, constraints, small methodsBenefits:
- 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:
python manage.py check --deployMust-know production settings:
DEBUG=False- Strong
SECRET_KEYfrom 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:
config/
settings/
base.py # INSTALLED_APPS, middleware, shared config
dev.py # DEBUG=True, local DB
staging.py
production.py # security flags, logging, caches# 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_permissionon 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 |
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:
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
@actionrather 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 |
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:
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.deleteon save signals—prefer explicit invalidation in services over scattered signals
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 dataA 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.
@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 |
# 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 count — django-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 |
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:
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, exceptionsPrinciples:
- 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
- Django documentation
- Django REST framework
- Django deployment checklist
- Django async support
- Database transactions
On-site prep
- Python developer interview questions
- Full stack developer interviews
- SQL technical interview questions
- Data science interview questions
- Git interview questions
- Flask SQLAlchemy
- Python yield
- Python try except
- Create a Python package
- Interview Questions category
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.

