Nested Data
Beginner Data Structures

Python Nested Data Structures

1Nested Data: overview

A nested data structure is a list, dict or tuple that contains other lists or dicts, and you reach a value deep inside by writing one key or index for each level, from the outside in.

GoalBy the end you can read and change values inside lists of dicts, dicts of lists and grids, loop over records, filter, sort and total them, read optional fields safely, and fix KeyError, IndexError and TypeError caused by using the wrong depth.

Before this lesson: Python Lists, Python Dictionaries

2Nested Data: diagram

Python Nested Data Structuresorderkeysvalues"id"1042"customer"{"name": "Ben", "city": "Pune"}"items"[{"sku": "A1", "qty": 2}, {"sku": "B7", "qty": 1}]"paid"Trueorder["items"][1]["sku"] gives "B7": a key, then an index, then a key
One dictionary whose values are a number, another dictionary, a list of dictionaries and a boolean. To reach "B7" you open one layer per bracket: the key "items", the index 1, then the key "sku".

3Nested Data: explained step by step

Real data is rarely flat. An order has a customer, and the customer has a city. A student has a name and a list of scores. Python copes by letting a list or dictionary hold other lists and dictionaries. That is called nesting, and it stops looking scary once you read it one layer at a time.

Four shapes you will meet again and again

Python
students = [                                        # list of dicts: one record per item
    {"name": "Asha", "score": 82},
    {"name": "Ben", "score": 67},
]
scores = {"Asha": [82, 90], "Ben": [67]}            # dict of lists: several values per key
users = {"u1": {"name": "Asha", "city": "Pune"}}    # dict of dicts: look up a record by id
grid = [[1, 2, 3], [4, 5, 6]]                       # list of lists: rows and columns

print(students[1]["name"])
print(scores["Asha"][-1])
print(users["u1"]["city"])
print(grid[1][2])

  • A list of dicts stores records, such as spreadsheet rows or results from a web API. You loop over it.
  • A dict of lists groups many values under one label, like all the scores of one student.
  • A dict of dicts is a lookup table: jump straight to a record by its id.
  • A list of lists is a grid. The first index picks the row, the second the column.

Real programs mix these freely.

Read deep values one layer at a time

Read the path from the outside in. Every [...] peels off one layer: a key if that layer is a dict, an integer index if it is a list.

Python
order = {
    "id": 1042,
    "customer": {"name": "Ben", "city": "Pune"},
    "items": [{"sku": "A1", "qty": 2}, {"sku": "B7", "qty": 1}],
}
items = order["items"]           # a list
second = items[1]                # a dict
print(second["sku"])
print(order["items"][1]["sku"])  # the same path in one expression

When a path gets long, save a middle step in a variable (items above). The name tells the reader what you are holding. If you lose track, print the part you have reached, or its type(), to see which kind of layer comes next.

Read optional data safely

order["coupon"]["code"] raises a KeyError if either key is missing. For fields that are allowed to be missing, use .get() and let the middle steps default to an empty dict so the chain carries on:

Python
profiles = [
    {"name": "Asha", "address": {"city": "Pune"}},
    {"name": "Ben"},                          # no address key at all
    {"name": "Cara", "address": None},        # key exists, but the value is empty
]
for person in profiles:
    city = (person.get("address") or {}).get("city", "unknown")
    print(person["name"], city)

Writing person.get("address", {}) would handle Ben but not Cara: her key is there, so the default is never used and None.get(...) would crash. (person.get("address") or {}) replaces both a missing and an empty value with {}. Use this only for optional data: for required fields a plain [...] is better, because a KeyError shows that the data is broken.

Change nested values

Assigning uses the same path. Every step except the last finds a container, and the last step changes it:

Python
cart = {"owner": "Asha", "items": [], "totals": {"qty": 0}}
cart["items"].append({"name": "pen", "qty": 3})    # grow the inner list
cart["items"][0]["qty"] += 1                       # update a value two layers down
cart["coupon"] = {"code": "SAVE5", "percent": 5}   # add a whole new nested dict
print(cart["items"])
print(cart["coupon"]["code"])

When you build a dict of lists, the inner list may not exist yet. by_city.setdefault(city, []).append(name) returns the list that is already stored, or stores a new empty one first, then appends to it.

Loop through records

Loop over a list of dicts with for record in records: and then read record["field"]. Loop over a dict of lists with for name, marks in scores.items():, which gives each key together with its list. For deeper data, put a loop inside a loop: an outer loop over orders and an inner loop over order["items"].

Filter and sort records

A list comprehension with an if keeps the records you want. sorted() takes a key= function that says which field to sort by. lambda s: s["score"] is a tiny one-line function: it receives one record and returns the value to compare.

