Addaly is in open beta. Things will change, and AI answers can be wrong — check anything that matters.

Python, From Zero, For AI

From your first line of code to your first API call.

Lesson 20 of 899 min

Nested data: dictionaries inside lists inside dictionaries

What real data looks like

Anything that arrives from an API, a config file or a database export has depth. Here is the shape of a chat model's reply, trimmed:

python
response = {
    "id": "chatcmpl-9f2",
    "model": "small-v1",
    "usage": {"prompt_tokens": 41, "completion_tokens": 88},
    "choices": [
        {"index": 0,
         "message": {"role": "assistant", "content": "Delhi is the capital."},
         "finish_reason": "stop"}
    ],
}

Getting the text out is one expression, read left to right:

python
text = response["choices"][0]["message"]["content"]

Take choices; it is a list; take the first item; it is a dictionary; take message; take content. Every navigation into nested data is that sentence. If you cannot say the sentence, you do not yet know the shape, and the next section is how to find out.

One model reply, five levels downresponsea dict — the whole JSON bodyresponse["choices"]a list — one entry per completion you asked for…[0]a dict — index, message, finish_reason…["message"]a dict — role and content…["content"]a str — the text you came forresponse["choices"][0]["message"]["content"] is that sentence read left to right. TypeError: listindices must be integers means you are one level too shallow and have forgotten a [0]; KeyError meansthe level is right and the name is wrong.
One model reply, five levels downresponsea dict — the whole JSON bodyresponse["choices"]a list — one entry per completion you asked for…[0]a dict — index, message, finish_reason…["message"]a dict — role and content…["content"]a str — the text you came forresponse["choices"][0]["message"]["content"] is thatsentence read left to right. TypeError: list indicesmust be integers means you are one level too shallowand have forgotten a [0]; KeyError means the levelis right and the name is wrong.

Look before you index

Do not guess. Print the structure:

python
print(type(response), list(response.keys()))
print(type(response["choices"]), len(response["choices"]))

For anything bigger, the standard library formats it readably:

python
import json
print(json.dumps(response, indent=2)[:2000])

json.dumps with an indent is better than pprint for API data because it shows you the thing as the server sent it, and truncating to the first 2,000 characters keeps a 10 MB response from filling the terminal.

The two error messages, and what each one means

TypeError: list indices must be integers or slices, not str

You used a string key on a list. You are one level too shallow — there is a list where you thought there was a dictionary, and you have forgotten a [0].

TypeError: string indices must be integers

You indexed into a string with a name. You are one level too deep — you already reached the text and kept going.

Those two messages between them account for most of the confusion in handling API responses, and each tells you exactly which direction you are wrong in.

Reading defensively, without hiding the problem

Chained get calls survive missing keys:

python
text = (response.get("choices") or [{}])[0].get("message", {}).get("content")

That is safe and nearly unreadable, and it converts a clear failure into a silent None. Prefer a small function that says what went wrong:

python
def extract_text(response):
    choices = response.get("choices")
    if not choices:
        raise ValueError(f"no choices in response: {list(response)}")
    return choices[0]["message"]["content"]

Now a change in the API gives you a message naming the keys that were there, which is the information you need at 2 a.m. The defensive one-liner gives you NoneType errors somewhere else entirely.

Walking a level

The common shapes are all loops over one level:

python
for choice in response["choices"]:
    print(choice["message"]["content"])

names = [u["name"] for u in payload["data"]["users"]]

by_id = {u["id"]: u for u in payload["data"]["users"]}

That last one — turning a list of records into a dictionary keyed by id — is worth remembering. It converts every later lookup from a scan into an instant one, and it is the same idea as the set lesson.

Depth you do not know in advance

For a tree of unknown depth, a function that calls itself:

python
def find_key(data, wanted):
    if isinstance(data, dict):
        for k, v in data.items():
            if k == wanted:
                yield v
            yield from find_key(v, wanted)
    elif isinstance(data, list):
        for item in data:
            yield from find_key(item, wanted)

list(find_key(response, "content"))     # ['Delhi is the capital.']

Eleven lines that will find every occurrence of a key anywhere in any JSON structure. Keep it; you will use it for exploring unfamiliar API responses more often than you expect.

Mutating nested data changes the shared thing

Everything from the copies lesson applies with more force here. config["limits"] handed to a function is the same dictionary the caller holds, and a function that adds a key to it has changed the caller's config. If a function must not modify what it is given, take a deepcopy at the top, or build and return a new structure — and say which one you did in the docstring.

Try this now

Save the response above to a file with json.dumps, read it back, and write a function returning (model, content, total_tokens) where the total is the sum of the two numbers in usage. Then delete the message key and confirm your function fails with a message you would be glad to see.

The one thing to keep

Navigating nested data is one sentence read left to right, and the two index TypeErrors tell you whether you are one level too shallow or one level too deep.

Before you move on

Code that has worked for months against an API starts raising `TypeError: list indices must be integers or slices, not str` on the line `data["results"]["items"]`. What does the message actually tell you?

Pick the one you would defend. Nobody sees your answer.

No ads. No data sale. No public scores on people. Ever.

© 2026 Addaly