Code explainer prompts: from a regex to a stack trace

Sixteen ready questions that ask the tool to explain code you did not write. Each one already carries a snippet, a pattern, a command or a stack trace, and says who the answer is for and how deep it should go.

Reading someone else’s code

Inheriting a repository: understand someone else's code when the comments never existed, read a file in a language you have never written, get a function explained line by line, and learn why it was built this way.

A function with no comments

Turns inherited code into one plain sentence about what it actually calculates.

Prompt

def recalc(items, rate): total = 0 for it in items: if it.status != "cancelled": total += it.price * it.qty if total > 10000: total = total * (1 - rate) return round(total, 2)# inherited this from the previous developer, no comments anywhere — explain what this function works out and under what conditions

Result

The function works out an order total with a discount applied.1. It multiplies price by quantity across the line items, skipping cancelled ones: a line with status "cancelled" never reaches the total.2. Once the total passes 10000, rate is applied to it — a rate of 0.1 means ten percent off.3. The result is rounded to two decimal places, i.e. to cents.The detail only the code tells you: the discount applies to the whole total, not to the amount above the threshold. An order of 10001 ends up cheaper after the discount than an order of 9999.

Code in a language you do not write

Explains an unfamiliar construct through the one you already use in your own language.

Prompt

import functoolsdef retry(times): def decorator(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): for attempt in range(times): try: return fn(*args, **kwargs) except TimeoutError: continue raise RuntimeError("all attempts failed") return wrapper return decorator# I write PHP and have never seen a decorator — explain it in terms I already know

Result

This is a decorator that takes an argument. The closest thing you already know in PHP is a function that returns a closure wrapped around another function.Read it from the inside out:- retry(3) returns decorator;- decorator receives your function and returns wrapper;- writing @retry(3) above a function means exactly fn = retry(3)(fn) — from then on the name points at wrapper.wrapper itself calls the original up to three times and quietly swallows TimeoutError, then raises RuntimeError once the attempts run out. functools.wraps is there so the wrapper keeps the original name and docstring — without it, the debugger and every traceback would just say wrapper.

A line-by-line read

Walks the snippet one line at a time, for when it is one specific line that makes no sense.

Prompt

def top_customers(orders, n): totals = dict() for order in orders: totals[order.customer_id] = totals.get(order.customer_id, 0) + order.total ranked = sorted(totals.items(), key=lambda pair: -pair[1]) return ranked[:n]# walk me through it line by line, especially the sorted call and the minus in front of pair

Result

1. totals = dict() — an empty dictionary that collects a sum per customer.2. The loop runs over the orders. totals.get(id, 0) returns what has been accumulated, or zero the first time a customer shows up — which is why no separate first-order check is needed.3. sorted over items() sorts customer-and-total pairs. The minus in front of pair[1] flips the order: descending instead of ascending, so the biggest customers end up on top.4. ranked[:n] keeps the first n pairs.It returns the pairs rather than just the ids on purpose: the caller needs the total to display it, and now does not have to add it up a second time.

Why it was written this way

Explains not what the code does, but why the author picked this shape for it.

Prompt

_cache = None_lock = threading.Lock()def get_config(): global _cache if _cache is None: with _lock: if _cache is None: _cache = load_config_from_disk() return _cache# why is the None check written twice, once outside the lock and once inside it

Result

This is double-checked locking, and each of the two checks is there for a different reason.The first one is the fast path. Once the config has been loaded, every later call sees the ready value and returns immediately without touching the lock. Acquiring a lock costs time on every single call, and it is genuinely needed exactly once in the life of the process.The second one is about the race. While the first thread holds the lock and reads the file, a second thread has already passed the outer check and is queued behind it. Without the inner check it would load the config a second time and overwrite the first result.The global line is what makes the assignment rebind the module-level variable instead of creating a new local one.

Errors and stack traces

Red text you have to get through before you can fix anything: what an error message actually means, the stack trace behind a crash, an install that fails on a clean machine, and one line out of the production log.

An application crash

Reads the stack trace: where it broke, which line is guilty, and what the error itself means.

Prompt

Traceback (most recent call last): File "app/orders.py", line 42, in create_order total = calc_total(items) File "app/pricing.py", line 17, in calc_total return sum(item.price * item.qty for item in items)AttributeError: 'dict' object has no attribute 'price'what actually happened here and which line should I be looking at

Result

AttributeError means the object has no attribute called price.Read a trace downwards for the call path and upwards for the cause:- create_order in orders.py, line 42, called calc_total;- calc_total in pricing.py, line 17, reached for item.price and blew up.Line 17 is where it failed, but that is not where the problem is. The message says items holds dictionaries rather than objects, and dot access does not work on a dictionary. So the caller handed over data straight from JSON without building it into a model. Fix it where items is assembled, or read the field as item["price"].

The install will not go through

Translates a dependency log out of package-manager language and into plain English.

Prompt

ERROR: Could not find a version that satisfies the requirement pandas==2.9.1 (from versions: 2.0.0, 2.1.4, 2.2.2)ERROR: No matching distribution found for pandas==2.9.1cannot install the dependencies, what is this message telling me

