Back to blog

Python Senior Interview Questions

pythoninterviewcareerbackenddatabase
Python Senior Interview Questions

A friend recently interviewed at one of the largest outsourcing companies in Vietnam for a senior Python backend role. They walked me through every question they were asked — two rounds, two interviewers, covering everything from "why are you leaving?" to deep database internals.

This post breaks down each question with the kind of answer a senior engineer actually gives — not a textbook definition, but the practical reasoning behind it.


Section 1 — Non-Technical (Interviewer 1)

These questions feel soft, but they reveal a lot about your self-awareness and communication style. Don't wing them.

"Introduce yourself"

Don't recite your resume. Tell a story. Hit:

  • What you've built and at what scale
  • The technical decisions you owned (and why)
  • What you're optimizing for next

Example frame: "I've spent the last 3 years building backend services in Python — FastAPI mostly — serving a few million requests per day. The biggest thing I've worked on recently is rearchitecting our async job pipeline to reduce latency by 40%. I'm looking for a role where I can go deeper on distributed systems design."

"Why are you leaving your current company?"

Be honest, be forward-looking. Avoid bashing your current employer. Good answers:

  • "I've grown as much as I can in this environment and want a bigger technical challenge."
  • "The company direction changed — it's less technical than when I joined."
  • "I want to work on problems at a larger scale."

"Why do you think you fit this company?"

Do your research. Frame your answer around their business domain and tech stack. Show that you've thought about their problems, not just your own resume.

"What project are you most proud of?"

This is your chance to show depth. Pick one project and go deep — don't list five shallow ones. A strong answer covers:

  • The problem: What were you actually solving? Why did it matter?
  • Your specific role: What decisions did you make, not the team?
  • The technical challenge: What was hard, and how did you think through it?
  • The outcome: Measurable impact — latency, throughput, cost, user growth

Example frame: "The project I'm most proud of is a real-time data pipeline I redesigned at my last company. We had a batch job running every 15 minutes that was causing noticeable data staleness for our analytics dashboard. I replaced it with an async event-driven pipeline using FastAPI background tasks and Redis Streams — reducing data lag from 15 minutes to under 5 seconds while cutting infrastructure cost by 30% because we eliminated a dedicated scheduler VM. The hard part was ensuring exactly-once delivery during failover, which I solved by using Redis consumer groups with manual ACK."

What interviewers are listening for:

  • Can you explain a complex system clearly to a non-technical audience?
  • Did you own the problem or just execute someone else's plan?
  • Do you understand the trade-offs you made, including what you didn't do?

Section 2 — Technical Deep-Dive (Interviewer 2)

Data Structures: tuple, list, set, dict differences

This sounds basic, but senior candidates are expected to go beyond "list is mutable, tuple is not."

StructureOrderedMutableDuplicatesUse case
listSequence of items you'll modify
tupleFixed record, function return values, dict key
setMembership testing, deduplication
dict✅ (3.7+)Keys: ❌Key-value lookup, structured data

What seniors add:

  • list operations like append() are O(1) amortized, but insert(0, x) is O(n) — don't use a list as a queue.
  • set and dict use hash tables under the hood — lookup is O(1) average.
  • frozenset exists as the immutable counterpart to set, and it's hashable.
  • tuple unpacking and namedtuple / dataclass for structured data.

What is Immutability? Why does it matter?

Definition: An immutable object cannot be changed after creation. In Python: int, float, str, tuple, frozenset, bytes.

Why it matters in practice:

  1. Thread safety: Immutable objects are safe to share across threads without locks.
  2. Hashability: Only immutable objects can be used as dict keys or set elements (they need a stable hash).
  3. Predictability: Functions that receive an immutable object can't accidentally modify the caller's data.
  4. Performance: Python interns small integers and interned strings — is checks can be O(1).

Common gotcha:

# This looks like mutation, but it's not
x = "hello"
x += " world"  # creates a new string, x now points to a new object
id_before = id(x)
 
# vs list (actual mutation)
lst = [1, 2, 3]
lst.append(4)  # same object in memory

AsyncIO vs MultiThreading vs MultiProcessing

This is one of the most important senior-level Python questions. Get the mental model right.

The core distinction is about the type of work:

I/O-bound work  → AsyncIO or Threading
CPU-bound work  → Multiprocessing

AsyncIO

  • Single thread, cooperative multitasking via the event loop
  • async/await lets you write non-blocking code that looks synchronous
  • Zero overhead from thread context switching
  • Best for: network calls, database queries, file I/O — anything where you wait
import asyncio
import httpx
 
