Your First AI Project

Lesson 9 of 14

When it breaks

Everyone who writes code spends a good deal of time looking at errors. Experienced programmers are not people who cause fewer of them — they are people who read them quickly and without dread. That is a learnable skill and this lesson is the whole of it.

Read the bottom first

Python prints a traceback: the chain of calls that led to the failure. Beginners read from the top, get lost in unfamiliar file paths, and conclude something catastrophic has happened.

The last line is the error. The line above it is usually your code. Start there, and go up only if you need to.

Here is a real one:

text
Traceback (most recent call last):
  File "demo.py", line 3, in <module>
    print(r["due"])
          ~^^^^^^^
KeyError: 'due'

Bottom line: KeyError: 'due'. There is no key called due in that dictionary. Line above: it happened at print(r["due"]), on line 3. That is the complete diagnosis, and the fix — r.get("due", "UNKNOWN") — follows immediately.

Now a longer one, which is where people give up:

text
Traceback (most recent call last):
  File "d2.py", line 3, in <module>
    data = json.loads(text)
           ^^^^^^^^^^^^^^^^
  File ".../json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File ".../json/decoder.py", line 338, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File ".../json/decoder.py", line 356, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Five frames, four of them inside Python's own JSON library. Ignore every line whose path is not yours. The last line is the error; the first frame is your code. Everything between is the library's internal journey, and it is almost never where your problem is.

Diagnosis: JSON parsing failed at the very first character. From lesson 7 you already know what that means — a preamble in front of the JSON — and salvage fixes it.

The five errors you will actually hit

KeyError: 'x' — you asked a dictionary for a key it does not have. Almost always a model returning a different field set than you asked for. Use .get(key, default).

TypeError: 'NoneType' object is not iterable — you looped over something that was None. A function returned nothing, usually because it failed quietly. In this project it is typically salvage finding no JSON. Check for None before looping: for item in items or [].

IndexError: list index out of range — you asked for element 3 of a list with 2 things in it. Check the length before you index.

json.JSONDecodeError — covered in lesson 7. The column number tells you where.

AuthenticationError / RateLimitError — the API. Key, credit, or too many requests. Lesson 6's retry handles the last one.

Five errors covers the great majority of what this project will throw at you, and each has a one-line fix.

The debugging loop

Print the thing. Not a description of the thing — the thing itself, and its type.

python
print(type(items), repr(items)[:300])

repr shows the representation, which distinguishes the string "None" from the value None, and [:300] stops a giant document flooding your screen. That one line resolves more confusion than any other habit.

Cut the problem in half. If ten steps run and something is wrong at the end, check step five. Then check the middle of whichever half is broken. Three or four cuts finds anything.

Change one thing at a time. Changing three and having it work leaves you not knowing why, which means you cannot fix it when it breaks again.

Make it fail on purpose. Once you have a theory, prove it. Feed the input you think is the problem directly and confirm it fails. A fix applied to an unconfirmed diagnosis is a coincidence waiting to be discovered.

The bug that is not an error

The hardest problems produce no traceback at all. The code runs, output appears, and it is wrong.

In this project there are three, all of which you have already been given defences against.

Silent truncation. stop_reason == "max_tokens", from lesson 4. No error, output looks fine, the end is missing.

Empty results treated as success. salvage returns None, for item in items or [] does nothing, zero rows are added, and the run reports success. Count your rows.

The model quietly inventing. No error is possible here by definition. This is what the UNKNOWN rules in lesson 5 exist to expose, and it is why lesson 10 exists.

Using AI to debug, without losing the skill

Pasting an error into an assistant and getting an explanation is a legitimate and excellent use — reading errors is exactly the sort of thing it is reliably good at, because tracebacks are highly structured and extremely well represented in its training.

The version that keeps the learning: ask it to explain the error, not fix the code.

"Explain what this traceback is telling me and what would typically cause it. Do not rewrite my code."

Then you make the change. The difference between the two habits, compounded over a few months, is the difference between someone who can debug and someone who can only ask.

Do this today: break something on purpose — index past the end of a list, parse a broken string — and practise reading the traceback bottom-up. Doing this deliberately three times removes most of the fear permanently.

← Previous