Interview Questions · Python Backend

Django & FastAPI Interview Questions for Python Backend Roles

The questions Python backend interviewers ask about Django's ORM and request cycle, FastAPI's async model and dependency injection, Pydantic, background jobs and performance, with code that was actually run and the follow-ups that test real production experience.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 23 min read
Two Python-inspired request pipelines flowing through middleware layers into a database
TL;DR
  • Python backend interviews test three layers: the runtime (GIL, asyncio, workers), the framework (Django's request cycle and ORM, FastAPI's dependencies and Pydantic), and production judgment (N+1 queries, transactions, background jobs, caching).
  • Django's most important interview topic is the ORM: lazy QuerySets, select_related vs prefetch_related, F() and select_for_update for concurrency.
  • FastAPI's most important topic is async def vs def: blocking code inside async def stalls the whole worker.
  • Know Pydantic v2's API (model_validate, model_dump, field_validator); v1-style answers are a sign of outdated experience.
  • All Django and FastAPI code below was run against recent releases of both frameworks.

Python backend interviews in India have shifted. A few years ago, "Django developer" interviews were mostly about models and views. Today the same companies, along with fintech, SaaS and AI startups, ask about async I/O, API design, query performance and background processing, because their Python services now sit on the hot path of real traffic. Whether you work in Django, FastAPI or both, the questions below are the ones that come up, grouped from runtime fundamentals to production.

If you need a refresher on core language topics (decorators, generators, mutability), start with our Python interview questions. For HTTP and API design questions that apply to both frameworks, see the REST API interview guide.

What Python Backend Interviewers Actually Test

Layer 1

Runtime

GIL, threads vs processes vs asyncio, how Gunicorn and Uvicorn workers serve requests.

Layer 2

Framework

Request lifecycle, ORM behaviour, middleware, dependency injection, validation, serialization.

Layer 3

Data

Query counts, indexes, transactions, locking, migrations on large tables.

Layer 4

Production

Background jobs, caching, observability, deploying with zero downtime, scaling.

Python Runtime for Backend Engineers: Questions 1-6

Q1. What is the GIL and does it matter for web backends?

The Global Interpreter Lock in CPython lets only one thread execute Python bytecode at a time within a process. It is released during blocking I/O (socket reads, database calls) and by many C extensions during heavy computation. For typical web backends that mostly wait on I/O, threads still give useful concurrency. For CPU-bound work (image processing, large JSON transformations, ML inference in pure Python), threads do not scale across cores, so you use multiple processes, a task queue, or native libraries. CPython 3.13 introduced an experimental free-threaded build without the GIL (PEP 703); it is not yet the default, so mention it as a direction rather than something to rely on.

Q2. Threads vs processes vs asyncio: when do you use each?

ModelGood forCost
Multiple processesCPU-bound work; isolating crashes; using all coresMemory per process; no shared state
ThreadsBlocking I/O with libraries that are not asyncGIL limits CPU parallelism; shared-state bugs
asyncioMany concurrent I/O waits (thousands of sockets) in one threadEverything on the path must be non-blocking; one blocking call stalls all

Production servers combine them: Gunicorn runs several worker processes (one per core or so), and each worker handles concurrency with threads (sync Django) or an event loop (Uvicorn workers for FastAPI or async Django).

Q3. How does asyncio work?

An event loop runs coroutines. When a coroutine hits await on I/O that is not ready, it yields control back to the loop, which runs another ready coroutine, and resumes the first when its I/O completes (the loop uses OS mechanisms like epoll to know). It is cooperative multitasking in a single thread: concurrency without parallelism. That is why one CPU-heavy or blocking call inside a coroutine freezes every other request on that loop.

Python
import asyncio
import time

async def call_service(name, delay):
    await asyncio.sleep(delay)          # stands in for a non-blocking HTTP or DB call
    return f"{name} done"

async def main():
    start = time.perf_counter()
    results = await asyncio.gather(
        call_service("profile", 0.3),
        call_service("orders", 0.5),
        call_service("wallet", 0.2),
    )
    print(results, f"{time.perf_counter() - start:.2f}s")   # about 0.5s, not 1.0s