async def fetch(url: str) -> str:
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.text
 
async def main():
    results = await asyncio.gather(
        fetch("https://api.example.com/a"),
        fetch("https://api.example.com/b"),
        fetch("https://api.example.com/c"),
    )
    print(results)
 
asyncio.run(main())

MultiThreading

  • Multiple threads, GIL limits true parallelism for CPU work
  • Useful for I/O-bound work when you're dealing with blocking libraries (e.g., requests, psycopg2)
  • Threads share memory — need locks for shared state
from concurrent.futures import ThreadPoolExecutor
import requests
 
def fetch(url: str) -> str:
    return requests.get(url).text
 
with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(fetch, urls))

MultiProcessing

  • Multiple OS processes, each with its own GIL
  • True CPU parallelism
  • Higher memory overhead, inter-process communication via queues/pipes
  • Best for: data processing, ML inference, CPU-heavy transformations
from concurrent.futures import ProcessPoolExecutor
 
def compute(n: int) -> int:
    return sum(i * i for i in range(n))
 
with ProcessPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(compute, [10**6, 10**6, 10**6, 10**6]))

Decision matrix

ScenarioUse
FastAPI endpoint calling external APIsAsyncIO
Legacy blocking library (requests, psycopg2)ThreadPoolExecutor
Image resizing, data transformationProcessPoolExecutor
ML model inference per requestProcessPool or dedicated workers
Mixing async and sync codeasyncio.run_in_executor()

I/O Bound vs CPU Bound Tasks

I/O bound: The task spends most of its time waiting for external resources — network, disk, database. The CPU is largely idle.

CPU bound: The task spends most of its time computing — crunching numbers, parsing, compressing, running ML models. The CPU is fully loaded.

How to tell which you have:

import time
 
# Profile it
start = time.perf_counter()
do_the_work()
elapsed = time.perf_counter() - start
 
# Also check CPU usage with htop or psutil while it runs

Why it matters for Python specifically: The GIL means threading won't help CPU-bound work. You must use multiprocessing (or write a C extension, use NumPy, etc.) to get true CPU parallelism.

How Python manages memory? CPython vs other interpreters?

CPython memory management:

  1. Reference counting: Every object tracks how many references point to it. When count hits 0, memory is freed immediately.
import sys
x = [1, 2, 3]
print(sys.getrefcount(x))  # 2 (x + the getrefcount arg)
y = x
print(sys.getrefcount(x))  # 3
del y
print(sys.getrefcount(x))  # 2
  1. Cyclic garbage collector: Reference counting alone can't handle circular references (a → b → a). CPython runs a cycle-detecting GC periodically for these cases.

  2. Memory allocator: CPython uses its own allocator (pymalloc) for objects ≤ 512 bytes, bypassing the system allocator for speed.

  3. Object pools: Small integers (-5 to 256) and interned strings are cached — Python reuses existing objects rather than creating new ones.

CPython vs other interpreters:

InterpreterGILMemory ModelUse case
CPython✅ (being removed in 3.13+)Reference counting + GCDefault, most compatible
PyPyTracing JIT, different GCSpeed-critical Python code
JythonJVM GCJava interop
GraalPyGraalVM GCPolyglot environments
MicroPythonN/AManual/simplifiedEmbedded, IoT

Key point for senior interviews: Python 3.13 introduced experimental support for running CPython without the GIL (free-threaded mode, PEP 703). This changes the threading story significantly for future Python.

What is Hashable? Why do we need it? How to make an object hashable?

Hashable: An object is hashable if it has a __hash__() method that returns an integer, and a __eq__() method — and critically, objects that compare equal must have the same hash.

Why we need it: dict keys and set elements must be hashable. The hash is used to determine the bucket in the underlying hash table — O(1) lookup depends on it.

Built-in hashability:

hash(42)        # ✅ int
hash("hello")   # ✅ str
hash((1, 2, 3)) # ✅ tuple (if all elements are hashable)
hash([1, 2, 3]) # ❌ TypeError: unhashable type: 'list'
hash({1: 2})    # ❌ TypeError: unhashable type: 'dict'

Making a custom class hashable:

class Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y
 
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y
 
    def __hash__(self) -> int:
        return hash((self.x, self.y))  # tuple of immutable fields
 
p1 = Point(1, 2)
p2 = Point(1, 2)
 
print(p1 == p2)   # True
print(p1 is p2)   # False
print({p1, p2})   # {Point(1, 2)} — deduplicated correctly

The contract: If you define __eq__, Python sets __hash__ to None by default (making the class unhashable). You must explicitly define __hash__ if you want both equality and hashability.

