Mid-level 12 min · March 06, 2026
Django Interview Questions

Django ORM N+1 — Why 501 Queries Killed a SaaS Dashboard

A single missing select_related() caused 501 queries instead of 1, timing out a production dashboard.

N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Drawn from code that ran under real load.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Django's MVT architecture places Controller role in URL dispatcher; Views are coordinators, templates are pure presentation.
  • QuerySets are lazy: database hit only on iteration, slicing, or explicit evaluation.
  • Use select_related() for ForeignKey/OneToOne (SQL JOIN) and prefetch_related() for ManyToMany/reverse FK (two queries).
  • Middleware executes top-to-bottom on request, bottom-to-top on response; order in MIDDLEWARE list is critical.
  • Signals decouple apps but make flow implicit; for same-app logic, prefer direct calls for clarity and testability.
  • The N+1 problem is the most common production performance killer; always check with django-debug-toolbar.
✦ Definition~90s read
What is Django Interview Questions?

Celery is a distributed task queue for asynchronous execution in Python. It offloads long-running or scheduled work (email sending, image processing, API calls) from the request-response cycle. Integration with Django uses django-celery-beat for periodic tasks and django-celery-results for storing results.

Think of Django like a fully-equipped restaurant kitchen.

You define tasks as decorated functions, then call them with .delay() or .apply_async(). Celery requires a message broker (Redis or RabbitMQ) and a worker process running separately. Key pattern: receive HTTP request, create background task, return response immediately.

Celery workers pick up tasks from the broker. For scheduled tasks, set CELERY_BEAT_SCHEDULE in Django settings. Warning: Celery adds operational complexity — you must monitor workers, handle retries, and manage concurrency. For simple async needs, Django's built-in threading or django-background-tasks might suffice.

Plain-English First

Think of Django like a fully-equipped restaurant kitchen. The menu is your URL routing, the chefs are your views, the pantry is your database accessed through the ORM, and the health inspector rules are your middleware. An interviewer isn't just checking if you know the kitchen exists — they want to know if you can run a dinner service under pressure. These questions reveal whether you understand how the whole kitchen works together, not just how to boil water.

Django powers some of the most-visited sites on the planet — Instagram started on it, Pinterest scaled with it, and thousands of startups ship with it every year. When a company posts a Django backend role, they're not looking for someone who memorised the docs. They want engineers who understand the framework's design decisions deeply enough to bend them when business requirements get weird. That's what separates a candidate who gets an offer from one who gets a polite rejection email.

The problem with most Django interview prep is that it's surface-level. Lists of questions with one-line answers that don't explain why Django works the way it does. That leaves you vulnerable the moment an interviewer asks a follow-up — 'okay, but why would you choose that approach?' — and you freeze. Real interviews dig into trade-offs, failure modes, and architectural judgment, not just API recall.

By the end of this article you'll be able to explain Django's MVT architecture, the ORM query lifecycle, middleware execution order, signals vs direct calls, and caching strategies — each with the real-world context that makes your answers land. You'll also know the three mistakes that silently kill otherwise good Django interviews.

What Django ORM N+1 Actually Is

The Django ORM N+1 problem is a query inefficiency where fetching a list of N parent objects triggers N additional queries to retrieve related child objects, instead of a single JOIN or prefetch. The core mechanic: lazy evaluation of QuerySets defers database hits until you access a related field, so iterating over 500 orders and accessing each order.user fires 1 query for orders + 500 queries for users = 501 total.

In practice, Django's ORM hides this behind clean syntax. A simple for order in Order.objects.all(): print(order.user.email) looks innocent but generates a separate SQL query per iteration. The ORM's select_related() and prefetch_related() methods exist specifically to collapse these into JOINs or batched queries, but they require explicit opt-in. Without them, the default behavior is lazy loading — convenient for prototyping, catastrophic under load.

You must use prefetching when rendering list views, dashboards, or any endpoint that serializes parent-child relationships. In production, a single N+1 on a dashboard endpoint serving 500 concurrent users can spike database connections to 250,000+ queries per second, overwhelming connection pools and causing cascading timeouts. The rule: if you iterate over a QuerySet and access a related field in the loop, you're writing an N+1 — always verify with django-debug-toolbar or connection.queries.

Lazy Loading Is Not Free
Django's lazy QuerySet evaluation is a performance trap: it defers work until the last possible moment, turning innocent attribute access into a database round-trip.
Production Insight
A SaaS dashboard listing 500 recent orders with customer names triggered 501 queries per page load, saturating a 50-connection pool and causing 5-second timeouts.
Exact symptom: database CPU at 100%, connection pool exhaustion errors, and page load times growing linearly with list size.
Rule of thumb: if you see a query count > (number of parent rows + 1), you have an N+1 — fix with select_related for FK/O2O and prefetch_related for M2M/reverse FK.
Key Takeaway
N+1 is not a Django bug — it's the default behavior of lazy ORMs that you must explicitly override.
Always profile query count in development: one query per parent row is the red line.
Use select_related for single-valued relationships and prefetch_related for multi-valued; never access related fields inside a loop without them.
Django ORM N+1 Query Problem Flow THECODEFORGE.IO Django ORM N+1 Query Problem Flow From lazy QuerySets to 501 queries killing dashboard performance Lazy QuerySet No DB hit until evaluated N+1 Trigger Loop over related objects triggers per-row query select_related JOIN for FK/O2O relations prefetch_related Separate query for M2M/reverse Optimized QuerySet Reduces queries to 1 or 2 ⚠ Forgetting select_related/prefetch_related in templates Always inspect with django-debug-toolbar before deploying THECODEFORGE.IO
thecodeforge.io
Django ORM N+1 Query Problem Flow
Django Interview Questions

