Lists: a row of numbered slots
One variable holds one thing. When you need to hold a pile of things, you use a list:
scores = [88, 92, 79]
names = ["Maya", "Ben"]
print(scores)
print(names)
Output:
[88, 92, 79]
['Maya', 'Ben']
Square brackets, items separated by commas. A list keeps its order, which is what makes numbered access possible.
You pull items out by index, and indexes start at 0:
scores = [88, 92, 79]
print(scores[0])
print(scores[2])
print(scores[-1])
print(len(scores))
Output:
88
79
79
3
Starting at 0 feels wrong for about a week and then becomes invisible. A useful way to think about it: the index is not "which item" but "how far from the start." The first item is zero steps from the start.
The consequence is that the last item's index is len - 1. Asking for scores[3] in a three-item list gives:
IndexError: list index out of range
Negative indexes count from the right, so scores[-1] is always the last item regardless of length. That's more readable than scores[len(scores) - 1].
Adding, changing, removing
scores = [88, 92, 79]
scores.append(100)
print(scores)
scores[0] = 90
print(scores)
scores.remove(79)
print(scores)
print(92 in scores)
Output:
[88, 92, 79, 100]
[90, 92, 79, 100]
[90, 92, 100]
True
append adds one item to the end. scores[0] = 90 replaces what's at that position. remove(79) deletes the first item equal to 79 — note it takes a value, not an index, which is a frequent source of confusion. in asks whether a value is present and gives back True or False.
One thing to note: these methods change the list in place and give back nothing. Writing scores = scores.append(1) sets scores to None and throws your data away. Call it on its own line.
Quick summaries
nums = [3, 1, 4, 1, 5]
print(sum(nums))
print(max(nums))
print(min(nums))
print(sorted(nums))
print(nums)
Output:
14
5
1
[1, 1, 3, 4, 5]
[3, 1, 4, 1, 5]
Look at the last two lines. sorted() hands back a new sorted list and leaves the original untouched. (nums.sort() is the other option — it rearranges the original and gives back nothing.) Knowing which one you're using saves you from "why didn't my list change" and its twin, "why did my list change."
Slicing: taking a stretch
nums = [0, 1, 2, 3, 4, 5]
print(nums[1:4])
print(nums[:3])
print(nums[3:])
Output:
[1, 2, 3]
[0, 1, 2]
[3, 4, 5]
The end index is excluded, exactly like range. nums[1:4] gives you positions 1, 2, and 3. Leave the left side blank to mean "from the beginning," leave the right side blank to mean "to the end."
The same syntax works on strings: "2026-03-01"[:7] gives "2026-03", which is a handy way to grab a year and month.
Dictionaries: look things up by name
Lists are right when you have many of the same kind of thing. They're painful when you're describing one thing with several attributes, because you'd have to remember that position 2 is the city. Nobody remembers that.
A dictionary labels each value instead of numbering it:
user = {
"name": "Maya",
"age": 18,
"city": "Portland",
}
print(user["name"])
user["age"] = 19
user["email"] = "maya@example.com"
print(user)
Output:
Maya
{'name': 'Maya', 'age': 19, 'city': 'Portland', 'email': 'maya@example.com'}
A dictionary is a set of key: value pairs. Keys are usually strings. Values can be anything, including other lists and dictionaries.
Assigning to a key that already exists replaces the value. Assigning to a key that doesn't exist creates it. Same syntax for both, which is convenient once you expect it.
Asking for a key that isn't there is an error:
user = {"name": "Maya"}
print(user["phone"])
KeyError: 'phone'
When a missing key is a normal possibility rather than a bug, use get:
user = {"name": "Maya"}
print(user.get("phone"))
print(user.get("phone", "not provided"))
Output:
None
not provided
get gives back None for a missing key, or a default you supply. That second form is the backbone of the grouping pattern coming up in a moment.
Looping over both
scores = [88, 92]
user = {"name": "Maya", "age": 18}
names = ["Maya", "Ben"]
for score in scores:
print(score)
for key, value in user.items():
print(f"{key}: {value}")
for i, name in enumerate(names):
print(f"#{i + 1} is {name}")
Output:
88
92
name: Maya
age: 18
#1 is Maya
#2 is Ben
.items() hands you the key and the value together each round. enumerate() hands you the position and the item together — use it when you need a counter alongside the value, instead of managing an index variable yourself.
Note the i + 1 in that last line. Python counts from 0; people count from 1. Converting at the moment of display, and only there, keeps the two conventions from tangling.
Lists of dictionaries: what real data looks like
students = [
{"name": "Maya", "score": 88},
{"name": "Ben", "score": 95},
]
for s in students:
print(f'{s["name"]} scored {s["score"]}')
Output:
Maya scored 88
Ben scored 95
Note the single quotes around the f-string so the double quotes inside the braces don't end it early.
This shape — a list of dictionaries — is what you get back from almost every web API and database query you'll ever touch. Reading it fluently is most of what it takes to read real code.
The grouping pattern
Counting things per category comes up constantly, and there's a standard way to do it:
bills = [
{"category": "Food", "amount": 25.5},
{"category": "Transit", "amount": 8.0},
{"category": "Food", "amount": 12.0},
]
totals = {}
for b in bills:
name = b["category"]
totals[name] = totals.get(name, 0) + b["amount"]
for name in sorted(totals):
print(f"{name}:{totals[name]}")
Output:
Food:37.5
Transit:8.0
The trick is totals.get(name, 0). The first time a category shows up it isn't in the dictionary yet, and get supplies 0 so the addition works. Without it you'd need an if name in totals check every round. Learn this line — you'll write it many times.
sorted(totals) walks the keys in alphabetical order, which makes the output stable instead of depending on insertion order.
Ten-second check
a = [1, 2, 3]
print(a[1:])
The answer is [2, 3] — start at index 1, run to the end. If you said [2], you were thinking of a[1:2], which stops before index 2.