Python
students = [
    {"name": "Asha", "score": 82, "city": "Pune"},
    {"name": "Ben", "score": 67, "city": "Delhi"},
    {"name": "Cara", "score": 91, "city": "Pune"},
]
from_pune = [s for s in students if s["city"] == "Pune"]
ranked = sorted(students, key=lambda s: s["score"], reverse=True)
print([s["name"] for s in from_pune])
print([s["name"] for s in ranked])
print(students[0]["name"])

Both lines build new lists and leave students alone. The records inside are still the very same dicts, which the next lesson explains.

Total up by category

"How much per category?" is the counting pattern with amounts instead of ones:

Python
expenses = [
    {"category": "food", "amount": 12.5},
    {"category": "travel", "amount": 40},
    {"category": "food", "amount": 7.5},
]
totals = {}
for expense in expenses:
    key = expense["category"]
    totals[key] = totals.get(key, 0) + expense["amount"]
print(totals)

Grids and transposing

A grid is read as grid[row][column]. Row totals are easy, because each row is a list. Column totals need zip(*grid): the star spreads the rows out as separate arguments, so zip(*sales) means zip(row0, row1), which pairs up the first items, then the second items, and so on. Those pairs are the columns.

Python
sales = [
    [10, 20, 30],      # Monday
    [15, 25, 35],      # Tuesday
]
print([sum(row) for row in sales])
print([sum(col) for col in zip(*sales)])
print([list(col) for col in zip(*sales)])   # a transpose: rows become columns
blank = [[0] * 3 for _ in range(2)]         # build each row separately
print(blank)

Build an empty grid with a comprehension like the last line. [[0] * 3] * 2 would put the same row in the grid twice, so changing one row would change both. The next lesson shows why.

Reading the errors

Using the wrong depth always raises one of a few errors, and each tells you where to look:

  • KeyError: 'city': the dict at this layer has no such key. It may live one layer deeper.
  • TypeError: list indices must be integers or slices, not str: this layer is a list, but you used a key.
  • IndexError: list index out of range: the list is shorter than you thought.
  • TypeError: string indices must be integers, not 'str': you reached a string, often because looping over a dict gives its keys.

Print the object just before the failing bracket and the right path is usually obvious.

Quick reference: Nested Data

4Nested Data: syntax

data[key]                    # dict layer: use a key
data[index]                  # list layer: use an integer index
order["items"][0]["sku"]     # one bracket per layer, outside in
order.get("a", {}).get("b")  # safe chain when keys may be missing
(order.get("a") or {}).get("b", "n/a")   # also safe when the value is None
order["items"].append(item)  # change an inner list in place
table.setdefault(key, []).append(x)       # create the inner list if missing
[r for r in records if r["city"] == "Pune"]          # filter records
sorted(records, key=lambda r: r["score"])            # sort by one field
[list(col) for col in zip(*grid)]          # transpose a grid

5Nested Data: run the example

Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.

Interactive example
orders = [
    {"id": 1001, "customer": {"name": "Asha", "city": "Pune"},
     "items": [{"name": "pen", "price": 2.5, "qty": 4}, {"name": "ink", "price": 4.0, "qty": 2}]},
    {"id": 1002, "customer": {"name": "Ben", "city": "Delhi"},
     "items": [{"name": "lamp", "price": 30.0, "qty": 1}]},
]
for order in orders:
    total = 0
    for item in order["items"]:
        total += item["price"] * item["qty"]
    print(order["customer"]["name"], order["customer"]["city"], total)

orders[1]["items"].append({"name": "bulb", "price": 3.0, "qty": 2})
print(len(orders[1]["items"]), orders[1]["items"][-1]["name"])
Expected outputAsha Pune 18.0
Ben Delhi 30.0
2 bulb

7Nested Data: how the example works

Hover or tap a step to highlight its lines in the example above.

  1. Line 1-6orders is a list of dicts. Each order dict holds a number, a nested customer dict and an items list, and every item is another dict. That is three layers of nesting.
  2. Line 7The outer loop visits one order dict at a time. order is the whole record, not a number or a name.
  3. Line 8The running total for this order starts at 0. It is created inside the outer loop, so every order begins again from zero.
  4. Line 9-10The inner loop walks the list found at order["items"]. For each item dict, item["price"] * item["qty"] is the cost of that line, and it is added to total.
  5. Line 11order["customer"]["name"] opens the customer dict and then reads its name. Asha's total is 2.5 * 4 + 4.0 * 2 = 18.0, and Ben's single lamp is 30.0.
  6. Line 13orders[1] is Ben's order, ["items"] is its list, and .append() adds a new item dict to that inner list. The list changes in place, so the order now holds two items.
  7. Line 14len() counts the items in Ben's list, and [-1] reads the last one, the bulb that was just added.