Django's MVT Architecture — Why It's Not Quite MVC

Almost every Django interview starts here, and almost every candidate fumbles it by saying 'Django uses MVC.' It doesn't — not exactly. Django uses MVT: Model, View, Template. The naming shift isn't cosmetic; it reflects a genuine architectural difference worth understanding.

In classic MVC, the Controller handles HTTP requests, decides what data to fetch, and tells the View what to render. In Django, that controller logic lives in the View function or class. Django's Template is purely the presentation layer — it has no business logic, and the framework enforces that through the deliberately limited template language. The 'Controller' in the traditional sense is Django's URL dispatcher itself, which routes the incoming request to the right view.

Why does this matter in an interview? Because understanding MVT shows you understand Django's philosophy: keep business logic in Python (views and models), keep presentation in templates, and trust the framework to wire them together. When you understand that philosophy, you make better decisions — like knowing that fat models and thin views is a Django best practice, not just a style preference.

Interviewers love asking 'where would you put business logic in a Django app?' The wrong answer is 'in the template.' The right answer is 'in the model or a service layer, with the view acting as a thin coordinator.'

io.thecodeforge.mvt_example.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package io.thecodeforge;

# ─── models.py ───────────────────────────────────────────────────────────────
# The Model owns the data AND the business rules around that data.
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=200)
    base_price = models.DecimalField(max_digits=8, decimal_places=2)
    is_on_sale = models.BooleanField(default=False)

    def discounted_price(self):
        """Business logic belongs in the model — fat models, thin views."""
        if self.is_on_sale:
            return self.base_price * 0.80  # 20% discount
        return self.base_price

# ─── views.py ────────────────────────────────────────────────────────────────
from django.shortcuts import render
from .models import Product

def product_list(request):
    on_sale_products = Product.objects.filter(is_on_sale=True)
    context = {
        'products': on_sale_products,
    }
    return render(request, 'shop/product_list.html', context)
Output
# Django evaluates QuerySet in the template layer or upon explicit evaluation.
# SQL: SELECT * FROM shop_product WHERE is_on_sale=True
Interview Gold:
When asked 'what's the difference between MVC and MVT?', say: 'In Django's MVT, the URL dispatcher plays the role of the Controller, routing requests to Views. Django's Views are closer to MVC Controllers — they coordinate data fetching and template rendering. The Template is strictly presentational, which is enforced by the template language's intentional limitations.' That answer shows architectural understanding, not just terminology recall.
Production Insight
Putting discount logic in a template tag bypasses testability and makes price changes across pages inconsistent.
Production teams enforce: one method per model, tested in isolation, called from views — never templates.
Rule: if you can't unit test it without a browser, it's in the wrong layer.
Key Takeaway
Django's MVT = URL dispatcher as Controller, View as Coordinator, Template as Presentation.
Business logic belongs in models or services — never in templates.
Rule: fat models, thin views, dumb templates.
Where to place business logic?
IfLogic operates on a single model (e.g., price, status)
UsePlace in a model method. Tests: ProductTestCase without views.
IfLogic coordinates multiple models or external services
UsePlace in a dedicated service layer (e.g., services/order_service.py).

Django ORM Deep Dive — QuerySets, Lazy Evaluation & the N+1 Problem

The ORM is where most Django interviews separate candidates. Everyone knows .filter() and .all(). The real question is whether you understand when the database is actually hit — because getting that wrong kills performance in production.

Django QuerySets are lazy. When you write Product.objects.filter(is_on_sale=True), nothing touches the database. Django builds a description of the query in memory. The database is only hit when you iterate, slice, call list(), or access len() on the QuerySet. This design lets you chain filters efficiently without redundant round-trips.

The N+1 problem is the most common ORM performance trap — and interviewers know it. It happens when you load a list of objects and then access a related object on each one inside a loop. That's one query to get the list, then N more queries for each related record. On a list of 500 orders, you've just fired 501 database queries instead of 2.

The fix is select_related() for ForeignKey/OneToOne relationships (SQL JOIN) and prefetch_related() for ManyToMany or reverse ForeignKey relationships (separate optimised query). Knowing which one to use — and why they work differently under the hood — is what makes you stand out.

io.thecodeforge.orm_patterns.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package io.thecodeforge;

from .models import Book, Author

# ✅ THE FIX — select_related() for ForeignKey (SQL JOIN, 1 query total)
def book_list_fast(request):
    # Django generates: SELECT book.*, author.* FROM book
    #                   INNER JOIN author ON book.author_id = author.id
    books = Book.objects.select_related('author').all()

    for book in books:
        # author data is already in memory — ZERO extra queries here
        print(book.author.name)

    return render(request, 'books/list.html', {'books': books})