asyncio.run(main())

Q4. What are WSGI and ASGI?

WSGI is the synchronous interface between Python web apps and servers: one call per request, returning a response. Gunicorn with sync workers and uWSGI serve WSGI apps. ASGI is its asynchronous successor: apps are coroutines and can handle long-lived connections such as WebSockets and server-sent events. Uvicorn, Hypercorn and Daphne are ASGI servers. Django supports both (wsgi.py and asgi.py); FastAPI is ASGI only, built on Starlette.

Q5. How many Gunicorn workers should you run?

The commonly cited starting point from Gunicorn's documentation is (2 × CPU cores) + 1 for sync workers. Then tune: more workers if requests are I/O-heavy and CPU is idle; fewer if memory is the constraint (each worker holds a full copy of the app). For FastAPI, run Uvicorn workers (one event loop per process), usually one or two per core. Always set a request timeout and max_requests with jitter to recycle workers that leak memory.

Q6. What is the difference between a decorator and middleware?

A decorator wraps a single function (for example, @login_required on one Django view or a caching decorator on a service function). Middleware wraps every request passing through the application. Use decorators for per-endpoint concerns and middleware for cross-cutting ones like request IDs, timing, CORS and authentication context.

Django Core: Questions 7-13

Q7. Walk through Django's request-response lifecycle.

  1. Server to handler: Gunicorn (WSGI) or Uvicorn (ASGI) passes the request to Django's handler, which builds an HttpRequest.
  2. Middleware (request phase): each middleware in MIDDLEWARE runs top to bottom: security headers, sessions, CSRF, authentication (which attaches request.user).
  3. URL resolution: the URL resolver matches the path against urlpatterns and picks the view with its captured arguments.
  4. View: the view runs business logic, uses the ORM, and returns an HttpResponse (rendered template, JSON, redirect).
  5. Middleware (response phase): middleware runs bottom to top on the way out, and can modify the response.
  6. Back to the server, which sends it to the client. Exceptions are routed through middleware's process_exception hooks and Django's error handlers.

Q8. What is MVT, and how does it differ from MVC?

Django calls its pattern Model-View-Template. The model is the data layer, the template is presentation, and the view holds request-handling logic, roughly what MVC calls the controller. The framework itself (URL dispatcher plus request handling) plays the rest of the controller role. In API-only projects with Django REST Framework, templates largely disappear and serializers take over presentation.

Q9. Write a custom Django middleware.

Python (Django)
import logging
import time
import uuid

logger = logging.getLogger(__name__)

class RequestTimingMiddleware:
    """Adds a request ID and logs latency for every request."""

    def __init__(self, get_response):
        self.get_response = get_response          # called once at server start

    def __call__(self, request):
        request.request_id = request.headers.get("X-Request-ID", uuid.uuid4().hex)
        start = time.perf_counter()
        response = self.get_response(request)     # everything below this middleware
        elapsed_ms = (time.perf_counter() - start) * 1000
        response["X-Request-ID"] = request.request_id
        logger.info("%s %s %s %.1fms", request.method, request.path,
                    response.status_code, elapsed_ms)
        return response

Add its dotted path to MIDDLEWARE. Order matters: put it near the top to time the whole stack. Follow-ups: middleware can short-circuit by returning a response without calling get_response (a maintenance-mode check), and Django supports async-capable middleware when serving under ASGI.

Q10. How does Django protect against CSRF, XSS and SQL injection?

CSRF: CsrfViewMiddleware requires a secret token (from a cookie, echoed in a form field or header) on unsafe methods, which a malicious third-party site cannot read. XSS: templates auto-escape variables; mark_safe and |safe turn that off, so they must only be used on trusted content. SQL injection: the ORM parameterizes queries; danger returns with raw() or extra() plus string formatting. Also set DEBUG = False, a secret SECRET_KEY from the environment, ALLOWED_HOSTS, and secure cookie settings in production.

