Give ChatGPT a question it can answer from the code

Pick one behavior you need to understand: a surprising return value, a loop that stops early, or a function that changes an object you passed in. Include the actual line that calls the function; shipping_fee(50, True), for example, tells ChatGPT more than the function name alone. Say whether you want beginner-level language or a terse technical trace. Remove secrets, customer data, private URLs, and code you are not allowed to share.

If the function calls a helper, include that helper or describe its inputs and outputs. If it reads a database, file, or API, include a safe example response. Otherwise, ask ChatGPT to name what it cannot determine. OpenAI's prompting guidance recommends specifying the result, relevant context, and useful output format; here, an exact call and a trace make the explanation checkable.

A complete example: which return wins?

This example is small enough to follow without knowing Python generators. The question is whether the first condition still matters when member is true. We sent this request and code to ChatGPT web on September 27; the line breaks below are formatted for the page. You can copy it, then replace the function and calls with your own approved code.

Complete worked prompt
Please explain this Python function for a beginner who knows if and return,
but is learning how an early return affects later checks. Do not rewrite it.

def shipping_fee(total, member):
    if total >= 50:
        return 0
    if member:
        return 3
    return 6

Trace these exact calls in a table: shipping_fee(50, False),
shipping_fee(49, True), and shipping_fee(49, False).
For each, list the condition results in order, where execution stops,
and the returned value. Explain what happens for
shipping_fee('50', False) without inventing business rules.
End with one question that checks whether I understand the first return.
Keep it under 250 words.

ChatGPT's response gave the following results. Each row can be checked by reading the conditions from top to bottom; a return ends this call immediately.

CallConditions checked, in orderWhere it stopsResult
shipping_fee(50, False)total >= 50 is trueFirst return; member is not checked0
shipping_fee(49, True)First condition false; member trueSecond return3
shipping_fee(49, False)Both conditions falseFinal return6

For shipping_fee('50', False), ChatGPT said Python 3 raises TypeError while comparing the string '50' with the number 50. It did not invent a rule to convert the string or charge a fee. Code can show what happens with an input without showing what the business intended to do with it.

Run the predictions, then explain one back

Save the function from the prompt in fee_example.py and append these checks. Run python3 fee_example.py, or use the Python 3 command configured on your machine. You should see Five checks passed. An assertion failure means a prediction needs another look; a missing Python command means the runtime needs to be set up before this check is possible.

assert shipping_fee(50, False) == 0
assert shipping_fee(49, True) == 3
assert shipping_fee(49, False) == 6
assert shipping_fee(50, True) == 0

try:
    shipping_fee("50", False)
except TypeError:
    pass
else:
    raise AssertionError("Expected TypeError")

print("Five checks passed")

To test understanding rather than collect another paraphrase, we deliberately sent ChatGPT a wrong summary:

Teach-back follow-up from the same run
My teach-back: because member is True, shipping_fee(50, True)
returns 3; the member rule overrides the total rule. Is that
correct? Point to the first line where my explanation goes wrong.
Then give me one new call to predict, but do not reveal its answer yet.

ChatGPT corrected the first wrong step: 50 >= 50 is true, so the function returns 0 before it checks member. It then asked us to predict shipping_fee(25, True). We answered “first condition false, member true, return 3”; ChatGPT confirmed that trace, and the same function returns 3 locally.

Try this with your own code: explain one result in your own words, including the condition that runs first. Ask ChatGPT to point to the first wrong step, then predict a new input before revealing the answer. This catches a misunderstanding that a copied explanation can hide.

When execution waits: a generator

A second case needs a different question: when does the function body run? A retained August 19 ChatGPT screenshot shows only the beginning of a generator explanation. The original input code was not saved, so the runnable function below reconstructs the described behavior. We checked it locally; it is not presented as the exact code from that earlier ChatGPT run.