# ✅ prefetch_related() for reverse FK / ManyToMany (2 queries, not N+1)
def author_list_with_books(request):
    # Query 1: SELECT * FROM author
    # Query 2: SELECT * FROM book WHERE author_id IN (1, 2, 3, ...)
    authors = Author.objects.prefetch_related('books').all()
    return render(request, 'books/authors.html', {'authors': authors})
Output
# SELECT_RELATED results in a single SQL JOIN.
# PREFETCH_RELATED results in two separate SELECT queries with an 'IN' clause.
Watch Out:
Using select_related() on a ManyToMany field won't work — it silently falls back to N+1 behaviour without raising an error. Always use prefetch_related() for M2M and reverse ForeignKey. A quick way to catch N+1 in development: install django-debug-toolbar and watch the SQL panel — it highlights duplicate queries in red.
Production Insight
N+1 queries are silent performance killers — the app works fine in development with 10 rows but crashes with 500.
Production teams set DEBUG=False early, but without django-debug-toolbar in dev they can't catch it.
Rule: run len(connection.queries) after any view that returns a list of related objects.
Key Takeaway
QuerySets are lazy; evaluation happens on iteration, slicing, or len().
select_related() (JOIN) for FK/O2O; prefetch_related() (two queries) for M2M/reverse FK.
Rule: use django-debug-toolbar to catch N+1 before it hits prod.
select_related vs prefetch_related
IfRelationship is ForeignKey or OneToOneField
UseUse select_related() — SQL JOIN, one query.
IfRelationship is ManyToManyField or reverse ForeignKey
UseUse prefetch_related() — two separate queries.

Middleware — Django's Request/Response Pipeline Explained

Middleware is one of those Django concepts that interviewers love because it reveals whether you understand the framework's internals or just its surface API. Most candidates know middleware exists for authentication and CSRF. Fewer can explain the execution order, and almost none can write custom middleware correctly on the spot.

Think of middleware as a stack of airport security layers. Your request passes through each layer on the way in, reaches the view (the gate), and then passes back through each layer in reverse on the way out. Django's MIDDLEWARE setting in settings.py defines this stack — top to bottom for requests, bottom to top for responses.

This bidirectional flow matters. SecurityMiddleware sits at the top intentionally — it enforces HTTPS redirects before any other processing happens. SessionMiddleware must come before AuthenticationMiddleware because auth needs the session to be set up first. Swap them and you get a cryptic AttributeError at runtime.

io.thecodeforge.middleware.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package io.thecodeforge;

import time
from django.http import JsonResponse

class RequestTimingMiddleware:
    """
    TheCodeForge Senior Pattern: Uses monotonic clock for reliability.
    """
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # ── INBOUND ──
        start_time = time.monotonic()

        response = self.get_response(request)

        # ── OUTBOUND ──
        duration = (time.monotonic() - start_time) * 1000
        response['X-Response-Time-MS'] = f"{duration:.2f}"
        return response
Output
X-Response-Time-MS: 12.45
Pro Tip:
The __init__ method receives get_response — a callable representing the rest of the middleware stack plus the view. The middleware doesn't know or care what's below it. This is the Chain of Responsibility design pattern in action. Mentioning design patterns in a Django interview immediately signals senior-level thinking.
Production Insight
Swapping SessionMiddleware and AuthenticationMiddleware order causes request.session to be None at auth time.
Production teams catch this during deployment with integration tests that exercise the full request lifecycle.
Rule: never reorder MIDDLEWARE without checking Django's documented ordering in the settings file.
Key Takeaway
Middleware stack: request goes top-down, response goes bottom-up.
Order matters: Session before Auth, Security first, custom last for response headers.
Rule: always include integration tests that verify the full pipeline.
Where does your custom middleware belong?
IfMiddleware must run before authentication (e.g., rate limiting, IP filtering)
UsePlace it BEFORE AuthenticationMiddleware in MIDDLEWARE list.
IfMiddleware modifies response headers (e.g., timing, CORS)
UsePlace it at the END of MIDDLEWARE so it runs last on the response path.

Django Signals vs Direct Calls — When Decoupling Costs You

Signals are one of Django's most misunderstood features, and they're a favourite interview topic because they expose how a candidate thinks about system design trade-offs — not just Django syntax.

Django signals implement the Observer pattern. When something happens — a model is saved, a user logs in, a request finishes — Django broadcasts a signal. Any code that has 'subscribed' to that signal runs automatically. The appeal is decoupling: the model that fires post_save doesn't need to know about the email sender, the audit logger, or the cache invalidator. They all subscribe independently.

But here's where interviewers trip people up: signals are not free. They make code flow implicit and hard to trace. When a User is saved and five signal handlers fire across three different apps, a new team member reading the save code has no idea any of that happens. That's a maintenance burden.

io.thecodeforge.signals.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package io.thecodeforge;

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import UserProfile

@receiver(post_save, sender=User)
def manage_user_profile(sender, instance, created, **kwargs):
    """
    Senior Approach: Use 'created' flag to avoid unnecessary DB writes on updates.
    """
    if created:
        UserProfile.objects.create(user=instance)