8Nested Data: common mistakes

Using a key from the wrong layer

✗ Wrong

order = {"customer": {"name": "Ben", "city": "Pune"}}
print(order["city"])
KeyError: 'city'

✓ Right

order = {"customer": {"name": "Ben", "city": "Pune"}}
print(order["customer"]["city"])

city lives inside the customer dict, one layer deeper than the top-level dictionary. Add the missing step to the path, or use .get() when the data is optional.

Indexing a list with a key

✗ Wrong

order = {"items": [{"sku": "A1", "qty": 2}]}
print(order["items"]["sku"])
TypeError: list indices must be integers or slices, not str

✓ Right

order = {"items": [{"sku": "A1", "qty": 2}]}
print(order["items"][0]["sku"])

order["items"] is a list of dicts, so the next step needs an index to choose one item (or a loop). The key "sku" belongs to the dict inside it.

Chaining .get() when the value is None

✗ Wrong

person = {"name": "Cara", "address": None}
print(person.get("address", {}).get("city", "unknown"))
AttributeError: 'NoneType' object has no attribute 'get'

✓ Right

person = {"name": "Cara", "address": None}
print((person.get("address") or {}).get("city", "unknown"))

The default {} is only used when the key is missing. Here the key exists and holds None, so the second .get() runs on None. Use or {} to cover both cases.

Looping over a dict of records and getting keys

✗ Wrong

users = {"u1": {"name": "Asha"}, "u2": {"name": "Ben"}}
for user in users:
    print(user["name"])
TypeError: string indices must be integers, not 'str'

✓ Right

users = {"u1": {"name": "Asha"}, "u2": {"name": "Ben"}}
for user in users.values():
    print(user["name"])

A for loop over a dict gives its keys, so user is the string "u1". Loop over users.values() to get the record dicts, or users.items() to get the id and the record.

Reading a position that is not in the list

✗ Wrong

orders = [{"id": 1}, {"id": 2}]
print(orders[2]["id"])
IndexError: list index out of range

✓ Right

orders = [{"id": 1}, {"id": 2}]
print(orders[-1]["id"])

Two orders have indexes 0 and 1, so index 2 does not exist. Use [-1] for the last order, or check len(orders) before indexing.

9Nested Data: exercises

Write your answer, then press Check. The checks run in your browser.

1. Dig into an order

Write code Easy

The dict order is given. Create three variables:

  • city: the customer's city
  • last_name: the "name" of the last item in order["items"] (it should still work if more items are added)
  • total_qty: the sum of the "qty" of all items

Then print city.

Your code
order = {
    "id": 1042,
    "customer": {"name": "Ben", "city": "Pune"},
    "items": [
        {"name": "pen", "qty": 4},
        {"name": "notebook", "qty": 2},
        {"name": "stapler", "qty": 1},
    ],
}

# your code here

2. Predict the grid

Predict the output Medium

Read the code and type exactly what it prints. Pay attention to when cols is created and what zip(*grid) produces.

Read this code
grid = [[1, 2, 3], [4, 5, 6]]
cols = [list(col) for col in zip(*grid)]
print(cols)
print(grid[1][-1], cols[2][0])
grid[0][0] = 99
print(sum(row[0] for row in grid))
print(cols[0])

2 more exercises → 6 more examples

Finished this lesson?
Progress is saved in this browser — create a free account to keep it across devices.

Questions learners ask

How do I access a value inside a nested dictionary or list in Python?

Chain one bracket per layer, from the outside in: use a key for a dict layer and an integer index for a list layer, as in order["items"][0]["sku"]. If you are not sure what a layer is, print it or print type(it) before going deeper.

How do I avoid a KeyError when a nested key might be missing?

Use .get() with an empty dict as the default for the middle steps: data.get("user", {}).get("city", "unknown"). If the key can also hold None, write (data.get("user") or {}).get("city", "unknown"). Keep plain [] for fields that must exist, so a bad record fails loudly.

What is the difference between a list of dictionaries and a dictionary of lists?

A list of dictionaries stores one record per item, so it suits rows of data that you loop over and filter. A dictionary of lists stores one column or group per key, so it suits 'all the scores of each student'. Converting between them is a short loop or comprehension, so pick the shape that makes your next step easiest.

Further reading

10Next lesson

Up nextCopying Lists and DictsAssigning a list or dict to another name does not copy it, it only adds a second name for the same object; a shallow co…

Related:
Python ListsPython DictionariesCopying Lists and DictsWorking with JSON in Python

← Python Dictionary Methods · All Data Structures lessons · Practise in the IDE