Skip to main content
PProfileMatch

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

  1. Pick the container by the question you will ask
  2. Lists and dicts at the appointment desk
  3. Sets: overlap and uniqueness
  4. Comprehensions: describe the result, not the loop
  5. Ordering, copying and nesting
  6. Common mistakes and the errors they produce
  7. How this shows up in real projects and interviews

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

ContainerQuestion it answers wellOrdered?Duplicates?Mutable?
listWhat is the 3rd item? Loop through them in orderYesAllowedYes
tupleA fixed record, such as (name, minutes)YesAllowedNo
dictWhat goes with this key?Insertion order keptKeys uniqueYes
setIs this item present? What do these groups share?NoNot allowedYes

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
# 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.

waiting_list.py
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.

visit_report.py
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 35

Three 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

pitfalls.py
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, so ordered = items.sort() leaves you with None. Use sorted(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 and d.get(key, default) when it is normal.
  • Set operators &, |, -, ^ answer overlap questions in one line; wrap in sorted() 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.

Ride this lesson

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.