Output
✅ Profile created for user: forge_admin
Watch Out:
If you forget the ready() method in apps.py to import your signals, your @receiver decorators never register — and the signal fires with no handlers attached. No error is raised. The code just silently doesn't work. This is one of the most common Django bugs in production and a favourite 'gotcha' interview question.
Production Insight
A forgotten import in apps.py.ready() caused a user profile creation to silently fail for a week in a production system.
The team only discovered it when customer support reported missing profiles.
Rule: always write a management command that tests signal registration explicitly.
Key Takeaway
Signals decouple apps but make flow implicit — use for cross-app concerns only.
Direct calls are clearer, easier to test, and easier to trace.
Rule: if you can't see the side effects by reading the calling code, you're hiding complexity.
Signal vs direct call
IfThe action is tightly coupled and within the same app (e.g., create profile on user signup)
UseUse a direct function call in the view or model method. Clear, traceable, testable.
IfThe action spans across multiple apps and must remain decoupled (e.g., audit logging, email notifications)
UseUse signals. Document the signal subscribers in a README or signal registry.

Caching Strategies in Django — Understanding the Cache Framework and Its Pitfalls

Django's cache framework is a sleeper hit in interviews. Most candidates know about cache_page but can't explain when to use low-level caching vs template fragments vs per-site caching. The key insight: caching is a trade-off between freshness and speed, and Django gives you the tools to choose.

Django provides a cache abstraction that supports multiple backends: in-memory (locmem), filesystem, database, and Redis/Memcached. For production, Redis is the default choice. The cache framework includes: - Per-view caching with @cache_page — simple, great for read-heavy views. - Template fragment caching with {% cache %} tag — caches a block of rendered HTML. - Low-level caching with cache.set() / cache.get() — most flexible, e.g., caching query results or expensive computations.

The biggest gotcha: cache key collisions. If you use the same key across different parts of your app, you may serve stale or wrong data. Also, cache invalidation is notoriously difficult — a reason many teams move to database-level caching (materialized views) for high-consistency scenarios.

io.thecodeforge.caching.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package io.thecodeforge;

from django.core.cache import cache
from .models import Product

def product_dashboard(request):
    # Low-level caching of expensive aggregation
    cache_key = 'product_count_by_category'
    data = cache.get(cache_key)
    if data is None:
        # Compute only if cache miss
        data = Product.objects.values('category').annotate(count=models.Count('id'))
        cache.set(cache_key, data, 60 * 5)  # 5 minutes
    return render(request, 'dashboard.html', {'categories': data})
Output
Cache miss: queries DB, sets cache for 5 minutes.
Cache hit: returns data from Redis without DB round-trip.
Cache Levels Decision Tree
  • Top: Per-view cache — entire response, low granularity, high hit rate for anonymous pages.
  • Middle: Template fragment cache — partial page updates, good for user-specific sections.
  • Bottom: Low-level cache — arbitrary data, maximum control, but requires manual invalidation.
Production Insight
A team used @cache_page(60*15) on a product listing page, but forgot to vary on the user's currency cookie.
Users saw wrong prices for 15 minutes — a classic cache poisoning bug.
Rule: always use @vary_on_headers or @vary_on_cookie when caching views with user-specific content.
Key Takeaway
Choose cache level by granularity: page → fragment → low-level.
Always vary cache by user context (cookies, headers) to avoid cross-user data leaks.
Rule: stale cache is often worse than no cache — implement proper invalidation triggers.
Which caching level to use?
IfEntire page is identical for all users (e.g., landing page)
UseUse @cache_page with a short timeout.
IfPage has a user-agnostic section (e.g., product grid)
UseUse template fragment caching {% cache %} around that section.
IfNeed to cache computed data across multiple views (e.g., aggregations)
UseUse low-level cache.set/get with a unique key per computation.

Project vs App — The Boundary That Kills Deployments

Most juniors treat a Django project like a monolith and apps like throwaway folders. That misunderstanding will cost you in production. A project is the wiring — settings, root URL conf, WSGI entry point. An app is a bounded context that does one thing well. Instagram runs thousands of apps per project. You want to break your code into apps the moment two views touch different database tables or different business logic. The rule: if you can swap an app out and the rest of the project still works, you designed it right. If removing an app breaks the URL patterns, you have a coupling problem. The project is the deployment unit; the app is the domain boundary. Mix them up and you get circular imports at 3 AM on a Friday.

ProjectVsApp.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — interview tutorial

# project root: myproject/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'billing',       # handles payments
    'inventory',     # handles stock
    'notifications', # handles email/SMS
]

# billing/models.py
from django.db import models

class Invoice(models.Model):
    order_id = models.CharField(max_length=32)
    amount = models.DecimalField(max_digits=10, decimal_places=2)

# inventory/models.py
from django.db import models

class StockItem(models.Model):
    sku = models.CharField(max_length=16, unique=True)
    quantity = models.IntegerField()

# They never import each other. Clean.
Output
billing app knows nothing about inventory.
inventory app knows nothing about billing.
Deployable independently. Testable independently.
Production Trap:
If you find yourself importing models from another app inside your models.py, you missed the boundary. Refactor now, not after the circular import error wakes you up.
Key Takeaway
A project is config; an app is domain. If you can't delete an app without breaking everything, you're building a monolith in disguise.