Q11. Why should you define a custom user model at the start of a project?

Swapping AUTH_USER_MODEL after the first migrations is painful, because foreign keys across the project and in third-party apps already point to auth.User. Starting with class User(AbstractUser): pass costs nothing and lets you add fields like phone number or use email as the login field later. Django's own documentation recommends this.

Q12. What are signals, and why are they often discouraged?

Signals (post_save, pre_delete, custom ones) let decoupled code react to events. The problems: the control flow is invisible when reading the code that triggers them; they run synchronously in the same transaction, so a slow receiver slows the request; they do not fire for QuerySet.update() or bulk_create(); and a failing receiver can break a save in surprising ways. Prefer explicit service-layer calls, and for side effects like emails, enqueue a task after commit with transaction.on_commit.

Q13. How do you run migrations safely on a large production table?

Separate schema changes from code changes and make each step backward compatible: add a nullable column (cheap) rather than a non-null column with a default on old database versions; backfill data in batches in a separate migration or job; then add the constraint. Rename by add-copy-switch-drop rather than RenameField while old code is still running. On PostgreSQL, create indexes with AddIndexConcurrently (from django.contrib.postgres.operations) in a non-atomic migration to avoid locking writes. Run sqlmigrate to see the actual SQL before applying.

Django ORM: Questions 14-24

The examples below use this schema.

Python (Django models)
from django.db import models

class Customer(models.Model):
    name = models.CharField(max_length=100)
    city = models.CharField(max_length=50, db_index=True)

class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="orders")
    amount_paise = models.BigIntegerField()
    status = models.CharField(max_length=20, default="CREATED")
    created_at = models.DateTimeField(auto_now_add=True)

class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items")
    sku = models.CharField(max_length=40)
    qty = models.PositiveIntegerField()

class Product(models.Model):
    sku = models.CharField(max_length=40, unique=True)
    stock = models.IntegerField()

Q14. What does it mean that QuerySets are lazy?

Building a QuerySet (Order.objects.filter(status="PAID").order_by("-created_at")) does not hit the database. The query runs when the QuerySet is evaluated: iterated, sliced with a step, converted with list(), len() or bool(), or when you call methods like count(), exists() or first(). Results are cached on the QuerySet, so iterating the same QuerySet twice queries once, but calling .filter() again creates a new QuerySet and a new query.

Q15. What is the N+1 query problem? Show it and fix it.

Python (Django)
# N+1: one query for orders, then one query per order for its customer
for order in Order.objects.all():
    print(order.customer.name)

# Fixed: one query with a SQL JOIN
for order in Order.objects.select_related("customer"):
    print(order.customer.name)

# Reverse FK / many-to-many: two queries total, joined in Python
for order in Order.objects.prefetch_related("items"):
    print(order.id, [item.sku for item in order.items.all()])

With 100 orders, the first loop runs 101 queries; the second runs 1; the third runs 2. We verified these counts with Django's CaptureQueriesContext on an in-memory database. In development, django-debug-toolbar or assertNumQueries in tests catches N+1 regressions before production does.

Q16. select_related vs prefetch_related?

select_relatedprefetch_related
RelationshipForward ForeignKey, OneToOneReverse ForeignKey, ManyToMany (also works for FK)
SQLOne query with JOINOne extra query per relation using WHERE id IN (...)
JoiningIn the databaseIn Python
RiskWide rows; joins across many tables get heavyLarge IN lists; filtering the prefetched relation needs Prefetch()

Customize prefetching with Prefetch("items", queryset=OrderItem.objects.filter(qty__gt=1), to_attr="bulk_items"). Note that calling .filter() on a prefetched relation inside the loop discards the prefetch and queries again.

Q17. What are F expressions and why do they matter for concurrency?

An F() expression refers to a column's value in the database, so the arithmetic happens in SQL. Read-modify-write in Python loses updates under concurrency: two requests read stock 5, both write 4. With F expressions, the database applies both decrements atomically.

Python (Django)
from django.db.models import F