With dataclasses:

from dataclasses import dataclass
 
@dataclass(frozen=True)  # frozen=True makes it immutable AND hashable
class Point:
    x: float
    y: float

How to interact with relational databases? Connection pool setup for multiple instances?

Basic interaction with SQLAlchemy (async):

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
 
engine = create_async_engine(
    "postgresql+asyncpg://user:pass@localhost/db",
    pool_size=10,        # base pool size
    max_overflow=20,     # extra connections beyond pool_size
    pool_timeout=30,     # seconds to wait for a connection
    pool_recycle=1800,   # recycle connections every 30 min (prevent stale)
)
 
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
 
async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

Connection pool sizing — the key question:

When you scale to multiple service instances, each instance has its own connection pool. The total connections hitting your database is:

total_connections = instances × pool_size

If you have 10 service instances × pool_size=10 = 100 connections to Postgres.

PostgreSQL has a hard max_connections limit (default 100). You'll hit it fast.

Solution: PgBouncer (connection pooler):

[Service Instances] → [PgBouncer] → [PostgreSQL]
    10 × 10 = 100      pool: 20         max: 100
  • Each service connects to PgBouncer, not directly to Postgres
  • PgBouncer maintains a small pool to Postgres and multiplexes
  • Use transaction pooling mode for stateless backends (most common)
  • Session pooling if you rely on session-level features (temporary tables, SET statements)

Practical settings for FastAPI + Postgres:

# Per-instance pool (when using PgBouncer)
engine = create_async_engine(
    DATABASE_URL,
    pool_size=5,          # small — PgBouncer handles the real pooling
    max_overflow=5,
    pool_pre_ping=True,   # validate connection before use
)

SQL Query Optimization — How & Why?

The mindset: Don't guess, measure. Start with EXPLAIN ANALYZE.

EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id, u.name
ORDER BY order_count DESC
LIMIT 20;

Key things to look for in the plan:

  • Seq Scan on a large table → missing index
  • Hash Join vs Nested Loop — Hash Join is better for large tables
  • High row estimates → stale statistics, run ANALYZE
  • Sort node with large row count → consider an index on the ORDER BY column

Optimization techniques:

1. Indexes — the 80% solution:

-- Composite index for common filter + sort pattern
CREATE INDEX idx_users_created_name ON users(created_at, name);
 
-- Partial index for active records only
CREATE INDEX idx_active_orders ON orders(user_id) WHERE status = 'active';
 
-- Covering index — query satisfied from index alone (no heap fetch)
CREATE INDEX idx_orders_covering ON orders(user_id, created_at, status);

2. Avoid N+1 queries:

# Bad: 1 query for users + N queries for orders
users = session.execute(select(User)).scalars().all()
for user in users:
    orders = session.execute(select(Order).where(Order.user_id == user.id)).all()
 
# Good: JOIN or eager load
users = session.execute(
    select(User).options(selectinload(User.orders))
).scalars().all()

3. Use pagination correctly:

-- Bad: OFFSET 10000 LIMIT 20 (scans 10020 rows)
SELECT * FROM orders ORDER BY created_at DESC OFFSET 10000 LIMIT 20;
 
-- Good: keyset pagination
SELECT * FROM orders
WHERE created_at < :last_seen_date
ORDER BY created_at DESC
LIMIT 20;

4. Analyze slow queries regularly:

-- Find slow queries in Postgres
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

Database Scaling — Cluster and ProxySQL

When a single Postgres instance can't keep up, you scale horizontally.

Read replicas (most common first step):

Primary (writes) ──→ Replica 1 (reads)
                 ──→ Replica 2 (reads)

Your application routes: SELECT → replicas, INSERT/UPDATE/DELETE → primary.

ProxySQL (common in MySQL/MariaDB environments):

  • Sits between your app and database cluster
  • Automatically routes queries based on rules (read/write splitting)
  • Connection pooling
  • Query rewriting and caching
  • Failover handling
App → ProxySQL → Primary (writes)
              → Replica 1 (reads)
              → Replica 2 (reads)

For Postgres, the equivalent tools are:

  • PgBouncer: Connection pooling
  • Patroni + HAProxy: High availability + read/write splitting
  • Citus: Horizontal sharding for Postgres

When to think about sharding:

Sharding (splitting data across multiple databases by key) is the last resort. Before sharding:

  1. Add indexes (free win)
  2. Add read replicas (scales reads)
  3. Vertical scale (bigger machine)
  4. Caching layer (Redis)
  5. Archive old data