def accepted_windows(delays, max_total):
    elapsed = 0
    for attempt, delay in enumerate(delays, start=1):
        if delay < 0:
            raise ValueError("delay must be non-negative")
        if elapsed + delay > max_total:
            break
        elapsed += delay
        yield {"attempt": attempt, "delay": delay, "elapsed": elapsed}

Ask ChatGPT to trace list(accepted_windows([1, 2, 4, 8], 6)), then compare it with a budget of 7 and with list(accepted_windows([1, -1], 10)). Include the exact call: creating accepted_windows(...), calling next(iterator), and calling list(iterator) produce different observations because a generator pauses at each yield. The budget-six trace is:

AttemptDelayElapsed beforeConditionActionElapsed after
1100 + 1 > 6 is falseYield attempt 11
2211 + 2 > 6 is falseYield attempt 23
3433 + 4 > 6 is trueBreak before yielding3

The retained screenshot shows the start of a ChatGPT explanation, not this complete table. Use the text trace and local checks for the full reconstructed example.

ChatGPT execution trace explaining a Python generator with elapsed state and a budget condition
The retained ChatGPT screenshot shows the purpose statement and beginning of a state trace. The complete teaching trace is available as text in this guide. Credit: Grow AI Skills editorial test capture. Open full-size image.

Creating the generator object does not execute its body. The first next() call reaches the first yield; the second resumes after that yield. With a budget of seven, attempt 3 is accepted because 3 + 4 > 7 is false. A negative second delay raises ValueError only when iteration reaches it. If the caller used list(...), that call raises instead of returning a partial list, although a caller using next() can already have received the first item. Python documents this deferred execution in its yield expression reference.

That last difference matters at the call site. With iterator = accepted_windows([1, -1], 10), one next(iterator) returns the first dictionary; the next call raises. With list(accepted_windows([1, -1], 10)), list consumes the generator until the error and never returns a list to assign, even though the first item was yielded internally. Ask ChatGPT which operation the caller actually performs before accepting a statement such as “the function returns one item.”

Run the four generator predictions

Save the function above in explain_trace.py, append the following tests, then run python3 -m unittest explain_trace.py -v. The final line should say OK with four tests run. These checks cover the stop row, equality boundary, delayed exception, and the fact that creating a generator does not start it.

import unittest


class TraceChecks(unittest.TestCase):
    def test_budget_six_stops_before_four(self):
        self.assertEqual(list(accepted_windows([1, 2, 4, 8], 6)), [
            {"attempt": 1, "delay": 1, "elapsed": 1},
            {"attempt": 2, "delay": 2, "elapsed": 3},
        ])

    def test_budget_seven_accepts_equality(self):
        self.assertEqual(list(accepted_windows([1, 2, 4, 8], 7)), [
            {"attempt": 1, "delay": 1, "elapsed": 1},
            {"attempt": 2, "delay": 2, "elapsed": 3},
            {"attempt": 3, "delay": 4, "elapsed": 7},
        ])

    def test_negative_delay_raises_on_resume(self):
        iterator = accepted_windows([1, -1], 10)
        self.assertEqual(next(iterator),
                         {"attempt": 1, "delay": 1, "elapsed": 1})
        with self.assertRaisesRegex(ValueError, "^delay must be non-negative$"):
            next(iterator)

    def test_creation_does_not_validate(self):
        iterator = accepted_windows([-1], 10)
        with self.assertRaisesRegex(ValueError, "^delay must be non-negative$"):
            next(iterator)

assertRaisesRegex fails when the expected error is missing and checks its message. All four tests passed locally on September 27 under Python 3.13.2. They verify these inputs, not every iterable, number type, or possible caller.

When a return value hides a change to the input

For code that changes an object you pass in, ask for the caller's state after the call as well as the return value. In this separate local example, alias and record refer to the same dictionary. The function returns a count, but it also changes the list inside that dictionary.

def add_tag(record, tag):
    tags = record.setdefault("tags", [])
    if tag not in tags:
        tags.append(tag)
    return len(tags)