Result

That version of the library does not exist.pip went to the package index, found pandas and listed what is available — the newest of them is 2.2.2. You are asking for 2.9.1, which nobody publishes, hence "no matching distribution".In practice it is almost always one of three things: a typo in the version number, a requirements.txt carried over from a newer project, or a version that only ships for a different Python. The second line is not a separate problem — it is the outcome of the first.

A line from the production log

Explains what the server is actually reporting when the log shows a timeout and three numbers.

Prompt

2026-09-12T03:14:07Z ERROR db pool: timeout acquiring connection after 30s, active=20 idle=0 waiting=1372026-09-12T03:14:07Z ERROR api POST /orders 503 in 30012ms request_id=8c1f2awhat is the server trying to tell me with these two lines

Result

The database connections ran out.First line: the pool waited 30 seconds for a free connection and gave up. active=20 means all twenty are in use, idle=0 means none are free, waiting=137 means a hundred and thirty-seven requests are queued behind them.Second line is the consequence: POST /orders hung for the same 30 seconds and returned 503.Read together: requests are arriving faster than the database releases connections. The pool is not broken — it is honestly reporting that it hit its ceiling. The next place to look is the long-running queries that hold a connection, and the pool size relative to the number of workers.

Regexes and commands

Lines that work until somebody touches them: explain a regex character by character, say what a validation pattern really lets through, decode an expression from a config file, and take apart a runbook command.

What the regex actually checks

Breaks the pattern into parts and says which addresses it lets through and which it rejects unfairly.

Prompt

^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z][A-Za-z]+$this sits in our form validation — what exactly does it check and what does it let through that it should not

Result

The pattern checks the shape of an address, not whether it exists.- ^ and $ require the whole string to match, not just a piece inside it.- The part before the at sign: letters, digits, dot, underscore, percent, plus and hyphen — one character or more.- @ — exactly one at sign.- The domain: letters, digits, dots and hyphens.- Then a dot and a zone of at least two letters.What slips through: two dots in a row, a leading dot in the local part, a domain that does not resolve. What gets rejected for no good reason: internationalised addresses and non-Latin top-level domains. For a form this is usually enough — existence is what the confirmation email is for.

An expression out of a config file

Shows which parts of a log line land in which capture group, and in what order.

Prompt

^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) - - \[([^\]]+)\] "([A-Z]+) ([^ ]+) [^"]+" ([0-9]+) ([0-9]+)$this parses an access.log line — what ends up in each group

Result

The expression splits a web-server log line into six capture groups.1. The IP address: four numbers separated by dots. The escaped dot means a literal dot, not "any character".2. The timestamp in square brackets: anything except a closing bracket is allowed inside, which is what stops the group from swallowing the rest of the line.3. The request method: capital letters.4. The path: everything up to the first space.5. The status code.6. The response size in bytes.The order of the groups is the order of the fields in the result: group one is the address, group five is the status. A line whose path contains a space will not match as a whole and is simply skipped.

A command copied out of a runbook

Takes the terminal chain apart before you press Enter on a production box.

Prompt

tar -czf - ./data | ssh backup@nas "cat > /backups/data-$(date +%F).tar.gz"# found this in our backup runbook — explain what will happen before I run it

Result

The command backs the data folder up onto another machine without storing anything on this one.- tar -czf - builds and compresses the archive, but writes it to standard output instead of a file: that is what the lone hyphen means.- The pipe hands that stream to the next command.- ssh connects to the host nas as the user backup and runs cat there, which drops the stream into a file.- $(date +%F) substitutes today’s date into the filename, producing data-2026-09-13.tar.gz.The archive never lands anywhere in full — it streams over the network as it is built, so no free disk space is needed on the source machine. There is one sharp edge: if a file with that name already exists, it is overwritten without a word.

SQL and database queries

Statements that fill half the screen: explain an SQL query join by join, work out what a window function is counting, and see what a statement risks on live data before it is let anywhere near production.

A report query

Turns a long SELECT into one sentence about which rows end up in the report.

Prompt

SELECT c.name, date_trunc('month', o.created_at) AS month, count(*) AS orders, sum(o.total) AS revenueFROM orders oJOIN customers c ON c.id = o.customer_idWHERE o.created_at >= now() - interval '1 year'GROUP BY 1, 2HAVING sum(o.total) > 1000ORDER BY revenue DESC-- explain what this query returns and why some customers never show up in the report

Result

The query works out revenue per customer, broken down by month, for the last year.- JOIN ties an order to a customer. It is a plain join, not a LEFT one, so customers who never ordered do not appear at all.- WHERE cuts off anything older than a year from the current moment.- date_trunc rounds the date down to the first of the month — that is what gathers individual orders into monthly buckets.- GROUP BY 1, 2 groups by the first two expressions in the select list: the customer name and the month.- HAVING drops buckets below 1000. Unlike WHERE, this filter runs after the sum has been computed.One row of the result is one customer in one month, not one customer overall.

A window function