def reserve_stock(sku, qty):
    # single atomic UPDATE ... SET stock = stock - qty WHERE sku = ... AND stock >= qty
    updated = Product.objects.filter(sku=sku, stock__gte=qty).update(stock=F("stock") - qty)
    return updated == 1     # False means not enough stock (or unknown SKU)

This conditional update is the simplest race-free inventory check and is a strong answer in flash-sale style questions (see flash sale checkout design).

Q18. How do transactions work in Django, and what is select_for_update?

By default Django runs in autocommit mode: each query commits immediately. transaction.atomic() (as a decorator or context manager) wraps a block in a transaction; nested atomic blocks become savepoints. select_for_update() issues SELECT ... FOR UPDATE, locking the selected rows until the transaction ends, so concurrent transactions touching those rows wait. It must be used inside atomic.

Python (Django)
from django.db import transaction

def transfer(from_id, to_id, amount):
    with transaction.atomic():
        # lock both wallets in a consistent order to avoid deadlocks
        ids = sorted([from_id, to_id])
        wallets = {w.id: w for w in Wallet.objects.select_for_update().filter(id__in=ids).order_by("id")}
        src, dst = wallets[from_id], wallets[to_id]
        if src.balance < amount:
            raise ValueError("insufficient balance")
        src.balance -= amount
        dst.balance += amount
        src.save(update_fields=["balance"])
        dst.save(update_fields=["balance"])
        transaction.on_commit(lambda: notify_transfer.delay(from_id, to_id, amount))

Notice on_commit: the notification task is enqueued only if the transaction commits, so a rollback never sends a "transfer done" message. Isolation levels and locking are covered in depth in our ACID and isolation levels guide.

Q19. Which operations bypass save() and signals?

QuerySet.update(), bulk_create(), bulk_update() and QuerySet.delete() do not call your model's save() method or send pre_save/post_save signals (bulk delete does send delete signals for collected objects, but via a different path). Also, auto_now fields are not updated by update(). If business logic lives in save() overrides, bulk paths silently skip it: another reason to keep logic in a service layer.

Q20. How do aggregate and annotate differ?

aggregate() returns a single dictionary summarizing the whole QuerySet. annotate() adds a computed value to each row, usually with a GROUP BY.

Python (Django)
from django.db.models import Count, Sum, Q

Order.objects.filter(status="PAID").aggregate(total=Sum("amount_paise"), n=Count("id"))
# {'total': 1234500, 'n': 42}

top_cities = (Customer.objects
              .values("city")
              .annotate(revenue=Sum("orders__amount_paise", filter=Q(orders__status="PAID")))
              .order_by("-revenue")[:5])

Q21. What are Q objects?

Q objects build complex conditions with OR (|), AND (&) and NOT (~): Order.objects.filter(Q(status="PAID") | Q(amount_paise__gte=100000), ~Q(customer__city="Pune")). Keyword arguments to filter() are always ANDed, so Q is needed for OR logic.

Q22. How do you reduce data loaded by a query?

only("id", "status") and defer("big_text_field") load partial model instances (accessing a deferred field triggers another query per object, a hidden N+1). values() and values_list(..., flat=True) return dicts or tuples without model instances, which is much cheaper for large exports. For huge result sets, iterator(chunk_size=2000) streams rows without caching them all in memory.

Q23. exists() vs count() vs len()?

