When you walk into a senior Python backend interview, the interviewer isn't looking for textbook definitions—they want to know how you've solved real problems at scale. Python backend interview questions: a step-by-step guide covers the topics that separate candidates who've shipped production systems from those who haven't. We'll walk through async patterns, query optimization, scaling strategies, and security practices that actually matter in the field.
How does async work? Event loop, coroutines and cooperative scheduling
Async/await is where many Python developers stumble. You need to understand not just the syntax, but why it exists and how it differs from threading. The event loop is the heart of it: a single-threaded scheduler that runs coroutines cooperatively. When a coroutine hits an await, it yields control back to the event loop, which can then run another coroutine. This is different from OS-level threading—there's no preemption, no race conditions on shared state, and no GIL contention.
Here's a concrete scenario: you're building a web scraper that needs to fetch data from 100 URLs. If you use threading, you'll hit the GIL and context-switching overhead. With async, you're managing 100 concurrent requests in a single thread.
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.text()
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
return await asyncio.gather(*tasks)
# Run it
urls = ['http://example.com/' + str(i) for i in range(100)]
results = asyncio.run(fetch_all(urls))
Interviewers often ask: "What happens if you forget to await?" If you call an async function without await, you get a coroutine object that never executes. You'll see a warning like coroutine was never awaited. Another classic: "Can you use async inside a synchronous function?" No—you can't await in a non-async function. You need asyncio.run() or loop.run_until_complete() at the entry point.
The key insight is that async doesn't make your code faster—it makes I/O-bound operations concurrent. CPU-bound work still needs multiprocessing to bypass the GIL.
How do you avoid N+1 queries? Select_related, prefetch_related or selectinload
This question appears in almost every serious backend interview. N+1 happens when you load a parent object, then loop through its children, triggering a query for each child. With 1,000 posts, you go from 1 query to 1,001.
The solution depends on the relationship and your ORM. In Django ORM, select_related() joins the related table in a single query—use it for ForeignKey and OneToOne. prefetch_related() runs a separate query and stitches the results in Python—use it for ManyToMany and reverse ForeignKey. In SQLAlchemy, you'd use selectinload() for the same purpose.
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Post(models.Model):
author = models.ForeignKey(Author, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
published_at = models.DateTimeField()
# Bad: N+1
posts = Post.objects.all()
for post in posts:
print(post.author.name) # Query per post
# Good: select_related for ForeignKey
posts = Post.objects.select_related('author').all()
for post in posts:
print(post.author.name) # No extra queries
Now consider a reverse relationship—an author with many posts:
from django.db.models import Prefetch
# Bad: N+1 on reverse FK
authors = Author.objects.all()
for author in authors:
for post in author.post_set.all(): # Query per author
print(post.title)
# Good: prefetch_related
authors = Author.objects.prefetch_related('post_set').all()
for author in authors:
for post in author.post_set.all(): # No extra queries
print(post.title)
A tip that impresses interviewers: you can combine them. select_related('publisher').prefetch_related('post_set') joins the publisher and prefetches posts in one go. Also, always check the query count in your tests using assertNumQueries() or Django Debug Toolbar.
How do you scale a Python API? Horizontal workers, caching, async I/O and background jobs
Scaling isn't a single lever—it's a set of levers you pull in order. A senior developer knows this hierarchy: first optimize your code, then add caching, then distribute work, then add workers.
Async I/O is your first win. If your endpoint calls three external APIs sequentially, that's wasteful. With async, you call all three concurrently:
import asyncio
from fastapi import FastAPI
app = FastAPI()
async def call_service_a():
# Simulate external call
await asyncio.sleep(1)
return {"data": "a"}
async def call_service_b():
await asyncio.sleep(1)
return {"data": "b"}
async def call_service_c():
await asyncio.sleep(1)
return {"data": "c"}
@app.get("/combined")
async def combined():
# Runs all 3 in parallel, completes in ~1 second instead of 3
results = await asyncio.gather(
call_service_a(),
call_service_b(),
call_service_c()
)
return results
Caching prevents repeated expensive operations. Use Redis for distributed caching. Cache query results, API responses, and computed values.
import redis
import json
cache = redis.Redis(host='localhost', port=6379, db=0)
def get_user_profile(user_id):
cache_key = f"user:{user_id}:profile"
# Check cache first
cached = cache.get(cache_key)
if cached:
return json.loads(cached)
# Cache miss: fetch from DB
user = User.objects.get(id=user_id)
profile = {
'id': user.id,
'name': user.name,
'email': user.email,
}
# Store in cache for 1 hour
cache.setex(cache_key, 3600, json.dumps(profile))
return profile
Background jobs remove slow operations from the request path. Use Celery, RQ, or Dramatiq to handle email sends, file processing, and heavy computations asynchronously.
from celery import Celery
app = Celery('tasks', broker='redis://localhost:6379')
@app.task
def send_email(to_address, subject, body):
# This runs in a worker, not blocking the HTTP response
import smtplib
# ... send email ...
pass
# In your API:
from fastapi import FastAPI
web = FastAPI()
@web.post("/invite")
async def send_invite(email: str):
send_email.delay(email, "Welcome", "Join us!")
return {"status": "invite sent"}
Horizontal scaling means running multiple instances behind a load balancer. Use containers (Docker) with Kubernetes or simpler orchestration. Ensure your app is stateless—no session data stored locally, everything in Redis or a database.
Interviewers want to hear about monitoring and metrics. Use APM tools like New Relic or DataDog to track response times, error rates, and database performance. Know your bottlenecks before scaling blindly.
How do you secure an API? Validation, auth, least privilege, dependency audits
Security is where negligence becomes liability. A good answer covers multiple layers.
Input validation prevents injection attacks and malformed data. Use Pydantic for FastAPI to define strict schemas:
from pydantic import BaseModel, EmailStr, Field
from fastapi import FastAPI
app = FastAPI()
class CreateUser(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
age: int = Field(..., ge=0, le=150)
@app.post("/users")
def create_user(user: CreateUser):
# Pydantic validates type, format, and constraints
# Invalid data is rejected automatically
return user
Authentication verifies identity. Use industry-standard JWT or OAuth2. Don't roll your own. Store passwords with strong hashing (bcrypt, argon2), never plaintext.
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthCredential
import jwt
security = HTTPBearer()
def verify_token(credentials: HTTPAuthCredential = Depends(security)):
try:
payload = jwt.decode(credentials.credentials, "secret_key", algorithms=["HS256"])
return payload
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
@app.get("/profile")
def get_profile(user=Depends(verify_token)):
return {"user_id": user['sub']}
Authorization enforces least privilege—each user/service has minimum permissions needed. Role-based access control (RBAC) or attribute-based (ABAC). Check permissions on every protected endpoint.
Dependency audits prevent supply-chain attacks. Use pip audit or Snyk to scan for known vulnerabilities in your dependencies. Pin versions in requirements.txt, don't use * wildcards. Review changelogs before upgrading.
HTTPS, CORS, and headers matter too. Always use HTTPS in production. Set CORS properly—don't allow * unless necessary. Add security headers: Content-Security-Policy, X-Content-Type-Options, Strict-Transport-Security.
Logging and secrets must be handled carefully. Never log passwords, tokens, or PII. Use environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault) for sensitive data, never hardcode them.
Common mistakes
- Confusing async with fast: Async makes I/O concurrent, not CPU faster. If your bottleneck is database computation, async won't help.
- Over-caching: Cache invalidation is notoriously hard. Cache liberally, but invalidate it aggressively when data changes. Stale data bugs are worse than no caching.
- Assuming prefetch_related is always better: It runs an extra query. For single objects or small result sets,
select_relatedcan be faster. - Relying solely on HTTPS for security: HTTPS only encrypts in transit. Validate and sanitize input, authenticate, and authorize every request.
- Scaling without monitoring: You scale to 10 instances, but now your database connection pool is exhausted. Know your limits before you hit them.
Key takeaways
- Async enables concurrent I/O in a single thread via an event loop and coroutines—understand the difference between I/O-bound and CPU-bound work.
- Eliminate N+1 queries with
select_related()for joins andprefetch_related()for separate queries in Django, orselectinload()in SQLAlchemy. - Scale in order: optimize code, add caching (Redis), use async I/O, offload slow work to background jobs (Celery), then add horizontal workers behind a load balancer.
- Security is layered: validate input strictly, authenticate with standard protocols (JWT/OAuth2), enforce least privilege, audit dependencies, and never hardcode secrets.
- Interviewers test depth. Know not just the what, but the why and when. Be ready with a production war story.






Comments
Loading comments…