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.
Before this lesson: Python Lists, Python Dictionaries
2Nested Data: diagram
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
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.
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 expressionWhen 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:
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:
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.
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:
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.
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
- Python Dictionary Cheat Sheet: Methods and Examples — A Python dictionary maps unique keys to values: read with `d[key]` or `d.get(key, default)`, add with `d[key]…
- Python List vs Dictionary: Differences and When to Use Each — A **list** stores values by position (`items[0]`), while a **dictionary** stores values by a meaningful key (…
- How to Flatten a List of Lists in Python — To flatten a list of lists one level, use `[x for row in nested for x in row]` or `list(itertools.chain.from_…
- Python Program to Transpose a Matrix — To transpose a matrix stored as a list of lists, use `[list(row) for row in zip(*matrix)]`: rows become colum…
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.
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.
- Line 1-6
ordersis a list of dicts. Each order dict holds a number, a nestedcustomerdict and anitemslist, and every item is another dict. That is three layers of nesting. - Line 7The outer loop visits one order dict at a time.
orderis the whole record, not a number or a name. - Line 8The running total for this order starts at 0. It is created inside the outer loop, so every order begins again from zero.
- 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 tototal. - Line 11
order["customer"]["name"]opens thecustomerdict and then reads itsname. Asha's total is 2.5 * 4 + 4.0 * 2 = 18.0, and Ben's single lamp is 30.0. - Line 13
orders[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. - Line 14
len()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"])✓ 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"])✓ 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"))✓ 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"])✓ 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"])✓ 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 ✓ CompletedThe dict order is given. Create three variables:
city: the customer's citylast_name: the"name"of the last item inorder["items"](it should still work if more items are added)total_qty: the sum of the"qty"of all items
Then print city.
order = {
"id": 1042,
"customer": {"name": "Ben", "city": "Pune"},
"items": [
{"name": "pen", "qty": 4},
{"name": "notebook", "qty": 2},
{"name": "stapler", "qty": 1},
],
}
# your code here
order["items"] or use sum() with a generator expression.order = {
"id": 1042,
"customer": {"name": "Ben", "city": "Pune"},
"items": [
{"name": "pen", "qty": 4},
{"name": "notebook", "qty": 2},
{"name": "stapler", "qty": 1},
],
}
city = order["customer"]["city"]
last_name = order["items"][-1]["name"]
total_qty = sum(item["qty"] for item in order["items"])
print(city)
2. Predict the grid
Predict the output Medium ✓ CompletedRead the code and type exactly what it prints. Pay attention to when cols is created and what zip(*grid) produces.
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])zip(*grid) pairs up the first items of each row, then the second items, and so on. cols is built once, before grid[0][0] changes, so it does not follow later edits.[[1, 4], [2, 5], [3, 6]]
6 3
103
[1, 4]
2 more exercises → 6 more examples
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