To check whether any row matches, use exists(): it runs a SELECT ... LIMIT 1. count() runs SELECT COUNT(*), fine if you need the number. len(qs) loads every row into memory and counts in Python, which is only sensible if you are about to use the rows anyway (then the QuerySet's cache is reused).

Q24. Does Django support async ORM queries?

Django added async views in 3.1 and an async interface to the ORM in 4.1 (aget(), acreate(), async for over QuerySets, acount()). Under the hood, much of the database work still runs through a thread via sync_to_async, so it prevents blocking the event loop rather than making queries faster. Transactions are not yet supported in async code in the same way, so atomic blocks are typically wrapped in a sync function. The practical rule: async Django pays off for views that wait on external HTTP calls or many concurrent slow I/O operations, not for plain CRUD.

Django REST Framework: Questions 25-29

Q25. What does a DRF serializer do?

It converts model instances to primitive data for JSON (serialization), and validates incoming data and converts it back into objects (deserialization). ModelSerializer generates fields and default create/update from the model.

Python (DRF)
from rest_framework import serializers

class OrderSerializer(serializers.ModelSerializer):
    customer_name = serializers.CharField(source="customer.name", read_only=True)

    class Meta:
        model = Order
        fields = ["id", "customer", "customer_name", "amount_paise", "status", "created_at"]
        read_only_fields = ["status", "created_at"]

    def validate_amount_paise(self, value):
        if value <= 0:
            raise serializers.ValidationError("amount must be positive")
        return value

Follow-up: customer_name reads order.customer.name for every row, so the view's queryset must use select_related("customer"), or the list endpoint is an N+1.

Q26. APIView vs GenericAPIView vs ViewSet?

APIView gives you methods per HTTP verb with DRF's request parsing, authentication and permissions. Generic views plus mixins (ListCreateAPIView) add standard list, create, retrieve, update and destroy behaviour from a queryset and serializer. ViewSets group the actions for a resource and pair with routers that generate URLs. Use ViewSets for standard CRUD resources and plain APIViews for operations that are not CRUD, like "initiate refund".

Q27. How do authentication and permissions work in DRF?

Authentication classes (session, token, JWT via a library like SimpleJWT) identify request.user. Permission classes (IsAuthenticated, IsAdminUser, or custom has_permission/has_object_permission) decide whether the request is allowed. Object-level permissions run only when the view calls get_object(), so for list endpoints you must also filter the queryset to the user's own data. Missing that filter is a common security bug (an insecure direct object reference).

Q28. How do you paginate and throttle in DRF?

Set DEFAULT_PAGINATION_CLASS: PageNumberPagination or LimitOffsetPagination for simple cases, CursorPagination for large, frequently changing tables because it uses keyset pagination and avoids deep OFFSET scans. Throttling classes (UserRateThrottle, AnonRateThrottle, ScopedRateThrottle) limit request rates using the cache backend; for distributed enforcement, the cache must be shared, such as Redis. See rate limiter design for the algorithms.

Q29. How do you version an API in DRF?

DRF supports URL path versioning (/api/v1/), namespace, host, query parameter and Accept-header versioning; the chosen version is available as request.version. URL path versioning is the most common because it is visible and cache-friendly. The harder question is policy: additive changes do not need a new version; removing or changing fields does, and old versions need a deprecation timeline.

FastAPI: Questions 30-40

Q30. What is FastAPI built on, and why is it popular?

FastAPI is built on Starlette (the ASGI toolkit for routing, middleware, WebSockets) and Pydantic (data validation). You declare request parameters and bodies with Python type hints; FastAPI validates input, serializes output, and generates an OpenAPI schema with interactive docs at /docs. It is popular for API-first services and ML model serving because it is fast to write, type-safe, and async-native.

Q31. Build a small FastAPI service with validation, dependencies and auth.

Python (FastAPI)
from contextlib import asynccontextmanager
from typing import Annotated

from fastapi import Depends, FastAPI, Header, HTTPException, Request, status
from pydantic import BaseModel, Field


class OrderIn(BaseModel):
    sku: str = Field(min_length=1, max_length=40)
    qty: int = Field(gt=0, le=10)


class OrderOut(BaseModel):
    id: int
    sku: str
    qty: int
    status: str


class Database:
    """Stand-in for a real connection pool / session factory."""
    def __init__(self):
        self.orders = {}
        self.next_id = 1

    def session(self):
        return self

    def close(self):
        pass


@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.db = Database()      # open pools and clients once at startup
    yield
    # close pools and clients at shutdown


app = FastAPI(lifespan=lifespan)


def get_db(request: Request):
    db = request.app.state.db.session()
    try:
        yield db                   # code after yield runs after the response
    finally:
        db.close()


def get_current_user(authorization: Annotated[str | None, Header()] = None) -> str:
    if authorization != "Bearer demo-token":
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token")
    return "user-42"


DB = Annotated[Database, Depends(get_db)]
CurrentUser = Annotated[str, Depends(get_current_user)]


@app.post("/orders", response_model=OrderOut, status_code=status.HTTP_201_CREATED)
async def create_order(order: OrderIn, db: DB, user: CurrentUser):
    order_id = db.next_id
    db.next_id += 1
    record = {"id": order_id, "owner": user, "status": "CREATED", **order.model_dump()}
    db.orders[order_id] = record
    return record                  # response_model drops the internal "owner" field


@app.get("/orders/{order_id}", response_model=OrderOut)
async def get_order(order_id: int, db: DB, user: CurrentUser):
    record = db.orders.get(order_id)
    if record is None or record["owner"] != user:
        raise HTTPException(status_code=404, detail="order not found")
    return record

We exercised this with FastAPI's TestClient: a valid request returns 201 without the owner field, qty: 0 returns 422 with a validation error, and a missing token returns 401. Talking through those three outcomes is a good way to present it.

Q32. How does dependency injection work in FastAPI?

A dependency is any callable declared with Depends(). FastAPI resolves the dependency graph per request: it inspects each dependency's own parameters (which can be other dependencies, headers, query parameters), calls them, and passes the results in. Dependencies with yield act like context managers for setup and teardown, such as opening and closing a database session. Within one request, a dependency used in several places is called once and cached unless you pass use_cache=False. In tests, app.dependency_overrides[get_db] = fake_db swaps implementations without patching.

Q33. async def vs def for FastAPI endpoints?

FastAPI runs async def endpoints directly on the event loop, and plain def endpoints in a thread pool. So:

  • Using an async driver (asyncpg, httpx.AsyncClient, async SQLAlchemy)? Use async def and await the calls.
  • Using blocking libraries (requests, psycopg2, a sync SDK)? Use plain def, so the call blocks a worker thread instead of the loop.
  • The bug interviewers look for: async def plus requests.get() or time.sleep(). That blocks the event loop and every concurrent request on that worker waits.

If you must call blocking code from an async function, use await run_in_threadpool(fn, ...) from Starlette or await asyncio.to_thread(fn, ...).

Q34. What changed in Pydantic v2?

Pydantic v2's validation core is written in Rust (pydantic-core), making validation much faster than v1. The API changed: parse_obj became model_validate, .dict() became model_dump(), .json() became model_dump_json(), @validator became @field_validator (and @model_validator for cross-field checks), and class Config became model_config = ConfigDict(...). ORM mode is now from_attributes=True.

Python (Pydantic v2)
from pydantic import BaseModel, ConfigDict, field_validator, model_validator

class Transfer(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    from_account: str
    to_account: str
    amount_paise: int

    @field_validator("amount_paise")
    @classmethod
    def positive(cls, v: int) -> int:
        if v <= 0:
            raise ValueError("amount must be positive")
        return v

    @model_validator(mode="after")
    def different_accounts(self):
        if self.from_account == self.to_account:
            raise ValueError("cannot transfer to the same account")
        return self

t = Transfer.model_validate({"from_account": " A1 ", "to_account": "B2", "amount_paise": 5000})
print(t.model_dump())   # {'from_account': 'A1', 'to_account': 'B2', 'amount_paise': 5000}

Q35. What does response_model do?

It validates and filters the endpoint's return value against a schema and documents it in OpenAPI. Fields not in the response model (like owner or a password hash) are dropped, which prevents accidental data leaks. Options such as response_model_exclude_none=True trim output further. Using separate input and output models (UserCreate with a password, UserOut without) is the standard pattern.

Q36. How are path, query, header and body parameters distinguished?

By declaration: a parameter whose name appears in the path template is a path parameter; a simple-typed parameter not in the path is a query parameter; a parameter typed as a Pydantic model is the JSON body; and Header(), Cookie(), Query(), Path() or Body() make the source explicit and add constraints, like limit: Annotated[int, Query(le=100)] = 20.

Q37. BackgroundTasks vs Celery?

FastAPI's BackgroundTasks runs a function after the response is sent, in the same process. It is fine for small, non-critical work like writing an audit log. It is not durable: if the process restarts, the task is lost, and heavy tasks compete with request handling. For emails, report generation, payment reconciliation or anything that must survive restarts, retry, or be scheduled, use a real queue: Celery, RQ, Dramatiq or arq, with Redis or RabbitMQ as the broker.

Q38. What are lifespan events?

Code that runs once when the application starts and once when it shuts down, used to create and close connection pools, HTTP clients, or load an ML model into memory. Modern FastAPI uses a lifespan async context manager (shown in Q31); the older @app.on_event("startup") decorators are deprecated.

Q39. How do you handle errors consistently in FastAPI?

Raise HTTPException for expected client errors. Register exception handlers with @app.exception_handler(MyDomainError) to map domain errors to responses in one place. Customize validation error responses by handling RequestValidationError. Return a consistent error shape (code, message, request ID) so clients and logs can rely on it.

Q40. How do you test FastAPI apps?

Use TestClient (built on httpx) for synchronous tests, or httpx.AsyncClient with an ASGI transport for async tests. Override dependencies to inject a test database or fake user. Using with TestClient(app) as client: runs the lifespan events, which matters if your startup code creates the resources your endpoints use.

Production and Performance: Questions 41-46

Q41. An endpoint is slow. How do you investigate?

  1. Measure first: check latency percentiles (p50, p95, p99) in your APM or logs to see whether it is always slow or only sometimes.
  2. Count queries: django-debug-toolbar, Django Silk, or SQL logging. Many slow Python endpoints are N+1 queries.
  3. Explain the slow query: EXPLAIN ANALYZE in PostgreSQL; add or fix indexes (see B-tree indexing explained).
  4. Profile Python: py-spy on a running process, or cProfile locally, if time is spent in Python rather than waiting.
  5. Check external calls: missing timeouts, sequential calls that could run concurrently, or a slow third-party API.
  6. Cache and offload: cache expensive reads in Redis; move non-essential work to a queue.

Q42. How do you cache in Django and FastAPI?

Django has a cache framework with per-view caching, template fragment caching and a low-level API (cache.get, cache.set), backed by Redis or Memcached in production. FastAPI has no built-in cache; you call Redis directly (redis-py has an asyncio client) or use a small library. The hard parts are the same in both: choosing keys, TTLs, invalidation on writes, and preventing a stampede when a hot key expires. Our Redis deep dive covers the patterns.

Q43. How do you manage database connections?

Django opens a connection per request by default; CONN_MAX_AGE keeps connections alive across requests in a worker, and Django 5.1 added built-in connection pooling for PostgreSQL via psycopg 3's pool. Many teams also run PgBouncer in front of PostgreSQL. With FastAPI and SQLAlchemy, you configure the engine's pool size and overflow and create one session per request through a dependency. The interview point: total connections = workers × pool size, and it must stay below the database's limit.

Q44. How does Celery work, and what are its pitfalls?

Producers (your web app) send task messages to a broker (Redis or RabbitMQ); Celery workers consume and execute them; results can be stored in a result backend. Pitfalls interviewers like: passing model instances instead of IDs (stale data, serialization issues); tasks that are not idempotent while retries or acks_late can run them twice; enqueuing a task before the database transaction commits, so the worker cannot find the row (fix with transaction.on_commit); and long tasks blocking short ones in the same queue (fix with separate queues).

Q45. How do you structure settings and secrets?

Keep configuration in environment variables (the twelve-factor approach), read through django-environ or Pydantic's settings support (pydantic-settings) in FastAPI. Never commit secrets; load them from a secrets manager in production. Separate settings for local, staging and production, with DEBUG off and strict hosts and CORS in production.

Q46. How do you deploy Python services with zero downtime?

Run multiple instances behind a load balancer and roll them one at a time with readiness checks; Gunicorn also supports graceful reloads. Make database migrations backward compatible (Q13) so old and new code can run together during the rollout. Containerize with Docker, and on Kubernetes use rolling deployments with readiness and liveness probes; see Docker and Kubernetes for backend engineers.

Django vs FastAPI: How to Answer the Comparison Question

AspectDjango (+ DRF)FastAPI
PhilosophyBatteries included: ORM, admin, auth, migrations, formsMinimal core; choose your ORM, auth and migrations
Concurrency modelSync-first; async views and partial async ORMAsync-first on ASGI; sync endpoints run in a thread pool
ValidationForms and DRF serializersPydantic models from type hints
API docsVia add-ons such as drf-spectacularOpenAPI and interactive docs built in
Admin panelExcellent, built inNone built in
Typical fitProducts with many models, internal tools, content-heavy appsMicroservices, ML serving, high-concurrency I/O APIs

A strong answer avoids loyalty: "For a new internal operations tool with many models and an admin panel, Django gets us to production fastest. For a model-serving or aggregation service that fans out to several APIs, FastAPI's async model and typed contracts fit better. In both, the database design will matter more for performance than the framework."

⚠️
Red flags in Python backend interviewsBlocking calls inside async def; not knowing how many queries an endpoint runs; read-modify-write updates without F() or locking; sending emails inside the request transaction; Pydantic v1 syntax presented as current; and "the GIL means Python can't do concurrency".
💡
Prepare one production storyHave one concrete example ready: "Our order list endpoint ran 240 queries; I added select_related and prefetch_related, cut it to 3, and p95 latency fell from 1.8s to 220ms." Specific numbers from your own work, honestly reported, beat any memorized answer.

To round out your preparation, practise SQL alongside the ORM with our SQL interview questions, and see the backend developer roadmap for what to learn next.

Frequently Asked Questions

Should I learn Django or FastAPI for backend interviews?▾
Learn the one your target companies use, and understand the other at a conceptual level. Django is common in established products and startups that need an admin panel, ORM and auth quickly. FastAPI is common for API-first services, ML model serving and newer microservices. The ORM, HTTP, database and async concepts transfer between them.
What is the most asked Django ORM question?▾
The N+1 query problem and the difference between select_related and prefetch_related. Interviewers also ask about lazy QuerySets, F expressions for race-free updates, select_for_update inside transaction.atomic, and when bulk operations skip save() and signals.
Is FastAPI faster than Django?▾
For I/O-heavy APIs running on an async stack, FastAPI usually handles more concurrent requests per process, and its request validation through Pydantic v2 is fast. In real applications, database queries and external calls dominate latency, so query design and caching matter far more than framework choice.
What happens if I call a blocking function inside an async def endpoint in FastAPI?▾
It blocks the event loop, so every other request handled by that worker process waits until it finishes. Either declare the endpoint with plain def so FastAPI runs it in a thread pool, use an async library for the I/O, or offload the call with run_in_threadpool or asyncio.to_thread.
Does the GIL make Python bad for backend services?▾
Not for typical web backends, which spend most of their time waiting on databases and networks; the GIL is released during I/O, and async or multiple worker processes handle concurrency. CPU-heavy work should go to multiple processes, task queues or native extensions. Free-threaded builds of CPython also exist experimentally since 3.13.
Do Python backend interviews include system design?▾
For roles with two or more years of experience, yes. Expect to design APIs, choose between synchronous calls and queues such as Celery with Redis or RabbitMQ, add caching, and explain how you would scale workers and the database.
How do I explain my Django or FastAPI project in an interview?▾
Describe the request path end to end: entry point, middleware or dependencies, validation, business logic, database queries and response. Then share one real problem you solved, such as an N+1 query or a slow endpoint, with how you measured it and the result.

Become Interview-Ready as a Backend Engineer

Prepflix's backend track covers APIs, databases, caching and system design with mock interviews tailored to Python backend roles.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.