promptdojo_

The shapes AI dumps on you — lists, dicts, and nesting — step 1 of 8

Every API response is the same two shapes nested

Every JSON blob that comes back from a Stripe call, every config file Cursor wrote you, every response from the OpenAI API, every row of data your front end fetches — all of it is some mix of the same two shapes wrapped around each other.

If you can read those two shapes, you can read 90% of the data you'll ever touch. If you can't, every API integration becomes a guessing game.

The two shapes

[10, 20, 30]                # list
{"name": "Maya", "score": 92}   # dict
  • List [a, b, c]ordered. Items live at numbered positions. You get them by index: tags[0] is the first item.
  • Dict {"key": value}labeled. Items live at named keys. You get them by name: user["name"] is the value at the "name" key.

That's the entire vocabulary. Every nested data structure you'll ever read is one of these two shapes containing more of either of these two shapes.

Indexing counts from zero — and -1 is the last item

pets = ["luna", "moose", "biscuit"]
#         0       1         2       <- positive indexes
#        -3      -2        -1       <- negative indexes

Two rules, memorized once, automatic forever:

  • Positive indexes count from the start, beginning at 0. The first item is pets[0]. The second item is pets[1] — this is the rule behind every off-by-one bug in AI-generated code, because lists count from zero and humans do not.
  • Negative indexes count from the end, beginning at -1. The last item is pets[-1]. Any time your prompt has the words "last", "latest", "most recent", the code Cursor writes has a [-1] in it — no need to know the list's length.

Reading past the end of a list crashes with IndexError: list index out of range. There's no "default" fallback — Python raises an exception.

The mental model: peel one layer at a time

When AI writes you code that reads response["data"]["users"][0]["email"], your eyes try to read it all at once and your brain panics. Don't. Read left to right, one bracket at a time:

  1. response — start with the whole thing (a dict).
  2. ["data"] — open the "data" key. What's inside? Probably another dict.
  3. ["users"] — open the "users" key. Now you have a list.
  4. [0] — first item of that list. Whatever those items were — probably dicts.
  5. ["email"] — open the "email" key on that one. Read the string.

Five steps, one layer each. The pattern never gets more complicated than this, no matter how nested the JSON gets. It just gets longer.

A worked example

The editor on the right has a typical user object. Read the structure top to bottom:

user = {                     # outer shape: dict
    "name": "Maya",          # name → a string
    "tags": ["pm", "early-adopter"],   # tags → a list of strings
    "score": 92,             # score → a number
}

Then two reads:

  • user["name"] — open the "name" key on user. You get "Maya".
  • user["tags"][0] — open the "tags" key on user (you get a list), then take index 0 (you get the first string in that list, "pm").

That ["tags"][0] move — into a dict, then into a list — is the shape you'll see in literally every codebase that talks to an API.

Where AI specifically gets this wrong

When Cursor writes code against an API response, it sometimes guesses at the shape. It'll assume response["users"] is a list when it's actually a dict, or vice versa, and the code crashes with a confusing error like TypeError: list indices must be integers or slices, not str.

When you see that error in AI-generated code, the fix is almost always: print the thing one level up, eyeball the actual shape, and adjust the brackets. We'll drill that later in this chapter.

Run the editor. Two prints, two paths into the same little structure.