Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

Python Backend Developer Top 100 Interview Questions

100 Python backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers the language core, concurrency and asyncio, web request handling, databases, caching, testing, security, performance and system design, from the basics to production scenarios.

100 questions

Questions 81-100 of 100

  1. Q81.How do you reduce database load from a Python API?

    Ask the database for less: fewer queries, smaller results and cheaper plans, then avoid repeating work through caching. Scaling the database should come after removing waste.

    • Remove per-row queries: load related data in one query with a join or an IN list, instead of one query per item in a loop.
    • Select only what you need: avoid loading whole objects or wide rows to show a few fields.
    • Index the access patterns: add indexes for the filters and sort orders the API actually uses, and check query plans.
    • Paginate: never return unbounded result sets.
    • Batch writes: insert or update many rows per statement and per transaction.
    • Cache reads: cache expensive or popular results with a clear expiry and invalidation rule.
    • Pool connections: reuse connections, and size the pool so the database is not overwhelmed by idle ones.
    • Move reads away: send heavy reports to a read replica or a separate reporting store.

    Measure it: track queries per request and total database time per endpoint. A sudden jump in queries per request after a release usually means a new loop of lazy loads.

  2. Q82.How do you batch database writes efficiently in Python?

    Send many rows per statement with executemany or multi-row inserts, and commit once per batch instead of once per row. Each commit forces the database to make data durable, so per-row commits are often the slowest part of a bulk load.

    • Chunk the input: split a large stream into batches of a few hundred to a few thousand rows, so memory stays bounded and a failure only rolls back one batch.
    • One transaction per batch: commit after each chunk; a single transaction for millions of rows can hold locks and grow logs for a long time.
    • Use bulk paths: many databases offer a faster bulk-load command for very large imports.
    • Make it restartable: record progress, or use upserts keyed on a natural ID, so a failed job can resume without duplicating rows.
    • Watch side effects: triggers, indexes and constraints all slow bulk writes; for huge loads, some teams build indexes after loading.

    itertools.batched, available in recent versions, splits any iterable into fixed-size tuples, so a generator over a file can feed batches without loading everything first.

    Example

    Two thousand five hundred generated rows are inserted in three transactions instead of 2,500 separate commits.

    import sqlite3
    from itertools import batched
    
    db = sqlite3.connect(":memory:")
    db.execute("CREATE TABLE events (id INTEGER PRIMARY KEY, kind TEXT)")
    rows = ((n, "click" if n % 3 else "view") for n in range(1, 2501))
    
    transactions = 0
    for chunk in batched(rows, 1000):
        with db:
            db.executemany("INSERT INTO events VALUES (?, ?)", chunk)
        transactions += 1
    
    print(transactions)  # 3
    print(db.execute("SELECT COUNT(*) FROM events").fetchone()[0])  # 2500
    print(db.execute("SELECT COUNT(*) FROM events WHERE kind = 'view'").fetchone()[0])  # 833
  3. Q83.How do you call external HTTP APIs reliably from Python?

    Treat every external call as something that can be slow, fail or return nonsense. Bound it with timeouts, retry carefully, protect your service from repeated failures, and validate what comes back.

    flowchart TD
      A["Call external API"] --> B{"Circuit open?"}
      B -->|yes| C["Fail fast or use fallback"]
      B -->|no| D["Send with connect and read timeouts"]
      D --> E{"Success?"}
      E -->|yes| F["Validate response and return"]
      E -->|no| G{"Temporary error and retry budget left?"}
      G -->|yes| H["Back off with jitter, then retry"]
      G -->|no| C
      H --> D
    • Timeouts: set both a connection timeout and a read timeout; many HTTP clients wait forever by default.
    • Connection reuse: keep one client or session per process, so connections and TLS handshakes are reused.
    • Retries: only for temporary failures such as timeouts and 503 responses, only for safe or idempotent requests, with exponential backoff and jitter.
    • Rate limits: respect 429 responses and their retry headers.
    • Validation: check status codes and parse the body against an expected schema before using it.
  4. Q84.How would you implement a simple circuit breaker in Python?

    A circuit breaker wraps calls to a dependency and tracks failures. After too many failures it opens and rejects calls immediately for a cool-down period, then lets a trial call through in a half-open state, closing again if the trial succeeds.

    stateDiagram-v2
      state "Closed" as C
      state "Open" as O
      state "Half-open" as H
      [*] --> C
      C --> O: Failures reach the threshold
      O --> H: Cool-down period ends
      H --> C: Trial call succeeds
      H --> O: Trial call fails

    Design points:

    • Closed: calls pass through; consecutive failures are counted, and a success resets the count.
    • Open: calls fail immediately with a clear error, so threads and connections are not tied up waiting on a dead service.
    • Half-open: after the cool-down, one call is allowed; failure reopens the circuit and success closes it.
    • Injected clock: passing the time source in makes the state changes easy to test.

    For production use:

    • Count failures over a rolling window and require a minimum number of calls, so one early failure does not trip it.
    • Allow only a limited number of trial calls while half-open.
    • Use one breaker per dependency, and export its state as a metric.
    • Treat only real failures, such as timeouts and server errors, as failures, not a 404 for a missing record.

    Example

    Three failures open the circuit, calls are rejected during the cool-down, and a successful trial closes it again.

    class CircuitBreaker:
        def __init__(self, failure_threshold, reset_after, clock):
            self.failure_threshold = failure_threshold
            self.reset_after = reset_after
            self.clock = clock
            self.failures = 0
            self.opened_at = None
    
        @property
        def state(self):
            if self.opened_at is None:
                return "closed"
            if self.clock() - self.opened_at >= self.reset_after:
                return "half-open"
            return "open"
    
        def call(self, func):
            if self.state == "open":
                raise RuntimeError("circuit open")
            try:
                result = func()
            except Exception:
                self.failures += 1
                if self.state == "half-open" or self.failures >= self.failure_threshold:
                    self.opened_at = self.clock()
                raise
            self.failures = 0
            self.opened_at = None
            return result
    
    now = [0.0]
    breaker = CircuitBreaker(failure_threshold=3, reset_after=30, clock=lambda: now[0])
    
    def failing():
        raise ConnectionError("service down")
    
    for _ in range(3):
        try:
            breaker.call(failing)
        except ConnectionError:
            pass
    
    print(breaker.state)  # open
    try:
        breaker.call(lambda: "ok")
    except RuntimeError as error:
        print(error)  # circuit open
    
    now[0] = 31.0
    print(breaker.state)  # half-open
    print(breaker.call(lambda: "ok"))  # ok
    print(breaker.state)  # closed
  5. Q85.How do you package and run a Python service in a container?

    Build a small, reproducible image that installs pinned dependencies, runs as a non-root user and starts the server directly as the main process, so it receives shutdown signals.

    1. Slim base image: start from a minimal Python base image matching the version you test with.
    2. Multi-stage build: compile wheels and anything needing build tools in a first stage, then copy only the installed packages into the final image.
    3. Dependencies before code: copy the lock file and install packages first, then copy the source, so code changes reuse the cached dependency layer.
    4. Pinned installs: install from the lock file with hashes.
    5. Unbuffered output: set PYTHONUNBUFFERED=1, so logs reach the container's output immediately instead of sitting in a buffer.
    6. Non-root user: create a dedicated user and switch to it.
    7. Direct start: start the application server as the main process, not through a shell wrapper that swallows signals.
    8. Configuration from the environment, with no secrets baked into the image.

    Sizing inside containers: worker counts should follow the container's CPU and memory limits, not the host's core count, which the process may still see.

  6. Q86.How do you upgrade a service to a new Python version safely?

    Treat the interpreter like any other dependency upgrade: find what breaks, fix it in small steps, prove behavior and performance in testing, then roll it out gradually.

    flowchart LR
      A["Read release notes and removals"] --> B["Check dependency support"]
      B --> C["Run tests on old and new versions"]
      C --> D["Fix warnings and failures"]
      D --> E["Compare performance and memory"]
      E --> F["Canary in production"]
      F --> G["Full rollout"]
    1. Read the change notes: look for removed modules, changed defaults and deprecations that are now errors.
    2. Check dependencies: confirm each one supports the new version and publishes prebuilt packages for it.
    3. Test both versions: run the suite on the old and new interpreters in the pipeline, and treat deprecation warnings as errors to surface hidden problems.
    4. Compare behavior: check performance, memory and startup time, since internals change between versions.
    5. Roll out gradually: canary a few instances, watch errors and latency, then complete the rollout.
    6. Update every place the version is set, such as images, local environments and type checker settings.

    Upgrading one version at a time, regularly, is far cheaper than jumping several versions after support ends.

  7. Q87.How do you handle WebSockets or server-sent events in an async Python service?

    Each open connection is a long-lived task that must stay cheap, so an async server is the natural fit. The hard parts are slow clients, scaling across workers and clean disconnects.

    WebSocketsServer-sent events
    Two-way messages over one upgraded connectionOne-way stream from server to client over plain HTTP
    Best for chat, collaboration and multiplayer featuresBest for notifications, feeds and progress updates
    The client must handle reconnection itselfBrowsers reconnect automatically and resume with event IDs
    flowchart LR
      A["Event producer"] --> B["Message broker channel"]
      B --> C["Worker process 1"]
      B --> D["Worker process 2"]
      C --> E["Connected clients on worker 1"]
      D --> F["Connected clients on worker 2"]
    • Authenticate on connect, and re-check permissions for every channel a client subscribes to.
    • Bounded per-client queues: a slow client must not make the server buffer without limit; drop messages or disconnect clients that fall too far behind.
    • Heartbeats: send pings, or periodic comments for server-sent events, so dead connections are detected and proxies do not close idle ones.
    • Fan-out across workers: clients connect to different processes, so publish events through a shared broker channel that every worker subscribes to.
    • Reconnection: clients reconnect after drops; event IDs let server-sent events resume from the last received message.
    • Shutdown: close connections with a proper close code, so clients reconnect to healthy instances.

    Never block inside the per-connection loop, or every client on that worker stalls.

  8. Q88.How do you handle database connection failures at startup and during runtime?

    Expect the database to be briefly unavailable, during startup races, failovers and network blips, and design so the service recovers on its own without crashing in a loop or serving broken requests.

    flowchart TD
      A["Service starts"] --> B{"Database reachable?"}
      B -->|no| C["Retry with backoff, readiness false"]
      C --> B
      B -->|yes| D["Readiness true, accept traffic"]
      D --> E{"Connection error during request?"}
      E -->|yes| F["Discard connection, retry once if safe"]
      E -->|no| D
      F --> D
    • At startup: retry the first connection with exponential backoff for a bounded time, and keep the readiness check failing until it succeeds, so no traffic arrives early.
    • Stale connections: pooled connections can be closed by the server or a firewall; validate them before use or recycle them after a maximum age.
    • During failover: in-flight transactions fail, so retry the whole transaction once when it is safe, and return a clear 503 otherwise.
    • Avoid crash loops: exiting immediately on a database error makes the platform restart the service again and again, adding load to a database that is already recovering.
    • Alert on symptoms: connection errors per minute and pool wait time show trouble before users report it.
  9. Q89.A Python API's 99th percentile latency spikes every few minutes. How would you investigate?

    Regular spikes point to something periodic, so line up the spike times with every scheduled or cyclical activity in the service, its dependencies and the platform.

    1. Confirm the pattern: measure the exact interval and which endpoints spike. All endpoints at once suggests something shared, such as the process, the database or the network.
    2. Look for timers: cache expiry times, background jobs, metrics flushes, log rotation, connection recycling and health checks often run on fixed intervals.
    3. Check garbage collection: large collections in services with many objects can pause a process; count collections and their duration.
    4. Check the database: periodic reports, maintenance jobs or checkpoints on the database can slow every query.
    5. Check the platform: autoscaling events, noisy neighbours and CPU throttling under container limits.
    6. Correlate with traces: compare the slowest traces during spikes with normal ones to see which stage grows.

    Once found, fix the cause rather than raising timeouts: spread out the periodic work, add jitter to expiry times, or move heavy jobs off the request path.

    Example

    Spikes to 2 seconds appear every 5 minutes on the job search endpoint.

    1. The cached list of popular searches has a 300-second expiry, the same as the spike interval.
    2. When it expires, about 200 concurrent requests all miss the cache and run the same 1.5-second query together.
    3. Adding a lock, so only one request rebuilds the entry while others serve the old value, removes the pile-up.
    4. Adding a random 0 to 30 seconds of jitter to related expiry times also stops entries expiring together, and the spikes disappear.
  10. Q90.An async Python service stops responding but its CPU usage is low. How would you debug it?

    Low CPU with no progress means tasks are waiting for something that is not happening: a blocked event loop thread, a deadlock, or a resource that is fully used and never released.

    1. Check whether the loop is alive: a health endpoint that does no I/O shows whether the event loop can run anything at all.
    2. Dump stacks: the standard faulthandler module can print every thread's stack on a signal, which shows a thread stuck in a blocking call.
    3. List tasks: dump all pending tasks with their stacks, using asyncio.all_tasks() and each task's stack, to see what each one is awaiting.
    4. Check pools and limits: connection pools, semaphores and queues at their maximum with waiters piling up suggest leaked slots, such as a connection never returned on an error path.
    5. Look for cycles: two tasks each awaiting something the other must produce, or a lock acquired twice.
    6. Enable debug mode in a test environment to log slow callbacks and coroutines that are never awaited.

    Typical root causes: a synchronous call blocking the loop in a function that rarely runs, a missing finally that leaks a pool slot, and an external call with no timeout waiting forever.

    Interview tip: stress that you would add a timeout to every await on external resources; it turns a silent hang into a visible, recoverable error.

  11. Q91.Python worker processes keep getting killed for running out of memory. What do you do?

    Find out whether memory grows steadily, which suggests a leak, or spikes on certain requests, which suggests large data being loaded at once. Then compare total usage with the container or machine limit.

    1. Check the arithmetic: workers multiplied by memory per worker, plus spikes, must fit under the limit with headroom.
    2. Find the spike: correlate the kills with requests or jobs running at the time, such as exports, reports or large uploads.
    3. Stream instead of loading: read query results in batches and stream responses, rather than building huge lists or strings.
    4. Fix leaks: compare allocation snapshots over time and bound any caches.
    5. Account for process memory sharing: worker processes that start by forking share memory at first, but Python's reference counting writes to objects, so shared pages are gradually copied into each worker.
    6. Tune: reduce workers per container, recycle workers after a number of requests, or give memory-heavy jobs their own workers with larger limits.

    Restarting is a stopgap. Unbounded memory per request is a design problem that will return with the next bigger customer.

    Example

    A container with a 4 GB limit runs 8 workers using about 350 MB each.

    1. Baseline use is 8 × 350 MB = 2.8 GB, leaving 1.2 GB of headroom.
    2. An export endpoint loads 400,000 rows into a list, adding about 1.3 GB to one worker.
    3. During an export, total use passes 4 GB and the platform kills the container, dropping every in-flight request.
    4. Streaming the export in batches of 1,000 rows keeps that worker's growth near 20 MB, and the kills stop.
  12. Q92.How would you design a URL shortener as a Python service?

    A short code maps to a long URL. Writes are rare, and reads are frequent and latency-sensitive, so the design centers on fast lookups through a cache in front of a durable store.

    flowchart LR
      A["Create request"] --> B["API service"]
      B --> C["Generate unique short code"]
      C --> D["Durable database"]
      E["Redirect request"] --> F["Cache lookup"]
      F -->|miss| D
      F --> G["Return redirect"]
      G --> H["Click event queue"]
      H --> I["Analytics workers"]

    Key decisions:

    • Code generation: encode a unique numeric ID in base 62, using digits and both letter cases. Seven characters cover over three trillion codes. Sequential IDs are guessable, so add randomness or shuffle the ID space if codes must not be enumerated.
    • Redirect type: a permanent redirect lets browsers cache it, which reduces load but hides repeat clicks; a temporary redirect keeps every click visible.
    • Caching: cache code-to-URL mappings with long expiry, since they rarely change.
    • Analytics off the hot path: publish click events to a queue and aggregate them in workers.
    • Abuse: validate destination URLs, rate-limit creation and support disabling malicious links.

    Example

    A base 62 encoder turns numeric IDs into short codes; seven characters reach about 3.5 trillion values.

    ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    
    def encode(number):
        if number < 0:
            raise ValueError("number must be non-negative")
        if number == 0:
            return ALPHABET[0]
        digits = []
        while number:
            number, remainder = divmod(number, 62)
            digits.append(ALPHABET[remainder])
        return "".join(reversed(digits))
    
    def decode(code):
        number = 0
        for char in code:
            number = number * 62 + ALPHABET.index(char)
        return number
    
    print(encode(125))  # 21
    print(encode(62**7 - 1))  # ZZZZZZZ
    print(decode(encode(987654321)) == 987654321)  # True
  13. Q93.How would you design a reliable webhook delivery system in Python?

    Accept events quickly, store them durably, and deliver them from background workers that sign each request, retry with backoff and give up gracefully, so slow or broken subscriber endpoints never affect the rest of the system.

    flowchart LR
      A["Business event"] --> B["Store delivery record"]
      B --> C["Delivery queue"]
      C --> D["Worker signs and sends request"]
      D --> E{"Subscriber accepted?"}
      E -->|yes| F["Mark delivered"]
      E -->|no| G{"Attempts left?"}
      G -->|yes| H["Schedule retry with backoff"]
      G -->|no| I["Mark failed and notify owner"]
      H --> C

    Design points:

    • Durability first: write the delivery record in the same transaction as the business change, so no event is lost if the process crashes.
    • Signatures: sign each body with a per-subscriber secret and include a timestamp, so receivers can verify origin and reject replays.
    • Idempotency: include a unique event ID, because retries mean receivers will sometimes get duplicates.
    • Isolation: short timeouts and per-subscriber concurrency limits stop one slow endpoint from blocking deliveries to others.
    • Backoff and limits: retry over hours with growing delays, then stop, and automatically pause endpoints that fail persistently.
    • Visibility: give subscribers a delivery log and a way to replay failed events.
  14. Q94.How would you design a multi-tenant Python backend?

    Choose how tenants share storage, then enforce tenant isolation in one central place so no query can forget it. Isolation failures, one customer seeing another's data, are among the most serious bugs a business application can have.

    Shared tables with a tenant columnSchema per tenantDatabase per tenant
    Every row carries a tenant IDEach tenant gets its own set of tablesEach tenant gets its own database
    Weakest isolation; one missed filter leaks dataStronger isolation, with more objects to migrateStrongest isolation and easy per-tenant backup
    Cheapest and simplest to operate at scalePractical for hundreds of tenantsHighest cost and operational effort
    Best for many small tenantsBest for mid-sized tenants needing separationBest for large or regulated customers

    Enforcing isolation:

    1. Resolve the tenant from the authenticated identity, never from a client-supplied field alone.
    2. Store it in a context variable for the request, and have the repository layer add the tenant filter to every query automatically.
    3. Where the database supports it, add row-level security policies as a second barrier.
    4. Include the tenant in cache keys, file paths and background job payloads.
    5. Test cross-tenant access explicitly, and add per-tenant rate limits so one noisy tenant cannot starve the others.
  15. Q95.How does the cache hit ratio change the load on a database?

    Only cache misses reach the database, so database load falls in proportion to the miss ratio, not the hit ratio. Small improvements in a high hit ratio therefore have a large effect.

    • database_queries_per_second = read_requests_per_second × (1 − hit_ratio)
    • average_latency = hit_ratio × cache_latency + (1 − hit_ratio) × database_latency

    What each term means:

    • hit_ratio: the share of reads served from the cache, between 0 and 1.
    • cache_latency and database_latency: the typical time for a read from each source.

    When to use it:

    • To estimate how much a cache protects the database, and what happens if the cache fails and every read becomes a miss.
    • To decide whether improving the hit ratio, for example with longer expiry or warming popular keys, is worth the staleness it brings.

    Plan for a cold cache after deploys or restarts: the database must survive the miss rate while the cache refills, or refills must be throttled.

    Example

    An API serves 2,000 reads per second; cache reads take 1 ms and database reads take 20 ms.

    1. At a 90% hit ratio, the database receives 2,000 × 0.10 = 200 queries per second.
    2. At 98%, it receives 2,000 × 0.02 = 40 queries per second, five times fewer.
    3. Average latency at 90% is 0.9 × 1 + 0.1 × 20 = 2.9 ms; at 98% it is 0.98 × 1 + 0.02 × 20 = 1.38 ms.
    4. If the cache goes down, the database suddenly receives all 2,000 queries per second, ten times its usual load at 90%, which is why cache failure needs a plan.
  16. Q96.How would you merge overlapping time intervals, such as bookings, in Python?

    Sort the intervals by start time, then walk through them, extending the last merged interval while the next one starts before it ends, and starting a new interval otherwise. Sorting makes a single pass enough.

    1. Validate: reject intervals whose start is after their end.
    2. Sort by start time, which takes O(n log n).
    3. Merge: if the current interval starts at or before the end of the last merged one, extend that end to the larger of the two; otherwise append it as a new interval.
    4. Return the merged list, which is still sorted.

    Decisions to state in an interview:

    • Whether intervals that only touch, such as 13 to 15 and 15 to 16, should merge. Here they do, which suits back-to-back bookings shown as one busy block.
    • Whether the input may be modified, or a new list should be returned.

    The same pattern answers related questions, such as finding free gaps between meetings or the total time a room was booked.

    Example

    Five bookings merge into three busy blocks, and touching intervals are joined.

    def merge(intervals):
        merged = []
        for start, end in sorted(intervals):
            if start > end:
                raise ValueError("start must not be after end")
            if merged and start <= merged[-1][1]:
                merged[-1][1] = max(merged[-1][1], end)
            else:
                merged.append([start, end])
        return [tuple(pair) for pair in merged]
    
    bookings = [(9, 11), (13, 15), (10, 12), (15, 16), (18, 19)]
    print(merge(bookings))  # [(9, 12), (13, 16), (18, 19)]
    print(merge([]))  # []
  17. Q97.How would you compute latency percentiles from access log lines in Python?

    Parse each line with a strict pattern, collect the durations, sort them, and pick the value at the requested rank. Percentiles describe what most users experience far better than an average, which a few slow requests can distort.

    1. Parse defensively: use a regular expression and skip or count lines that do not match, rather than crashing on one malformed line.
    2. Sort the durations once.
    3. Nearest-rank method: the p-th percentile is the value at position ceil(p / 100 × n) in the sorted list, counting from 1.
    4. Report several values: the median, the 95th and the 99th percentiles together show both typical and worst-case behavior.

    For production scale:

    • Sorting every value does not scale to billions of requests, so monitoring systems use histograms or summary sketches that estimate percentiles in fixed memory.
    • Percentiles cannot be averaged across servers; merge the underlying histograms instead.
    • Different tools use different interpolation methods, so small differences between tools are expected.

    Example

    Ten log lines give a median of 110 ms, while the 95th percentile is 1,020 ms because of one slow request.

    import math
    import re
    
    LOG = """GET /jobs 200 120ms
    GET /jobs 200 95ms
    POST /apply 201 340ms
    GET /jobs/7 200 88ms
    GET /jobs 500 1020ms
    GET /login 200 60ms
    GET /jobs 200 110ms
    POST /apply 201 290ms
    GET /jobs 200 130ms
    GET /jobs/9 404 45ms"""
    
    pattern = re.compile(r"^(\w+) (\S+) (\d{3}) (\d+)ms$")
    
    def percentile(values, p):
        ordered = sorted(values)
        rank = math.ceil(p / 100 * len(ordered))
        return ordered[max(rank, 1) - 1]
    
    durations = []
    for line in LOG.splitlines():
        match = pattern.match(line)
        if match:
            durations.append(int(match.group(4)))
    
    print(len(durations))  # 10
    print(percentile(durations, 50))  # 110
    print(percentile(durations, 95))  # 1020
    print(round(sum(durations) / len(durations), 1))  # 229.8
  18. Q98.How do you document a Python API so clients can rely on it?

    Generate a machine-readable description of the API from the same types and validation rules the code uses, then add the human context that types cannot carry: purpose, examples, errors and guarantees.

    • One source of truth: frameworks can derive the request and response schemas from typed models, so the documentation cannot drift from the code.
    • Complete operations: each endpoint lists its parameters, body schema, response shapes, status codes and authentication requirements.
    • Examples: realistic request and response examples answer most client questions faster than prose.
    • Errors: document the error body format and every stable error code clients may handle.
    • Behavior: describe pagination, rate limits, idempotency keys, ordering and how long tokens last.
    • Change policy: state how versions work, what counts as a breaking change, and how deprecations are announced, with a changelog.

    Keep it trustworthy:

    1. Validate the published description in the pipeline and fail the build if it breaks.
    2. Run contract tests that send documented examples to the real service.
    3. Generate client libraries from the description where clients want them.
  19. Q99.How should you explain a Python backend project you personally built?

    Pick one real project and tell it as a clear story: the problem, your part, the key decisions, the hardest issue and the measurable outcome. Interviewers want your judgment, not a tour of every library.

    1. Context: what the service did, who used it and its scale, such as requests per second or data volume.
    2. Your role: what you owned personally, separate from the team's work.
    3. Design decisions: for example, synchronous versus async, how you structured the code, and which database and caching approach you chose and why.
    4. A hard problem: a performance issue, a data bug or a concurrency problem, how you diagnosed it, and what the evidence showed.
    5. Quality and operations: how you tested it, deployed it and monitored it.
    6. Results: numbers where possible, such as "p95 latency fell from 900 ms to 180 ms" or "the nightly job went from 3 hours to 20 minutes".
    7. Reflection: what you would do differently now.

    What the interviewer is listening for:

    • Trade-off reasoning, not just a list of technologies.
    • Ownership, and honesty about mistakes.
    • Whether you can explain technical choices simply.
  20. Q100.How do you discuss a production incident you debugged in a Python service?

    Walk through the incident in order: what users saw, how you found the cause, how you fixed it, and what you changed so it would not happen again. Calm, evidence-driven reasoning matters more than a dramatic story.

    1. Impact: what broke, for whom and for how long, in concrete terms such as error rates or affected orders.
    2. Detection: how the problem was noticed, and whether alerts or customers found it first.
    3. Mitigation: what you did to stop the impact quickly, such as a rollback, a feature flag or scaling, before the root cause was known.
    4. Diagnosis: the evidence you gathered, such as logs, traces, stack dumps or profiles, and the hypotheses you ruled out.
    5. Root cause: stated plainly, including contributing factors such as a missing timeout or test.
    6. Prevention: the fixes, tests, alerts or process changes that followed, and how you verified they worked.
    7. Lessons: what you learned about the system or about debugging under pressure.

    What the interviewer is listening for:

    • Restoring service first, then investigating.
    • Blameless language that focuses on systems rather than people.
    • Follow-through that makes the failure less likely or less harmful.