Django's Auth System — Don't Roll Your Own, But Know Where It Bleeds

Django ships with a full auth system: users, groups, permissions, sessions, and a password hasher that rotates algorithms automatically. Most interviewers will ask how you'd extend it because every production app needs custom fields or social login. The stock User model has a username, email, password fields. If you need a phone number or a tenant ID, do not monkey-patch. Subclass AbstractUser or AbstractBaseUser before the first migration runs. Once you have 10,000 users in the database, changing the auth model is a migration horror story you don't want. The session backend stores session data in the database by default — fine for small apps, but for high-traffic replace it with Redis or a signed cookie backend. And for the love of god, never store plain-text tokens in the session. Use Django's built-in token authentication or JWT with a proper expiry. The auth framework handles password hashing, login throttling, and CSRF. Let it do its job.

CustomUser.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — interview tutorial

# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    tenant_id = models.CharField(max_length=16, null=True)
    phone = models.CharField(max_length=20, unique=True)

# settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
]

# migrate FIRST, then populate.
# If you already ran 'python manage.py migrate' with the stock User,
# you need a multi-step migration. Painful. Don't learn this the hard way.
Output
python manage.py makemigrations accounts
python manage.py migrate
Custom user ready. Now add users with tenant_id and phone.
No legacy migration debt.
Senior Shortcut:
Before writing a single line of business logic, set AUTH_USER_MODEL to a custom model. You can always add fields later, but you cannot easily swap from the default User to a custom one after data exists.
Key Takeaway
Customize Django's auth model before the first migration. After that, every change requires a migration from hell.

Class-Based Views vs Function-Based Views — Pick Your Weapon

Interviewers love this one because it reveals whether you actually write production Django or just follow tutorials. Function-based views (FBVs) are Python functions that take a request and return a response. Simple, explicit, easy to test. Class-based views (CBVs) wrap that logic into reusable classes with mixins. The problem: CBVs smuggle in magic. A CreateView inherits from ten layers of mixins. When a junior sees a 500 error on form validation and doesn't know which parent class is overwriting the save method, they lose an hour. My rule: use FBVs for anything with less than three lines of custom logic. Use CBVs when you need to reuse the same pattern across 10+ endpoints — but only if you write explicit get/post methods. Never chain MixinOrder madness. The Django docs show you a ListView that works in three lines. Real production code needs a query filter, permissions check, and a pagination offset. Write that as a CBV with a single method override, or just keep it as an FBV. Your colleagues will thank you.

CBVvsFBV.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — interview tutorial

# views.py
from django.views.generic import ListView
from django.shortcuts import render
from .models import Order

# CBV — good when you need the same pattern everywhere
class RecentOrdersView(ListView):
    model = Order
    template_name = 'orders/recent.html'
    queryset = Order.objects.filter(status='paid')[:50]

# FBV — explicit, clear, easy to debug
def recent_orders_view(request):
    orders = Order.objects.filter(status='paid')[:50]
    return render(request, 'orders/recent.html', {'orders': orders})

# Pick your poison. Just know why.
Output
CBV: 4 lines of code, but 12 lines of inheritance you don't see.
FBV: 4 lines of code, zero hidden logic.
Production Trap:
If your CBV overrides more than two methods, it's time to refactor back to an FBV or split the class. Hidden complexity kills debugging velocity.
Key Takeaway
FBVs for logic under 5 lines. CBVs for reusable patterns — but only if you can override one method and get out.

Why Your Django App Dies Without a Virtual Environment

Virtual environments are not optional. They're the difference between a reproducible build and a production meltdown. When you install packages globally, you're begging for version conflicts — Django 3.2 on one project, 5.0 on another, same box, instant disaster.

The why is isolation. Each project gets its own Python interpreter and package tree. You lock dependencies in requirements.txt so the next dev (or your CI pipeline) gets exactly the same bits. No surprises. No 'but it worked on my machine'.

In production, you're deploying against a frozen environment. If you skip this, a botched pip install on the server can break your live site. That's not a bug — that's negligence.

Immutability wins. Always pin your versions. Use python -m venv venv and activate it before you type pip install a single thing.

setup_env.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — interview tutorial

// Create and activate virtual environment
import subprocess
import sys

subprocess.run([sys.executable, '-m', 'venv', 'venv'])
# On Windows: venv\Scripts\activate
# On macOS/Linux: source venv/bin/activate

# Install Django and pin the version
subprocess.run(['pip', 'install', 'django==5.0.2'])

# Freeze dependencies for reproducibility
subprocess.run(['pip', 'freeze', '>', 'requirements.txt'])
Output
Successfully installed Django-5.0.2
...
Production Trap:
Never run pip install without an active virtual environment in a CI/CD pipeline. You'll pollute the global Python installation and break system packages.
Key Takeaway
Always isolate dependencies per project using python -m venv — your future self and your deployment pipeline will thank you.

makemigrations vs migrate — The Silent Schema Contract

Newcomers think makemigrations and migrate do the same thing. They don't. And confusing them will cost you a rollback.