record = {"tags": ["queued"]}
alias = record
assert add_tag(record, "reviewed") == 2
assert alias["tags"] == ["queued", "reviewed"]
assert add_tag(record, "reviewed") == 2

empty_record = {}
assert add_tag(empty_record, "new") == 1
assert empty_record == {"tags": ["new"]}

Ask ChatGPT: “After add_tag(record, 'reviewed'), what does the function return, and what does alias['tags'] contain? What changes on the duplicate call and on an empty dictionary?” The first call appends a tag; the duplicate call does not; setdefault creates the list when it is missing. All three effects were checked locally under Python 3.13.2. A response that mentions only the returned integer has missed the part the caller may care about.

Trace the first call in order: setdefault finds the existing list, the membership check sees that "reviewed" is absent, append changes that list, and len returns 2. The duplicate call sees the tag and skips append. Because alias = record refers to the same dictionary, reading through alias shows the new list item; no copy was made. For empty_record, setdefault first inserts a new list under "tags". This is why an explanation should separate the returned value from changes a caller can observe later.

Use the same method on your own code

SituationWhat to give ChatGPTWhat to check yourself
One short functionThe function, one exact call, your question, and your skill levelBranch order, return value, one boundary input
Code calls another helperThe helper or its known input/output contractWhich conclusion changes if that helper behaves differently
File, database, or API dependencyAn approved sample response or a small stub, if availableWhether unknown external behavior is treated as a fact
Large codebaseOne entry point and the nearby files needed to follow the callThe call path and files that support the claim
Cannot run the codeKnown inputs, relevant tests, and missing environment detailsMark predictions as unverified until you can run or inspect them

Worked case: the missing helper changes the answer

Suppose a teammate sends only this snippet. The input is specific, but the helper eligible is absent. A confident answer of "ready" or "hold" would guess a rule the snippet does not provide.

def order_status(order):
    if order["cancelled"]:
        return "cancelled"
    if eligible(order):
        return "ready"
    return "hold"

order = {"cancelled": False, "paid": True, "stock": 0}
order_status(order)
Prompt when a dependency is missing
Trace order_status(order) using the code and input above. Stop at any
function whose definition I have not supplied. Show what happens if
eligible(order) returns True, if it returns False, and if eligible is
undefined at runtime. Do not infer the business rule from the field names.
Tell me exactly what code or observed result would settle the answer.

The first condition is false, so execution reaches eligible(order). If that name is undefined when the call runs, Python raises NameError there. If a definition exists elsewhere, a true result leads to "ready" and a false result leads to "hold". The fact that paid is true and stock is zero does not settle which result the helper returns: one possible implementation checks payment alone; another also requires stock above zero. These are alternative local examples, not claims about the missing teammate code. To finish the trace, ask for the real eligible definition or its observed return for this order, then rerun the exact call. The undefined and both example-helper paths were checked locally under Python 3.13.2.

If the code is too large to paste, start with one behavior rather than asking for an explanation of the whole repository. For a multi-file onboarding task, use a codebase tool that can read the relevant files and identify what it did not inspect. The question here stays the same: which statement supports each claimed result?

When ChatGPT's prediction disagrees with a run

Compare the exact code, input, and call first. Then find the first condition where the predicted path differs from the run. For example, if an explanation claimed shipping_fee(50, True) returned 3, the first wrong step would be skipping total >= 50. Ask ChatGPT to correct that step before rewriting the whole answer:

Correction prompt for a mismatch
Your explanation predicts [VALUE], but running [EXACT CALL] returned
[ACTUAL VALUE OR ERROR]. Here is the exact code I ran: [APPROVED CODE].
Trace the conditions in execution order. Identify the first step where your
prediction differs, correct the result, and say what remains uncertain.
Do not rewrite the function.

Keep the smallest checks that expose the branch, delayed work, or mutation you needed to understand. If the task turns into finding a defect, continue with the debugging workflow; if it turns into changing working code, use the refactoring workflow. A passing check confirms the behavior of those inputs. It cannot by itself tell you whether the business rule was intended.