Sharding adds significant operational complexity — avoid until you genuinely need it.

How to ensure good code quality? SonarQube?

Code quality is a system, not a single tool. Here's the layered approach:

Layer 1 — Static analysis (catches bugs before runtime):

# Type checking
mypy app/
 
# Linting (code style + common bugs)
ruff check app/
 
# Security vulnerabilities
bandit -r app/

Layer 2 — Testing:

# pytest with coverage
pytest --cov=app --cov-report=term-missing --cov-fail-under=80
  • Unit tests for pure functions and business logic
  • Integration tests for database interactions and API endpoints
  • Don't mock the database if you can avoid it — use a test database

Layer 3 — Code review culture:

  • PR reviews are not about catching bugs — they're about knowledge sharing
  • Keep PRs small (< 400 lines) so they're actually reviewable
  • Reviewer checks: readability, edge cases, security, performance implications

Layer 4 — SonarQube / SonarCloud:

SonarQube scans your codebase for:

  • Bugs: Code that will likely fail at runtime
  • Vulnerabilities: Security issues (SQL injection, hardcoded secrets)
  • Code smells: Maintainability problems (duplicated code, complex functions)
  • Coverage gates: Fail the build if test coverage drops below threshold
# Example CI gate in GitHub Actions
- name: SonarCloud Scan
  uses: SonarSource/sonarcloud-github-action@master
  with:
    args: >
      -Dsonar.python.coverage.reportPaths=coverage.xml
      -Dsonar.qualitygate.wait=true  # Fail CI if quality gate fails

The honest take: SonarQube is most valuable as a safety net in CI/CD, not as a replacement for good engineering practices. Fix issues at the source (code review culture, pair programming, clear coding standards) — tools are multipliers, not substitutes.

What is CI/CD? Jenkins?

CI (Continuous Integration): Every code change is automatically built, tested, and verified. The goal is to catch integration problems early — before they compound.

CD (Continuous Delivery / Deployment): Every passing build is automatically deployable (Delivery) or actually deployed (Deployment) to an environment.

Code push → CI pipeline → CD pipeline → Production
              ↓                ↓
           Tests pass      Deploy to staging → Deploy to prod

A typical CI pipeline for a Python service:

# .github/workflows/ci.yml
jobs:
  test:
    steps:
      - name: Install dependencies
        run: pip install -e ".[dev]"
 
      - name: Type check
        run: mypy app/
 
      - name: Lint
        run: ruff check app/
 
      - name: Test with coverage
        run: pytest --cov=app --cov-fail-under=80
 
      - name: Security scan
        run: bandit -r app/
 
      - name: SonarCloud
        uses: SonarSource/sonarcloud-github-action@master

Jenkins vs GitHub Actions vs GitLab CI:

ToolHostingBest for
JenkinsSelf-hostedFull control, complex pipelines, on-prem
GitHub ActionsCloud (GitHub)OSS projects, GitHub repos, simple setup
GitLab CICloud or self-hostedGitLab repos, built-in container registry

Jenkins-specific: Jenkins uses a Jenkinsfile (Groovy DSL) and runs on your own infrastructure. Common in enterprises with existing Jenkins setups or air-gapped environments. The trend for new projects is away from Jenkins toward cloud-native CI.

How do you deploy to staging/production?

The deployment process depends on your infrastructure, but here's a typical Python service flow:

Container-based deployment (most common):

1. CI builds Docker image
2. Image pushed to registry (ECR, Docker Hub, GCR)
3. CD pulls new image and deploys

Example with GitHub Actions + Docker:

- name: Build and push Docker image
  run: |
    docker build -t myapp:${{ github.sha }} .
    docker push registry.example.com/myapp:${{ github.sha }}
 
- name: Deploy to staging
  run: |
    ssh deploy@staging "docker pull registry.example.com/myapp:${{ github.sha }} && \
    docker-compose up -d --no-deps app"

Zero-downtime deployment strategies:

  • Rolling update: Replace instances one by one — no downtime, easy rollback
  • Blue/green: Run two identical environments, switch traffic — instant rollback
  • Canary: Route a small % of traffic to new version first, monitor, then roll out

For Kubernetes:

# k8s deployment with rolling update
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

Staging vs Production differences:

  • Staging uses production-like data (anonymized) and identical infrastructure
  • Feature flags let you deploy code to production but activate features gradually
  • Always deploy to staging first, run smoke tests, then promote to production

Docker vs Kubernetes? Cloud vs On-Premises?

Docker vs Kubernetes — they're not alternatives, they work together:

DockerKubernetes
What it doesPackages and runs containersOrchestrates containers at scale
UnitContainerPod (1+ containers)
ScopeSingle machineCluster of machines
Use whenLocal dev, single-server deployMulti-node, auto-scaling, HA needed
Docker:      [App] → [Container] → runs on one machine
Kubernetes:  [App] → [Container] → [Pod] → [Node] → [Cluster]
                                              ↑ auto-scheduled, auto-healed

For a Python backend service:

  • Local dev: docker compose up — spin up app + postgres + redis together
  • Small production (< 5 services, 1-2 servers): docker compose on a VPS
  • Larger production (many services, auto-scaling needed): Kubernetes (EKS, GKE, AKS)

Cloud vs On-Premises:

CloudOn-Premises
Cost modelPay-as-you-go (OpEx)Upfront hardware (CapEx)
ScalingElastic, minutesWeeks (procurement cycle)
MaintenanceManaged by providerYour team's responsibility
Data controlProvider's data centersYour data center
ComplianceDepends on region/certificationFull control

When to choose on-premises:

  • Strict data sovereignty requirements (government, banking, healthcare)
  • Existing infrastructure investment
  • Predictable, steady-state workloads where cloud gets expensive

When to choose cloud:

  • Variable traffic (pay for what you use)
  • Speed to market matters
  • Small ops team (no infra specialists on staff)

Hybrid is common in Vietnam's enterprise market — sensitive data on-prem, application layer on cloud.

Background Tasks — Cron vs Systemd vs Supervisor?

Background tasks are jobs that run outside the request/response cycle. How you manage them matters for reliability.

The three tools serve different purposes:

Cron — scheduled jobs at fixed times:

# crontab -e
# Run cleanup every day at 2 AM
0 2 * * * /usr/bin/python3 /app/scripts/cleanup.py >> /var/log/cleanup.log 2>&1
 
# Run every 5 minutes
*/5 * * * * /app/scripts/health_check.py
  • Best for: periodic tasks (reports, cleanup, data sync)
  • Weakness: no retry on failure, no overlap prevention, logs are basic

Systemd — long-running services managed by the OS:

# /etc/systemd/system/myapp-worker.service
[Unit]
Description=MyApp Background Worker
After=network.target
 
[Service]
User=appuser
WorkingDirectory=/app
ExecStart=/app/.venv/bin/python -m app.worker
Restart=always
RestartSec=5
 
[Install]
WantedBy=multi-user.target
systemctl enable myapp-worker
systemctl start myapp-worker
systemctl status myapp-worker
  • Best for: workers that must always be running (queue consumers, WebSocket servers)
  • Handles restarts, integrates with system boot, logs via journalctl

Supervisor — process manager, common in Python deployments:

# /etc/supervisor/conf.d/myapp.conf
[program:myapp-worker]
command=/app/.venv/bin/python -m app.worker
directory=/app
user=appuser
autostart=true
autorestart=true
stderr_logfile=/var/log/myapp-worker.err.log
stdout_logfile=/var/log/myapp-worker.out.log
numprocs=4                    ; run 4 worker processes
process_name=%(program_name)s_%(process_num)02d
  • Best for: running multiple worker processes, legacy Python deployments
  • Similar to systemd but more Python-ecosystem-friendly, easier multi-process config

In a modern stack (Kubernetes / Docker):

  • Cron jobs → Kubernetes CronJob or cloud scheduler (AWS EventBridge, GCP Cloud Scheduler)
  • Long-running workers → Kubernetes Deployment with restartPolicy: Always
  • Task queues → Celery + Redis/RabbitMQ, or cloud-native (AWS SQS, GCP Pub/Sub)

Decision guide:

NeedTool
Run script at 3 AM dailyCron
Keep a process alive 24/7 on a Linux serverSystemd
Run 4 Python worker processes, easy restartSupervisor
Scheduled jobs in KubernetesK8s CronJob
Queue-based async tasks (retries, priorities)Celery + Redis

How to prepare for a senior Python interview

These questions reveal what a senior role actually requires:

  1. Deep understanding of Python internals — not just how to use the language, but why it works the way it does (GIL, memory model, hashability)
  2. Systems thinking — knowing when to use async vs threads vs processes, and how that decision changes at scale
  3. Database fluency — query optimization, connection management, scaling strategies
  4. Engineering discipline — code quality as a system (testing + static analysis + review culture)
  5. DevOps awareness — CI/CD pipelines, container deployment, infrastructure trade-offs

The interviewers aren't looking for memorized answers. They're looking for candidates who can reason through trade-offs and explain why — not just what.

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.