makemigrations reads your models, diffs them against your migration history, and generates a new migration file. It's a blueprint. No tables touched. It's the declaration of intent.

migrate executes that blueprint against your database. It creates tables, alters columns, runs data migrations. If your migration file has a typo or references a column that doesn't exist yet, migrate fails hard.

The WHY: this separation lets you review generated SQL before committing. python manage.py sqlmigrate myapp 0002 shows the exact DDL. You catch stupid mistakes — like dropping a column you need — before it hits production.

Never auto-apply migrations in CI. Review them. Test them against a staging DB. A bad migration is a production outage waiting to happen.

migration_commands.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — interview tutorial

# Step 1: Detect model changes and create migration blueprint
# Runs: Django compares models to last migration
$ python manage.py makemigrations
Migrations for 'inventory':
  inventory/migrations/0002_product_price.py
    - Add field price to product

# Step 2: Review the generated SQL before applying
$ python manage.py sqlmigrate inventory 0002

BEGIN;
--
-- Add field price to product
--
ALTER TABLE `inventory_product` ADD COLUMN `price` decimal(10,2) NOT NULL DEFAULT 0.00;
COMMIT;

# Step 3: Apply the migration against the database
$ python manage.py migrate
Operations to perform:
  Apply all migrations: inventory
Running migrations:
  Applying inventory.0002_product_price... OK
Output
Operations to perform:
Apply all migrations: inventory
Running migrations:
Applying inventory.0002_product_price... OK
Production Trap:
Always run sqlmigrate on a staging DB before applying to production. A missing default value or wrong field type can lock your table and take the site down.
Key Takeaway
makemigrations generates the plan; migrate executes it. Never skip reviewing the SQL plan before applying to production.

CSRF Token — Why Django Makes You Jump Through Hoops

The csrf_token is not a suggestion — it's Django's firewall against Cross-Site Request Forgery attacks. CSRF tricks an authenticated user into executing unwanted actions on your site. Imagine a bank: an attacker crafts an image tag that triggers a transfer. If your site trusts all POST requests without validation, you're owned.

Django's solution: embed a cryptographically signed token in every form rendered by the server. When the browser submits the form, it includes this token in a hidden field. Django compares it against the session cookie. Mismatch? 403 Forbidden.

The WHY: this token is unique per user session and changes periodically. An attacker can't guess it, and because of same-origin policy, they can't read it from another site either. It's defense in depth.

You must include {% csrf_token %} inside every <form> that does POST. Every single one. The template won't compile without it by default — and that's a feature, not a bug.

csrf_template.htmlPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — interview tutorial

<!-- forms/transfer.html -->
<form method="post" action="/transfer/">
    {% csrf_token %}
    <!-- Django injects: <input type="hidden" name="csrfmiddlewaretoken" value="random_signed_value"> -->
    <label for="amount">Amount:</label>
    <input type="number" name="amount" id="amount" required>
    <label for="recipient">Recipient:</label>
    <input type="text" name="recipient" id="recipient" required>
    <button type="submit">Send Money</button>
</form>

<!-- views.py -->
from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt

# NEVER do this in production:
@csrf_exempt  # BAD — disables CSRF protection
def transfer_view(request):
    if request.method == 'POST':
        amount = request.POST.get('amount')
        recipient = request.POST.get('recipient')
        # Process transfer unsafely
        return redirect('/success')
    return render(request, 'forms/transfer.html')
Output
CSRF token is injected automatically, preventing forged cross-origin requests.
Senior Shortcut:
If you need CSRF exemption for a webhook (like Stripe), use @csrf_exempt on that view only — and never for user-facing forms. Token validation is the cheapest security you'll ever implement.
Key Takeaway
The csrf_token is a session-gated guard against request forgery. Never disable it on user-facing POST forms.

Q 38. How Do You Exclude Records That Match a Condition in Django ORM?

You exclude records using .exclude() or ~Q(). exclude() returns a QuerySet of objects that do not match the given lookup. It's logically equivalent to NOT in SQL. For a single condition, Model.objects.exclude(field=value) works. For complex OR conditions, use Q objects with the ~ operator: Model.objects.filter(~Q(field1=value1) | Q(field2=value2)). This avoids writing raw SQL. Remember: exclude() is evaluated lazily like any QuerySet. Chaining exclude() with filter() creates an AND condition: first filter includes, then exclude removes. For nullable fields, exclude(field=None) does not exclude rows where the field is NULL — SQL != and <> don't match NULL. Use exclude(field__isnull=True) or wrap with Q(field__isnull=True) | Q(...) to handle this correctly.

exclude_example.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — interview tutorial
from myapp.models import Order
from django.db.models import Q

# Exclude paid orders
paid_orders = Order.objects.exclude(status='paid')

# Exclude orders that are paid OR cancelled
active = Order.objects.filter(
    ~Q(status='paid') | ~Q(status='cancelled')
)

# Exclude NULL values explicitly (avoid gotcha)
no_date = Order.objects.exclude(
    Q(shipped_date__isnull=True) | Q(shipped_date='')
)

