Debugging Python Tracebacks With LLMs in October 2026: Paste the Full Traceback, Reproduce Before You Patch, and Fix the Cause Instead of Hiding the Error
Pasting an error into a chat window and asking "why is this broken?" is one of the most common ways people use LLMs today. It often works. It also often produces a confident fix for the wrong problem: a try/except that hides the error, a type cast that silences it, or a change to a line that was never the cause. This guide covers a workflow for debugging Python tracebacks with an LLM that keeps you in charge of the diagnosis and makes the model's suggestions easy to check.
Why tracebacks trip up LLMs
A Python traceback tells you where an exception surfaced, not necessarily where the bug lives. The last frame is where Python gave up; the mistake that set things up might be several frames earlier, in a different module, or in data that arrived long before the crash. A model that only sees the final line, such as KeyError: 'user_id', can only guess. It will usually guess the most common cause in its training data, which may have nothing to do with your code.
Models also have a strong pull toward making the error go away. "Use dict.get() with a default" ends the KeyError, but if the key is missing because an upstream API changed its response format, you have just traded a loud crash for silently wrong data. The workflow below is built to avoid that trade.
The famous 1947 logbook moth: the cause of a malfunction is often somewhere other than where the symptom shows up. Image: First Computer Bug, 1947.jpg by Courtesy of the Naval Surface Warfare Center, Dahlgren, VA., 1988., Public domain, via Wikimedia Commons.
Step 1: Paste the whole traceback, not the last line
Copy everything from Traceback (most recent call last): to the final exception line. If there is a chained exception, include both parts. Python prints "During handling of the above exception, another exception occurred" or "The above exception was the direct cause of the following exception" when one error leads to another, and the first exception is often the real story.
Before pasting, remove anything sensitive. Tracebacks can contain file paths with usernames, and exception messages sometimes include connection strings, tokens, or customer data. Replace them with obvious placeholders like <REDACTED_TOKEN> so the model knows something was there.
Step 2: Add the code for the frames that matter
A traceback shows one line per frame. The model needs the surrounding function to reason about it. For each frame that is in your own code (skip frames inside the standard library or third-party packages unless you suspect them), include the full function. A useful prompt looks like this:
I'm getting this error in Python 3.12 running a Flask 3.0 app.
Full traceback:
<paste traceback>
Code for the frames in my project:
<paste functions from app/routes.py and app/services/users.py>
What I expected: the /profile endpoint returns the user's profile JSON.
What happened: 500 error on about 1 in 20 requests, only in production.
What I've already ruled out: the database is reachable; the same user ID works locally.
Before suggesting a fix, list the two or three most likely root causes
and what evidence would confirm or rule out each one.
Three details in that prompt do most of the work:
- Versions. Python, framework, and key library versions change behavior. An error that means one thing in pandas 1.x can mean another in pandas 2.x.
- Expected versus actual. "It crashes" is less useful than "it crashes on 1 in 20 requests, only in production." Intermittent failures point toward data, timing, or environment, not toward a plain logic error.
- Hypotheses before fixes. Asking for ranked causes with evidence keeps the model from jumping straight to a patch, and gives you something you can check.
Step 3: Reproduce before you patch
Do not apply any fix until you can trigger the error on purpose. A reproduction turns a guess into a test. Ask the model to help you build the smallest one:
Write a minimal script that reproduces this KeyError using only the
get_user_profile function and a hand-written input dict. Do not mock
anything else. If you think a specific input shape causes the error,
show that input.
Then run it. If the script reproduces the error, you have confirmed one hypothesis and you have a ready-made regression test. If it does not, the model's top guess was wrong, and you have learned that cheaply. Go back to the list of causes and try the next one.
For errors that only happen in production, reproduction usually means capturing real input. Add temporary logging just before the failing line that records the shape of the data (key names, types, lengths) rather than the full values, so you do not write personal data into logs:
logger.warning(
"profile payload shape: keys=%s types=%s",
sorted(payload.keys()),
{k: type(v).__name__ for k, v in payload.items()},
)
Once you see a failing payload's shape, you can build a local reproduction from it.
Step 4: Ask for the cause, then the fix
When you know which hypothesis is right, ask the model to explain the cause in one or two sentences before it writes any code. If the explanation does not match what you observed, stop and correct it. A good explanation names the specific condition: "The upstream service omits user_id for guest sessions, and get_user_profile assumes every payload has it." A weak explanation is generic: "The key might not exist in the dictionary."
Then ask for the fix, with constraints:
- Fix the cause where it originates, not only where it crashes.
- Do not add a broad
except Exceptionor a bareexcept:. - If a value can legitimately be missing, handle that case explicitly and say what the code should do (return a 404, skip the record, log and continue).
- Keep the change as small as possible and show it as a diff.
Step 5: Watch for fixes that hide the error
Review every suggested patch for these patterns. Each one can be the right answer occasionally, but each one is also a common way to make a bug quieter instead of fixing it:
- Swallowing exceptions.
try: ... except Exception: passturns a crash into missing behavior that nobody notices. - Defaulting missing values.
data.get("price", 0)stops theKeyErrorbut may write a price of zero into your database. - Casting away type errors. Wrapping a value in
str()orint()to fix aTypeErrorcan mask the fact that the wrong object arrived in the first place. - Adding
Nonechecks everywhere. If a function should never receiveNone, the fix is to find out who passed it, not to guard every caller. - Pinning or upgrading packages without a reason. Version changes can fix real incompatibilities, but ask the model which changelog entry or behavior change it is relying on, then check the package's own release notes.
When you see one of these, ask directly: "Does this fix the cause or hide the symptom? What happens to the data in the case that used to crash?"
Step 6: Verify the fix with the reproduction
Run your reproduction script against the patched code. The original error should be gone, and the behavior in that case should be what you decided it should be, not just "no exception." Then turn the reproduction into a proper test so the bug cannot quietly return:
def test_profile_handles_guest_session_without_user_id():
payload = {"session_id": "abc", "is_guest": True}
result = get_user_profile(payload)
assert result is None # guests have no profile; caller returns 404
Run your full test suite as well. A fix in a shared helper can change behavior for other callers that depended on the old path.
Common traceback types and what to include
Different exceptions need different context. Including the right extra detail saves a round trip.
ImportError and ModuleNotFoundError
Include the output of python --version, which python (or where python on Windows), and pip show <package>. Many of these errors come from running a different interpreter or virtual environment than the one the package was installed into, which the model cannot see from the traceback alone.
AttributeError
Include the type of the object that lacked the attribute. Adding print(type(obj)) right before the failing line often answers the question by itself: you expected a dataclass and got a dict, or expected a response object and got None.
Errors deep inside a library
When the last frames are inside a third-party package, the bug is still usually in how your code called it. Include the frame where your code calls into the library and the exact arguments you passed. Ask the model to compare those arguments with the library's documented signature for your installed version, then check the documentation yourself.
Async and concurrency errors
For asyncio errors such as "coroutine was never awaited" or "Event loop is closed," say how the program starts (for example, asyncio.run(), a framework's event loop, or a Jupyter notebook) since the fix depends heavily on who owns the event loop.

A debugger shows real runtime values; paste those into the conversation instead of letting the model guess. Image: Gdb-7.3-debugging-session.png by Der Messer, CC BY-SA 3.0, via Wikimedia Commons.
Use the debugger alongside the model
An LLM reasons about code it can read; a debugger shows you the actual values at runtime. Use both. Python's built-in breakpoint() drops you into pdb at any line. Running a crashing script with python -m pdb script.py stops at the exception so you can inspect local variables in the failing frame. When you find a surprising value, paste it into the conversation: "At the crash, payload is {'session_id': 'abc', 'is_guest': True}." Concrete runtime values cut the model's guessing down sharply.
A short checklist
- Paste the full traceback, including chained exceptions, with secrets redacted.
- Include the code for your own frames and the versions you are running.
- State expected versus actual behavior and what you have ruled out.
- Ask for ranked hypotheses with evidence before any fix.
- Reproduce the error on purpose before changing code.
- Reject fixes that swallow, default, or cast the error away without a reason.
- Confirm the fix with the reproduction, then keep it as a test.
The model is a fast way to generate hypotheses and draft patches. The traceback, the reproduction, and the test are how you find out which of those are true. Keep that division of labor and LLM-assisted debugging becomes faster without becoming riskier.
Comments
Post a Comment