Explains what OVER computes and how a window differs from an ordinary GROUP BY.

Prompt

SELECT customer_id, created_at, total, sum(total) OVER (PARTITION BY customer_id ORDER BY created_at) AS running_total, row_number() OVER (PARTITION BY customer_id ORDER BY total DESC) AS rnFROM orders-- explain what these two OVER clauses compute and why the rows are not collapsed

Result

A window function computes across a group without collapsing the rows: the output has as many rows as the input, each one simply gains two extra columns.- PARTITION BY customer_id splits the table into per-customer piles. A pile is the window.- In the first column the sum is a running total: ORDER BY created_at sets the order inside the window, and by default the frame covers everything from the start of the window to the current row. For a customer’s third order it is the sum of the first three.- The second OVER orders differently — by total, descending — and row_number stamps positions onto it: 1 goes to that customer’s largest order.That is exactly what separates a window from GROUP BY: grouping would have left one row per customer and thrown the individual orders away.

What the query is risking

Explains the query and points out what it will trip over once real data is involved.

Prompt

SELECT *FROM orders oLEFT JOIN order_items i ON i.order_id = o.idWHERE DATE(o.created_at) = '2026-09-01' AND o.status != 'cancelled'ORDER BY o.created_at DESC-- instant on the test database, hangs in production — explain the query and what it runs into here

Result

The query pulls the orders placed on the first of September together with all of their line items. LEFT JOIN means an order with no items still appears in the output, with the order_items columns empty.Where it trips on real data:- DATE(o.created_at) is a function wrapped around the column, and it stops the index on created_at from being usable: the database has to compute a date for every row in the table. Invisible on a hundred test rows, a full scan on millions.- SELECT * across a one-to-many join returns each order once per line item. There are far more rows out than there are orders, and the application has to collapse them itself.- Both tables have columns of the same name (id, created_at), so the result mixes them together and the names no longer say which table a field came from.

Explaining to a beginner or in an interview

When the explanation matters more than the code itself: explain code to a beginner looking at a first program, talk through a technical interview answer out loud, and put a script in terms a manager follows.

A first program

An explanation for someone in their second week of code: what each line does and why it is written that way.

Prompt

nums = [4, 8, 15, 16, 23, 42]total = 0for n in nums: if n % 2 == 0: total += nprint(total / len(nums))# just started learning Python — walk me through what is happening here step by step

Result

The program adds up the even numbers in the list and divides the result by how many numbers there are IN TOTAL.1. nums is a list of six numbers.2. total = 0 is the box we will add into. The zero is there so there is something to start from.3. for n in nums is the loop: n becomes each number of the list in turn.4. n % 2 == 0 means the remainder after dividing by two is zero, i.e. the number is even.5. total += n adds what we found to what we have.6. Dividing by len(nums) divides by the length of the list, which is six.This looks like a bug in the intent: the list holds four even numbers adding up to 70, and that is divided by six. If you want the average of the even numbers, divide by how many even ones there were.

An interview question

A breakdown of the algorithm you could say out loud at a whiteboard.

Prompt

def search(nums, target): lo, hi = 0, len(nums) - 1 while hi >= lo: mid = (lo + hi) // 2 if nums[mid] == target: return mid if nums[mid] > target: hi = mid - 1 else: lo = mid + 1 return -1# got asked this in an interview — explain the idea behind the algorithm and why it is fast

Result

This is a binary search over a sorted array. There is one idea in it: each step throws away half of what is left.- lo and hi are the bounds of the range the answer could still be in.- mid is the middle. The double slash is integer division, so you get an index rather than a fraction.- If the middle is the target, you are done.- If the middle is larger, the answer is to the left, so hi moves.- Otherwise it is to the right, so lo moves.- The loop ends when the bounds cross: the value is not there and -1 comes back.The cost is logarithmic: a million elements means about twenty steps instead of a million. The hard requirement is that the array is already sorted — otherwise the algorithm silently returns a wrong answer.The usual follow-up in an interview: in languages with fixed-width integers lo + hi can overflow, which is why people write lo + (hi - lo) // 2 there.

Explaining it to your manager

A retelling of the script with no jargon at all, for a conversation with someone who does not read code.

Prompt

import csv, smtplibrows = list(csv.DictReader(open("orders.csv")))late = [r for r in rows if r["shipped_at"] == ""]body = "Orders not shipped yet: " + str(len(late))smtplib.SMTP("mail.local").sendmail("bot@shop.local", "ops@shop.local", body)# I need to explain to a non-technical manager what this script does

Result

The script checks which orders have not shipped yet and reports the number by email.Step by step, without jargon:1. It opens the orders table — an ordinary file that Excel would open too.2. It picks out the rows where the shipping date is empty: the order was placed but never left.3. It counts how many such rows there are.4. It sends an email to the team address with that single number in it.What a manager needs to know: the script changes nothing and gets in nobody’s way — it only reads and reports, so it can run every hour if you like. Its one weak spot is that it reads a file, which means it shows the picture as of the last export rather than right now.

Code explainer prompts: 16 ready-made reads | iBro