Skip to content
spot_the_ai_bug

Python debugging & code review interview questions

Python interviews love "what's wrong with this function?" questions because the language hides its sharp edges behind friendly syntax. Each question below is a short, realistic snippet with exactly one bug. Read it like a pull request, decide which line is wrong, then reveal the answer and the fix.

Python gotchas interviewers love

  • Default arguments are evaluated once, at definition time.
  • Negative slicing with a variable breaks when the variable is 0.
  • f-strings in SQL or shell commands are injection vectors.
  • Check-then-act on the filesystem is a race between processes.
  • Logging whole request objects leaks tokens and cookies.

5 Python spot-the-bug questions

Each snippet has exactly one bug. Find the line before you open the answer.

Mutable default argument

add_tag() should return a fresh tag list for each post unless one is passed in. Why do tags from one post show up on another?

def add_tag(tag: str, tags: list[str] = []) -> list[str]:    """Append a tag and return the list of tags."""    if tag not in tags:        tags.append(tag)    return tags  first = add_tag("python")   # ["python"]second = add_tag("sql")     # expected ["sql"]
Show answer

line 1 · Wrong API usage

Default values are evaluated once, when the function is defined, so every call without tags shares the same list. second comes back as ["python", "sql"], and the list keeps growing for the life of the process.

Fix
def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:    tags = [] if tags is None else tags

Interview follow-up: Interviewers often ask why None is the conventional sentinel and whether the same trap applies to dicts and sets (it does).

f-string SQL query

This looks up a user by email for the login form. What's the problem?

import sqlite3  def find_user(conn: sqlite3.Connection, email: str):    cur = conn.cursor()    cur.execute(f"SELECT id, email, password_hash FROM users WHERE email = '{email}'")    return cur.fetchone()
Show answer

line 6 · Security / injection

The email is pasted into the SQL text, so input like ' OR '1'='1 changes the query and returns the first user, letting an attacker log in as someone else. Let the driver bind the value instead.

Fix
    cur.execute("SELECT id, email, password_hash FROM users WHERE email = ?", (email,))

Interview follow-up: Expect a follow-up on why escaping quotes by hand is not enough, and how ORMs parameterize queries for you.

Check-then-create directory

Several worker processes call ensure_dir() at startup. Occasionally one crashes with FileExistsError. Why?

import os  def ensure_dir(path: str) -> str:    """Create the directory if it doesn't exist yet."""    if not os.path.exists(path):        os.makedirs(path)    return path
Show answer

line 7 · Race condition

This is a time-of-check to time-of-use race. Two processes can both see that the path doesn't exist, then both call makedirs, and the slower one raises FileExistsError. Make the operation itself idempotent instead of checking first.

Fix
    os.makedirs(path, exist_ok=True)

Interview follow-up: A good answer generalizes: prefer atomic operations (exist_ok, O_EXCL, INSERT ... ON CONFLICT) over check-then-act.

Last N items with a slice

last_n() powers the "recent activity" widget. A user set their widget size to 0. What do they see?

def last_n(items: list, n: int) -> list:    """Return the last n items (n >= 0)."""    if n < 0:        raise ValueError("n must be non-negative")    return items[-n:]
Show answer

line 5 · Wrong edge case

When n is 0, -n is also 0, and items[0:] is the whole list. Asking for zero items returns everything.

Fix
    return items[max(len(items) - n, 0):]

Interview follow-up: Interviewers like this one because the code is correct for every n except the boundary. Ask yourself about 0, 1, and n > len(items).

Logging every request header

This middleware was added to debug a production issue. What should a reviewer flag?

import logging logger = logging.getLogger(__name__)  def log_request(request) -> None:    logger.info("%s %s", request.method, request.path)    logger.info("headers: %s", dict(request.headers))
Show answer

line 8 · Leaked secret

Request headers include Authorization bearer tokens, session cookies and API keys. Writing them to logs leaks live credentials to everyone with log access and to every log pipeline they pass through.

Fix
    SENSITIVE = {"authorization", "cookie", "x-api-key"}    safe = {k: ("[redacted]" if k.lower() in SENSITIVE else v) for k, v in request.headers.items()}    logger.info("headers: %s", safe)

Interview follow-up: Mention that leaked tokens must be rotated, not just deleted from logs.

Every day, everyone gets the same pull request written by AI. It looks clean. It has comments. It has exactly one real bug.

  1. 1

    Find the line

    Tap the buggy line, then hit Submit. You get 3 attempts.

  2. 2

    Read the hints

    A miss within 2 lines is 🔥 Warm. Anything further is ❄️ Cold.

  3. 3

    Name the bug

    Found it? Bonus round: pick the bug type from 4 options. It doesn't affect your win, just your bragging rights.

// your share card

found in 3/3

🟥 cold miss · 🟨 warm miss · 🟩 found · ⬛ unused

Keyboard: ↑ ↓ to move between lines, Enter to submit. A new bug drops every day at midnight IST.

0
Played
0
Win %
0
Streak
0
Max

Attempts distribution