print(list(active))
Output
[<Order: Order 1>, <Order: Order 3>]
Production Trap:
exclude() on nullable fields does not exclude NULL rows. Always chain __isnull=True to avoid silent missing data.
Key Takeaway
Use exclude() for simple NOT, ~Q() for complex OR conditions, and handle NULL explicitly.

Both reduce database queries on related models, but they work differently. select_related works on ForeignKey and OneToOneField relationships — it performs a SQL JOIN and retrieves related objects in a single query. Use it when you know you'll access the related object's fields, like order.user.name. prefetch_related works on ManyToManyField and reverse ForeignKey relationships — it performs a separate query for each relationship and joins them in Python. Use it for reverse relations like author.books.all(). Choosing wrong kills performance: select_related on a ManyToMany creates a Cartesian product; prefetch_related on a ForeignKey runs unnecessary extra queries. For chained relations, select_related('user__profile') works, but prefetch_related('books__reviews') does not flatten — you need Prefetch objects for deeper nesting.

select_vs_prefetch.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — interview tutorial
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

# select_related: JOIN authors into book query
books = Book.objects.select_related('author')
for b in books:
    print(b.author.name)  # no extra query

# prefetch_related: separate query for each author's books
authors = Author.objects.prefetch_related('book_set')
for a in authors:
    print([b.title for b in a.book_set.all()])
Output
J.K. Rowling
['Harry Potter', 'Fantastic Beasts']
Production Trap:
Using select_related on a ManyToMany causes a massive JOIN explosion. For reverse FK relations, prefetch_related is the only correct option.
Key Takeaway
select_related = SQL JOIN (FK/O2O). prefetch_related = separate query + Python merge (M2M/reverse FK).

28. What Is Celery, and How Does It Integrate with Django?

Celery is a distributed task queue for asynchronous execution in Python. It offloads long-running or scheduled work (email sending, image processing, API calls) from the request-response cycle. Integration with Django uses django-celery-beat for periodic tasks and django-celery-results for storing results. You define tasks as decorated functions, then call them with .delay() or .apply_async(). Celery requires a message broker (Redis or RabbitMQ) and a worker process running separately. Key pattern: receive HTTP request, create background task, return response immediately. Celery workers pick up tasks from the broker. For scheduled tasks, set CELERY_BEAT_SCHEDULE in Django settings. Warning: Celery adds operational complexity — you must monitor workers, handle retries, and manage concurrency. For simple async needs, Django's built-in threading or django-background-tasks might suffice.

celery_tasks.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — interview tutorial
from celery import shared_task
from django.core.mail import send_mail

@shared_task
def send_welcome_email(user_email):
    send_mail(
        'Welcome!',
        'Thanks for signing up.',
        'from@example.com',
        [user_email],
        fail_silently=False,
    )

# In a view, call:
# send_welcome_email.delay('user@example.com')
Output
Task sent to broker. Worker will execute async.
Production Trap:
Celery workers must be restarted after code changes. If broker goes down, tasks queue indefinitely or drop. Always set task time limits and retry policies.
Key Takeaway
Celery = async task queue with broker + workers. Use for background jobs but know the ops overhead.
● Production incidentPOST-MORTEMseverity: high

The ORM-N+1 That Took Down a SaaS Dashboard

Symptom
A SaaS dashboard's order listing page took 6+ seconds to load, timing out the request and causing internal server errors during peak hours.
Assumption
The developer assumed that accessing order.product.name in the template would be optimized by Django's ORM, believing that caching would handle any performance issues.
Root cause
The view used Order.objects.filter(status='paid') without select_related('product'). The template iterated over orders and accessed order.product.name in each row, causing one query for the list and then N additional queries per order — the N+1 problem. With 500 orders, that's 501 queries instead of 1.
Fix
Changed the view to Order.objects.filter(status='paid').select_related('product'). Also added django-debug-toolbar to the development environment and configured the CONN_MAX_AGE setting to 60 seconds to reuse database connections.
Key lesson
  • Always inspect SQL queries in development with django-debug-toolbar — it highlights duplicate queries in red.
  • Use select_related() for ForeignKey relationships and prefetch_related() for ManyToMany/reverse FK — know the difference.
  • Never assume Django optimises related lookups; lazy evaluation hides the cost until the template executes.
