Fundamentals

Read a Python Traceback in Five Minutes

Stop scrolling past red text. Read a Python traceback bottom-up, find the one line that is yours, follow chained errors, and turn crashes into readable output.

A traceback is the block of red-looking text Python prints when your script crashes. It looks like punishment, but it is closer to a receipt: it records exactly what broke, where, and which calls led there. By the end of this guide you will be able to take any traceback from an AI API script, name the real problem in under a minute, and point at the single line you need to change. Budget five minutes for the reading method and another ten to wire up the error handler in Step 5.

This guide belongs to Debugging Python AI Errors, the section that covers the specific failures you hit when a Python script talks to a hosted model. The other guides there fix named errors; this one teaches you to identify which error you are looking at in the first place.

Prerequisites

You need Python 3.10 or newer. Python 3.11 and later add the ^^^^ caret markers under the exact expression that failed, which makes tracebacks considerably easier to read, so upgrade if you can. Check what you have:

python --version

Work inside a virtual environment, an isolated folder of packages that belongs to one project. If that phrase is new, read Create a Python Virtual Environment for AI first, then come back. Install the two packages used in the examples:

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install "openai>=1.40" "python-dotenv>=1.0"

Put your API key in a .env file beside your script:

OPENAI_API_KEY=sk-your-real-key-here

Then add that file to .gitignore before you write another line, so the key never reaches a public repository:

echo ".env" >> .gitignore

Every code block below assumes those four things: Python 3.10+, an active virtual environment, the openai package, and a .env holding your key.

Step 1 — Read it from the bottom up

Python prints a traceback in chronological order: the first call your program made appears at the top, and the failure appears at the bottom. That is the opposite of the order you need. The useful information is at the end, so train yourself to jump straight there.

The last line of any traceback has two halves separated by a colon: the exception type on the left and the message on the right. The type is a Python class name such as AuthenticationError, KeyError or TypeError, and it tells you the category of failure. The message is written by whoever raised it and usually names the specific value that caused trouble. Read those two things, and only then start scrolling up.

The four parts of a Python traceback and the order to read them Four stacked blocks representing the traceback header, a frame from your own file, a frame from library code, and the final exception line, each paired on the right with the order in which you should read it. Traceback header most recent call last Read it 4th Just a banner Frame in your file app.py, line 17 Read it 2nd Usually your bug Frame in the SDK openai/_base_client Read it 3rd Rarely your bug Exception line AuthenticationError Read it 1st What went wrong
Python prints a traceback oldest call first, so the two blocks that matter — the exception line and your own frame — are the last two on screen.

Here is a complete traceback from a script that calls OpenAI with a key that the API rejected. Your file paths and the library line numbers will differ, but the shape is always this:

Traceback (most recent call last):
  File "/home/you/summariser/app.py", line 24, in <module>
    print(summarise(open("notes.txt").read()))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/you/summariser/app.py", line 17, in summarise
    response = client.chat.completions.create(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/you/summariser/.venv/lib/python3.12/site-packages/openai/_utils/_utils.py", line 275, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/you/summariser/.venv/lib/python3.12/site-packages/openai/resources/chat/completions/completions.py", line 914, in create
    return self._post(
           ^^^^^^^^^^^
  File "/home/you/summariser/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1266, in post
    return cast(ResponseT, self.request(cast_to, opts, stream=stream))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/you/summariser/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1054, in request
    raise self._make_status_error_from_response(err.response) from None
openai.AuthenticationError: Error code: 401 - Incorrect API key provided

Walk it the way the diagram says:

  1. Last line. openai.AuthenticationError: Error code: 401 - Incorrect API key provided. Type and message. The key you sent was refused. Nothing about your prompt, your model name or your internet connection is implicated.
  2. Deepest frame naming your file. The second block, app.py, line 17, in summarise. That is where your program touched the API. The fix lives in that function or in the value it was given.
  3. The library frames. Five frames inside site-packages/openai/. They describe how the SDK turned your call into an HTTP request and how it converted the server's 401 response into a Python exception. You did not write them and you will not edit them.
  4. The header. Traceback (most recent call last) is a fixed sentence Python prints every time. It carries no information about your specific failure.

Four lines read, problem identified: the key. The fix is covered in Fix the 401 Unauthorized Error in OpenAI Python.

Step 2 — Find the deepest frame that is yours

Each indented File "..." block is a frame: one function call that was still in progress when the crash happened. Python stacks them as it goes. Your script calls a function, that function calls the SDK, the SDK calls its HTTP layer, and that layer raises. Every one of those calls gets a frame.

That is why the very last frame is nearly always library code. It is the place the exception was raised, not the place the mistake was made. Someone else's carefully written code noticed that the thing you handed it was wrong and complained. So the frame you care about is the deepest one whose path is inside your project folder rather than inside site-packages/.

A quick way to spot the boundary: look for site-packages or lib/python3. in the path. Anything containing those strings is installed code. Anything else is yours.

Decision tree for choosing which traceback frame to investigate A decision tree starting from the question of whether the bottom frame sits in a file you wrote, branching into a your-code path and a library path, each leading to the next action to take. Bottom frame in a file you wrote? Yes No Your own code Fix that exact line Installed library Scroll up to yours Print the values used on that line Check what you passed into the call
Two questions get you to the line worth editing: is the deepest frame yours, and if not, what did you hand to the library just before it complained?

There is one honest exception to the rule. If the traceback contains no frame from your project at all, the failure happened somewhere your code was invoked indirectly — inside a background thread, a callback, or a framework that called you. In that case read the exception type and the library name, and search for that combination rather than hunting for your own line.

Step 3 — Follow the chain when there are two tracebacks

Sometimes Python prints two tracebacks separated by a sentence. The two sentences you will meet are:

  • During handling of the above exception, another exception occurred: — a second error happened while an except block was already running.
  • The above exception was the direct cause of the following exception: — you wrote raise NewError(...) from original, explicitly linking them.

In both cases Python prints the original failure first and the newer one last. This inverts the bottom-up habit you just learned, so slow down here: the bottom traceback is what stopped your program, but the top traceback is usually why.

This script produces a chain. The model is asked for JSON, wraps its answer in a Markdown code fence, json.loads refuses it, and the handler raises a friendlier error of its own:

import json
import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()  # .env is listed in .gitignore

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))


