Python Lists, Dictionaries, Sets and Comprehensions
ProgrammingLesson 3 of 45 min read
What you'll learn
- Choose between list, tuple, dict and set for a given problem
- Add, remove, look up and loop over items without common pitfalls
- Write list, dict and set comprehensions and generator expressions
- Group and count data with dictionaries and compare groups with set operations
On this page
A clinic's receptionist needs three things in one morning: the ordered list of appointment slots, a way to see instantly who holds the 09:45 slot, and a check on which patients from the waiting list are also already booked. Those are three different shapes of data, and Python gives each its own built-in container. Picking the right one is most of the skill in this lesson; the syntax is the easy part.
Pick the container by the question you will ask
| Container | Question it answers well | Ordered? | Duplicates? | Mutable? |
|---|---|---|---|---|
| list | What is the 3rd item? Loop through them in order | Yes | Allowed | Yes |
| tuple | A fixed record, such as (name, minutes) | Yes | Allowed | No |
| dict | What goes with this key? | Insertion order kept | Keys unique | Yes |
| set | Is this item present? What do these groups share? | No | Not allowed | Yes |
Looking up a key in a dict or a member in a set takes roughly constant time, whereas x in some_list scans the list from the start. That difference is invisible with five items and very visible with five hundred thousand, which is why choosing the container is a performance decision as well as a readability one. You will go much deeper on this in data structures.
Lists and dicts at the appointment desk
# clinic.py - a morning's appointment book
slots = ["09:00", "09:15", "09:30", "09:45", "10:00"]
booked = {"09:15": "Rao", "09:45": "Lindqvist"}
print(slots[0], slots[-1], slots[1:3])
slots.append("10:15")
slots.insert(0, "08:45")
removed = slots.pop() # removes and returns the last item
print(removed, len(slots))
free = [s for s in slots if s not in booked]
print(free)
booked["10:00"] = "Okonkwo"
print(booked.get("09:30")) # None, no error
print(booked.get("09:30", "free"))
try:
booked["09:30"]
except KeyError as err:
print("KeyError:", err)
for time, patient in sorted(booked.items()):
print(time, "->", patient)Output
09:00 10:00 ['09:15', '09:30'] 10:15 6 ['08:45', '09:00', '09:30', '10:00'] None free KeyError: '09:30' 09:15 -> Rao 09:45 -> Lindqvist 10:00 -> Okonkwo
Slicing slots[1:3] returns a new list from index 1 up to but not including 3, and negative indexes count from the end. booked[key] raises KeyError when the key is missing, while booked.get(key, default) returns a default instead. Use brackets when a missing key is a bug you want to hear about and .get when absence is normal.
Sets: overlap and uniqueness
Sets shine when the question is about membership or overlap. The operators map directly to the Venn-diagram words: & for both, | for either, - for only in the left one and ^ for exactly one of the two.
morning = {"Rao", "Lindqvist", "Okonkwo", "Haddad"}
waiting_list = {"Haddad", "Silva", "Rao"}
print(sorted(morning & waiting_list)) # in both
print(sorted(waiting_list - morning)) # waiting only
print(sorted(morning | waiting_list))
print(sorted(morning ^ waiting_list))
# remove duplicates but keep first-seen order
calls = ["Rao", "Silva", "Rao", "Haddad", "Silva"]
print(list(dict.fromkeys(calls)))
print(type(set()), type({}))Output
['Haddad', 'Rao'] ['Silva'] ['Haddad', 'Lindqvist', 'Okonkwo', 'Rao', 'Silva'] ['Lindqvist', 'Okonkwo', 'Silva'] ['Rao', 'Silva', 'Haddad'] <class 'set'> <class 'dict'>
Comprehensions: describe the result, not the loop
A comprehension builds a new collection from an iterable in one expression: [expression for item in iterable if condition]. It is not a speed trick so much as a readability one. When a loop exists only to filter or transform, the comprehension states the intent in one line. When the body needs several statements, side effects or nested logic, write a normal loop; a three-level comprehension is harder to read than the loop it replaced.
visits = [
{"patient": "Rao", "doctor": "Dr Mehta", "minutes": 20},
{"patient": "Silva", "doctor": "Dr Park", "minutes": 35},
{"patient": "Haddad", "doctor": "Dr Mehta", "minutes": 15},
{"patient": "Lindqvist", "doctor": "Dr Park", "minutes": 10},
{"patient": "Okonkwo", "doctor": "Dr Mehta", "minutes": 45},
]
long_visits = [v["patient"] for v in visits if v["minutes"] >= 20]
print(long_visits)
by_doctor = {}
for v in visits:
by_doctor.setdefault(v["doctor"], []).append(v["patient"])
print(by_doctor)
minutes_per_doctor = {}
for v in visits:
minutes_per_doctor[v["doctor"]] = minutes_per_doctor.get(v["doctor"], 0) + v["minutes"]
print(minutes_per_doctor)
lookup = {v["patient"]: v["minutes"] for v in visits} # dict comprehension
doctors = {v["doctor"] for v in visits} # set comprehension
print(lookup["Silva"], sorted(doctors))
print(sum(v["minutes"] for v in visits)) # generator expression
print(max(visits, key=lambda v: v["minutes"])["patient"])
first, *rest = [v["patient"] for v in visits]
print(first, rest)
for name, mins in zip(["Rao", "Silva"], [20, 35]):
print(name, mins)Output
['Rao', 'Silva', 'Okonkwo']
{'Dr Mehta': ['Rao', 'Haddad', 'Okonkwo'], 'Dr Park': ['Silva', 'Lindqvist']}
{'Dr Mehta': 80, 'Dr Park': 45}
35 ['Dr Mehta', 'Dr Park']
125
Okonkwo
Rao ['Silva', 'Haddad', 'Lindqvist', 'Okonkwo']
Rao 20
Silva 35Three details are worth stealing. setdefault and get(key, 0) are the two classic ways to group and count with a plain dict (the standard library's defaultdict and Counter do the same job with less typing). A generator expression, written like a comprehension without the brackets, feeds sum one value at a time instead of building a list first. And starred unpacking, first, *rest = items, splits a sequence without index arithmetic.
Ordering, copying and nesting
Two habits save real debugging time. First, sorted(items, key=...) returns a new list and accepts a function that says what to sort by, so sorted(visits, key=lambda v: v["minutes"], reverse=True) puts the longest appointments first without touching the original. Second, remember that copying a container copies only one level. list(visits) gives you a new outer list, but the dicts inside are the same objects, so editing copy[0]["minutes"] also edits the original. When you truly need independent nested data, copy.deepcopy(visits) does it, at a cost, and in many cases it is cleaner to build new dicts with a comprehension instead of mutating shared ones.
Common mistakes and the errors they produce
rooms = [[]] * 3 # three labels on ONE inner list
rooms[0].append("Rao")
print(rooms)
rooms = [[] for _ in range(3)] # three separate lists
rooms[0].append("Rao")
print(rooms)
queue = ["a", "b", "c", "d"]
for item in queue:
if item in ("a", "b"):
queue.remove(item) # changing a list while looping over it
print(queue)
try:
{[1, 2]: "x"}
except TypeError as e:
print("TypeError:", e)
d = {"x": 1}
try:
for k in d:
d["y"] = 2
except RuntimeError as e:
print("RuntimeError:", e)
print(sorted([3, 1, 2]), [3, 1, 2].sort())Output
[['Rao'], ['Rao'], ['Rao']] [['Rao'], [], []] ['b', 'c', 'd'] TypeError: unhashable type: 'list' RuntimeError: dictionary changed size during iteration [1, 2, 3] None
- **
[[]] * 3.** Every row is the same list, so one append appears in all of them. Use a comprehension to create independent lists. - Removing items while iterating. The loop skips elements, which is why
'a'was removed but'b'survived above. Build a new list with a comprehension, or loop over a copy. - Mutable dict keys. Lists and sets are unhashable and raise
TypeError: unhashable type: 'list'. Use a tuple or a string as the key. - Adding keys during dict iteration. Raises
RuntimeError: dictionary changed size during iteration. Collect changes first and apply them after the loop. - Assigning the result of `.sort()`. It sorts in place and returns
None, soordered = items.sort()leaves you withNone. Usesorted(items)when you want a new list.
How this shows up in real projects and interviews
Real Python work is mostly reshaping collections: turning rows from a database into a dict keyed by ID, grouping events by user, de-duplicating records, finding what two systems disagree on. Those are dict, setdefault/defaultdict, set and comprehensions. In interviews, the two-sum family of problems and "find the duplicate" questions are really tests of whether you reach for a dict or set instead of a nested loop, and the follow-up is nearly always the time complexity. See arrays and strings for more practice with that thinking.
Try it yourself
Given orders = [("ana", "tea"), ("bo", "coffee"), ("ana", "coffee"), ("cy", "tea"), ("bo", "tea")] (customer, drink), build (1) a dict mapping each customer to a set of drinks they ordered, (2) the set of customers who ordered both tea and coffee, and (3) a dict counting how many times each drink was ordered. Print all three with sorted output.
Show a hint
For (1) use setdefault(name, set()).add(drink). For (2) intersect the set of tea customers with the set of coffee customers, each built with a set comprehension. For (3) use get(drink, 0) + 1.
Key takeaways
- Pick the container by the question: order and position (list), key lookup (dict), membership and overlap (set), fixed record (tuple).
- Use
d[key]when a missing key is a bug andd.get(key, default)when it is normal. - Set operators
&,|,-,^answer overlap questions in one line; wrap insorted()to print them. - Use a comprehension when a loop only filters or transforms; use a normal loop when the body is complex.
- Never change a list or dict's size while iterating over it, and never multiply a list of lists with
*.
Test yourself
A trick from this lesson's ride: Braces were claimed by dicts first; a set has to be spelled out as set() Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
What is the difference between a list and a tuple in Python?
Both are ordered sequences, but a list can be changed after creation and a tuple cannot. Use a tuple for a fixed record such as (latitude, longitude), and a list for a collection that grows or shrinks. Because tuples are immutable (and hashable when their contents are), they can be dictionary keys.
When should I use a set instead of a list in Python?
Use a set when you mostly ask whether something is present or want to remove duplicates or compare groups. Membership tests on a set are close to constant time, while on a list they scan every item. Use a list when order or duplicates matter.
Are list comprehensions faster than for loops?
They are usually somewhat faster for simple transforms because the looping happens inside the interpreter, but the main reason to use them is clarity. Measure with timeit if speed matters, and switch back to a normal loop when a comprehension becomes hard to read.