Production debug guideSymptom → Action guide for common ORM pitfalls4 entries
Symptom · 01
Page loads slowly with increasing database connections
Fix
Check number of queries using django-debug-toolbar SQL panel. Look for duplicate queries in red.
Symptom · 02
Template rendering shows hundreds of identical queries
Fix
Move the offending ORM call into the view and add select_related() or prefetch_related() before passing data to template.
Symptom · 03
Queries are fast but response still slow
Fix
Check for missing index on filter fields. Run EXPLAIN on generated SQL. Add db_index=True or composite indexes.
Symptom · 04
select_related() seems to have no effect on ManyToMany
Fix
Replace select_related() with prefetch_related()select_related() silently fails on M2M fields without raising an error.
★ Django Production Quick-Fix Cheat SheetFast commands and fixes for common Django production issues.
N+1 queries in template
Immediate action
Add select_related() or prefetch_related() to view's queryset.
Commands
django-admin show_urls
from django.db import connection; print(connection.queries)
Fix now
Add 'django-debug-toolbar' as INSTALLED_APPS and check the SQL panel.
Middlewares not executing in expected order+
Immediate action
Review MIDDLEWARE list in settings.py — ensure SessionMiddleware before AuthenticationMiddleware.
Commands
python manage.py show_urls
python manage.py check --deploy
Fix now
Reorder MIDDLEWARE according to Django docs: Security, Session, Common, CsrfView, Authentication, Message, XFrame.
Signals not firing on model save+
Immediate action
Verify signal handlers are imported in the AppConfig.ready() method.
Commands
python manage.py shell -c "import io.thecodeforge.signals; print('registered')"
Check if `ready()` method exists in apps.py and imports the signals module.
Fix now
Add from . import signals inside the ready() method of the AppConfig class in apps.py.
Cache hit rate is low or cache is stale+
Immediate action
Configure proper cache backend (Redis) and set appropriate TIMEOUT. Use vary_on_headers for per-user caching.
Commands
python manage.py shell -c "from django.core.cache import cache; cache.keys('*')"
Check cache key prefixes and ensure invalidation strategy (e.g., cache.set version).
Fix now
Use @cache_page(60 * 15) for view caching, or low-level cache API for fine-grained control.
select_related vs prefetch_related
Aspectselect_related()prefetch_related()
Relationship typeForeignKey, OneToOneFieldManyToManyField, reverse ForeignKey
SQL strategySingle JOIN queryTwo separate queries + Python join
Number of queriesAlways 1Always 2 (regardless of row count)
Best forAccessing a parent object from a childAccessing many children from a parent
Memory usageLower — one result setHigher — two result sets merged in Python
Can be chained with filter()Yes — filters apply to JOINYes — with Prefetch() object for fine control
Works across database shardsNo — JOIN requires same DBYes — queries can hit different DBs

Key takeaways

1
Django's MVT puts the 'Controller' role in the URL dispatcher
Views are coordinators, not controllers. Business logic belongs in fat models or service layers, never in templates.
2
QuerySets are lazy
the database is only hit when you iterate, slice, or call list(). Chain filters freely; use select_related() for FK joins and prefetch_related() for M2M/reverse FK to kill N+1 queries.
3
Middleware executes top-to-bottom on requests and bottom-to-top on responses. Order in MIDDLEWARE matters
SessionMiddleware must always precede AuthenticationMiddleware or you'll get an AttributeError.
4
Use signals for genuine cross-app decoupling (like hooking into Django's built-in User model). For same-app logic, a direct function call is clearer, easier to test, and easier for future developers to trace.
5
Cache per-view for static pages, template fragments for user-agnostic sections, low-level API for computed data. Always vary cache by user context to avoid cross-user leaks.

Common mistakes to avoid

4 patterns
×

Calling QuerySet methods inside a template tag loop

Symptom
Django Debug Toolbar shows hundreds of identical SQL queries for a single page load.
Fix
Move all ORM calls into the view, use select_related()/prefetch_related(), and pass fully evaluated data to the template. Templates should never trigger database queries.
×

Forgetting to import signals in apps.py ready()

Symptom
Signal handlers silently never fire — no error, no traceback, just missing behaviour in production.
Fix
Add import yourapp.signals inside the ready() method of your AppConfig class. The import itself registers the @receiver decorators as a side effect.
×

Putting business logic in templates using custom template tags

Symptom
Price calculations, discount logic, or access control checks scattered across .html files, impossible to unit test.
Fix
Move all logic to model methods or service functions, call them in the view, and pass simple values to the template. Ask yourself: 'Could I test this without a browser?' If no, it's in the wrong place.
×

Using @cache_page without vary_on_headers for user-specific pages

Symptom
Users see cached responses meant for other users, leading to data leaks and wrong content.
Fix
Apply @vary_on_headers('Cookie') or @vary_on_cookie decorator on the view, or use template fragment caching instead.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Given a Django Model with an N+1 issue, how do you optimize it using sel...
Q02SENIOR
Explain the Django request-response lifecycle from Gunicorn to the Templ...
Q03SENIOR
How would you implement database sharding or read-replicas in a Django p...
Q04SENIOR
What is the difference between AuthenticationMiddleware and RemoteUserMi...
Q01 of 04SENIOR

Given a Django Model with an N+1 issue, how do you optimize it using select_related vs prefetch_related?

ANSWER
First identify the relationship type. For ForeignKey/OneToOne, use select_related() to perform a SQL JOIN, reducing queries to 1. For ManyToMany or reverse ForeignKey, use prefetch_related() which performs two separate queries and merges in Python. Always add django-debug-toolbar to verify the number of queries before and after.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the N+1 problem in Django and how do you fix it?
02
How do you handle heavy, long-running tasks in a Django view?
03
What are Django's generic class-based views (GCBVs) and when should you use them?
04
How do you debug a slow Django page in production?
05
What is the difference between process_request and process_response in middleware?
N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Drawn from code that ran under real load.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Python Interview. Mark it forged?

12 min read · try the examples if you haven't

Previous
Python Data Structures Interview Q
4 / 4 · Python Interview
Next
Top 50 JavaScript Interview Q