def extract_price(description: str) -> float:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "user", "content": f"Return JSON with a price key for: {description}"},
        ],
    )
    data = json.loads(response.choices[0].message.content)
    return float(data["price"])


def process(description: str) -> float:
    try:
        return extract_price(description)
    except Exception:
        raise RuntimeError(f"no price found in: {description[:40]}")


if __name__ == "__main__":
    print(process("A 2-litre stainless steel kettle, boxed, unused."))

When the model answers with a code fence instead of bare JSON, the output looks like this:

Traceback (most recent call last):
  File "/home/you/prices/app.py", line 21, in process
    return extract_price(description)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/you/prices/app.py", line 18, in extract_price
    data = json.loads(response.choices[0].message.content)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/you/prices/app.py", line 27, in <module>
    print(process("A 2-litre stainless steel kettle, boxed, unused."))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/you/prices/app.py", line 23, in process
    raise RuntimeError(f"no price found in: {description[:40]}")
RuntimeError: no price found in: A 2-litre stainless steel kettle,
How a chained traceback is assembled from two separate failures A left to right flow showing an original exception, a second exception raised inside the except block that handled it, and the combined output Python prints with the original failure first. 1. Real cause JSONDecodeError 2. Your except raises a new one 3. Python prints both, cause first During handling of the above exception
The joining sentence marks where your error handler took over; the traceback printed above it is the failure you actually need to fix.

The bottom traceback says RuntimeError: no price found. True but useless — you wrote that message. The top traceback says JSONDecodeError: Expecting value: line 1 column 1 (char 0), which is the real story: the model's reply did not start with a JSON value. Fix JSONDecodeError with AI API Responses in Python covers stripping code fences before parsing.

If you add from exc to your raise statement, Python swaps the joining sentence for the "direct cause" wording and keeps the same two blocks. Either way, read the top block first.

Step 4 — Reproduce it in the smallest script that still fails

Once you know the exception type and the guilty line, shrink the problem. A traceback from a 300-line automation script mixes your bug with everything else the script does. Copy the failing call into a new file with nothing else in it, and run that instead.

Keep only what the failing line needs: the imports, the client, and one hard-coded input. No loops, no file reading, no command-line arguments.

"""repro.py — the smallest script that still reproduces the failure."""
import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()  # .env is listed in .gitignore

key = os.getenv("OPENAI_API_KEY")
print("Key loaded:", bool(key), "| starts with:", (key or "")[:7])

client = OpenAI(api_key=key)

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Reply with the single word: ok"}],
)
print(repr(response.choices[0].message.content))

Run it with python repro.py. Two outcomes, both useful. If it fails the same way, you have a ten-line file to experiment on and you can change one thing at a time. If it succeeds, the problem is not the API call at all — it is something your larger script does to the input beforehand, and you now know where to look.

That print of the key is deliberate. Printing bool(key) and the first seven characters confirms load_dotenv() found your file without exposing the secret in your terminal history. A surprising share of AuthenticationError reports turn out to be None arriving where a key should be, usually because the script was run from a different folder than the one holding .env.

Step 5 — Make future failures readable

Reading tracebacks is a skill you should need less often over time. The way to need it less is to catch exceptions where you can do something sensible about them and print a one-line summary instead of a wall of frames.

Two attributes do almost all the work. type(e).__name__ gives the exception's class name as a plain string, and str(e) gives its message. Together they are the same information as the traceback's last line, in a form you can log, count, or branch on.

import os
import traceback
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()  # .env is listed in .gitignore

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))


