- 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_relatedvsprefetch_related,F()andselect_for_updatefor concurrency. - FastAPI's most important topic is async def vs def: blocking code inside
async defstalls 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
Runtime
GIL, threads vs processes vs asyncio, how Gunicorn and Uvicorn workers serve requests.
Framework
Request lifecycle, ORM behaviour, middleware, dependency injection, validation, serialization.
Data
Query counts, indexes, transactions, locking, migrations on large tables.
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?
| Model | Good for | Cost |
|---|---|---|
| Multiple processes | CPU-bound work; isolating crashes; using all cores | Memory per process; no shared state |
| Threads | Blocking I/O with libraries that are not async | GIL limits CPU parallelism; shared-state bugs |
| asyncio | Many concurrent I/O waits (thousands of sockets) in one thread | Everything 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.
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.
- Server to handler: Gunicorn (WSGI) or Uvicorn (ASGI) passes the request to Django's handler, which builds an
HttpRequest. - Middleware (request phase): each middleware in
MIDDLEWAREruns top to bottom: security headers, sessions, CSRF, authentication (which attachesrequest.user). - URL resolution: the URL resolver matches the path against
urlpatternsand picks the view with its captured arguments. - View: the view runs business logic, uses the ORM, and returns an
HttpResponse(rendered template, JSON, redirect). - Middleware (response phase): middleware runs bottom to top on the way out, and can modify the response.
- Back to the server, which sends it to the client. Exceptions are routed through middleware's
process_exceptionhooks 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.
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.
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.
# 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_related | prefetch_related | |
|---|---|---|
| Relationship | Forward ForeignKey, OneToOne | Reverse ForeignKey, ManyToMany (also works for FK) |
| SQL | One query with JOIN | One extra query per relation using WHERE id IN (...) |
| Joining | In the database | In Python |
| Risk | Wide rows; joins across many tables get heavy | Large 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.
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.
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.
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.
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.
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 defandawaitthe 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 defplusrequests.get()ortime.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.
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?
- Measure first: check latency percentiles (p50, p95, p99) in your APM or logs to see whether it is always slow or only sometimes.
- Count queries: django-debug-toolbar, Django Silk, or SQL logging. Many slow Python endpoints are N+1 queries.
- Explain the slow query:
EXPLAIN ANALYZEin PostgreSQL; add or fix indexes (see B-tree indexing explained). - Profile Python: py-spy on a running process, or cProfile locally, if time is spent in Python rather than waiting.
- Check external calls: missing timeouts, sequential calls that could run concurrently, or a slow third-party API.
- 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
| Aspect | Django (+ DRF) | FastAPI |
|---|---|---|
| Philosophy | Batteries included: ORM, admin, auth, migrations, forms | Minimal core; choose your ORM, auth and migrations |
| Concurrency model | Sync-first; async views and partial async ORM | Async-first on ASGI; sync endpoints run in a thread pool |
| Validation | Forms and DRF serializers | Pydantic models from type hints |
| API docs | Via add-ons such as drf-spectacular | OpenAPI and interactive docs built in |
| Admin panel | Excellent, built in | None built in |
| Typical fit | Products with many models, internal tools, content-heavy apps | Microservices, 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."
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".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
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 →