def ask(prompt: str, show_frames: bool = False) -> str | None:
    """Return the model's reply, or None after printing a readable error."""
    try:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
        )
        return response.choices[0].message.content
    except Exception as e:
        print(f"FAILED [{type(e).__name__}] {e}")
        if show_frames:
            traceback.print_exc()
        return None


if __name__ == "__main__":
    for question in ["Name one primary colour.", "Name one prime number."]:
        answer = ask(question, show_frames=True)
        print(answer if answer is not None else "(skipped)")

Three things changed compared with letting the script crash. The loop finishes instead of stopping on the first bad item. Each failure prints one scannable line such as FAILED [RateLimitError] Error code: 429. And traceback.print_exc() is available behind a flag when you want the full frame list back, so you lose nothing.

Keep the except Exception as e form. A bare except: with no exception class also swallows KeyboardInterrupt, which means Ctrl+C stops working and you can no longer interrupt a stuck loop. When you are ready to record failures to a file rather than the terminal, Log and Monitor AI API Calls in Production shows the pattern.

Quick reference: exception type to next question

Exception typeWhat Python is telling youThe question to ask next
ModuleNotFoundErrorAn import found nothingIs this the interpreter pip installed into? See Fix ModuleNotFoundError: No Module Named openai
AuthenticationErrorThe server refused your keyDid load_dotenv() actually find .env?
APIConnectionError, SSLErrorThe request never reached the APICan this machine reach the internet, and is a proxy in the way?
AttributeError, IndexErrorYou read a field that was not thereWhat does print(response) show the reply actually contains?

Two of these have dedicated fixes here: Fix Connection and Timeout Errors with AI APIs and Fix SSL: CERTIFICATE_VERIFY_FAILED in Python.

Troubleshooting

SyntaxError: '(' was never closed points at a line that looks perfect. Python reports where it gave up, not where you slipped. An unclosed bracket, quote or brace on the line above swallows the next line into the same expression. Check the line immediately before the one named.

IndentationError: unexpected indent after pasting code. Mixed tabs and spaces, or a stray leading space, on the reported line. Select the block in your editor and re-indent it with spaces only; Choose a Code Editor for Python AI Work covers editors that show whitespace.

The traceback lists only library files and none of yours. Your code was called from inside a framework or a thread, so no frame of yours is on the stack. Search the exception type together with the library name, and add a try/except around the call that hands work to that library.

Nothing prints at all, and the script exits silently. Something is catching the exception and discarding it — usually a bare except: or an except Exception: pass. Replace pass with traceback.print_exc() and run again.

RecursionError: maximum recursion depth exceeded fills the screen. A function is calling itself without a stopping condition, so the same two or three frames repeat thousands of times. Scroll to the very bottom, note the repeating pair of function names, and add the exit condition.

When to use this vs. alternatives

  • Reading the traceback vs. a step debugger. Reading is faster for anything with a clear exception type and message, which covers nearly every API failure. Reach for pdb or your editor's debugger when the script produces a wrong answer rather than an exception, since there is no traceback to read.
  • Reading the traceback vs. pasting it into a chatbot. Pasting works, but only after you have redacted keys and file paths, and it teaches you nothing about your own project. Read the last line yourself first; ask for help on the part you genuinely cannot decode.
  • Printing errors vs. logging them. print is right while you are developing at the terminal. Once a script runs unattended on a schedule, switch to Python's logging module so failures get timestamps and land in a file you can review later.

Tracebacks stop being intimidating the moment you accept that only two of their lines are usually about you: the exception line at the bottom and the deepest frame carrying your own filename. Read those two, shrink the failure into a small script, and add a handler that prints type(e).__name__ so the next crash arrives already summarised. Back to Debugging Python AI Errors.

Frequently asked questions

Why do you read a Python traceback from the bottom up?

The bottom line names the exception type and the message, which tells you what actually went wrong. Everything above it is the route Python took to get there, printed oldest call first. Starting at the bottom gives you the answer in one line instead of five screens of file paths.

Which line of a traceback is my bug?

Usually the deepest line that names a file you wrote, not the very last file listed. The final frames almost always sit inside a library such as the openai package. Scan upward from the exception until you see your own filename, and start there.

What does During handling of the above exception, another exception occurred mean?

It means a second error was raised while Python was already running an except block. Python prints the original failure first, then that sentence, then the newer failure. The first traceback is normally the real cause; the second one is your error handler reporting it.

How do I print a Python error without stopping the script?

Wrap the risky call in try and except Exception as e, then print type(e).__name__ and str(e). That gives you the exception class and its message on one line while the rest of the program keeps running. Call traceback.print_exc() as well when you still want the full frame list.

Do I need a debugger to understand a traceback?

No. A traceback already contains the exception type, the message, the file, the line number and the source of every call in the chain. A step debugger helps when you need to inspect variable values mid-run, but most AI API failures are solved by reading four lines carefully.