Python Part I · Module 7 · Sets
Checking Membership and Looping Over a Set
A set exists to answer one question well: is this value present? That question is asked with an operator you have known since Module 3, so the syntax costs you nothing — what is new is understanding why this collection is the right place to ask it, and that answer is about how a set finds things rather than about any feature it has. The second half of the chapter walks through a set one element at a time, which is the only way in, since there are no positions to index. That walk comes with a condition attached, and rather than asking you to take it on trust this chapter measures it.
Foundation13 min readPythonPart IFundamentalsSetsMembership
Part I · Module 7 · Chapter 7.2 · Checking Membership and Looping Over a Set
1. The Question, and the Operator You Already Have
failed = {"burst", "reset"}
print("burst" in failed)
print("smoke" in failed)True
FalseThat is in from Chapter 3.6, unchanged. It produces a bool, so everything Module 3 said about combining booleans applies:
failed = {"burst", "reset"}
found = "reset" in failed
print(found, type(found))
print(found and (len(failed) == 2))True <class 'bool'>
Truenot in asks the opposite and reads better than negating:
failed = {"burst", "reset"}
print("smoke" not in failed)
print("burst" not in failed)True
FalseAnd the matching rules are the ones you already know — whole elements, case-sensitive:
failed = {"burst", "reset"}
print("bur" in failed)
print("BURST" in failed)False
False"bur" is not an element, and "BURST" is a different string from "burst". Neither of those is new; both have been true since Chapter 4.6.
2. Same Answer, Different Work
Put the two collections side by side. They agree, always:
failed_list = ["burst", "reset"]
failed_set = {"burst", "reset"}
print("burst" in failed_list)
print("burst" in failed_set)
print(("burst" in failed_list) == ("burst" in failed_set))True
True
TrueThe answers match. What differs is how each one arrives at the answer.
A list is a sequence, so in checks its elements one after another until it finds a match or runs out. To decide that something is absent, it has to look at every element — there is nowhere else the value could be hiding.
A set is organised around the values themselves. Rather than walking the collection, it computes where the value would have to be and looks there. That is what the hashability requirement from Chapter 7.1 was for: the element's value determines where it lives, so finding it does not require a search.
The better reason to reach for a set is usually not speed at all. It is that a set says what the data is: a group of distinct names where membership is the question. Chapter 7.6 turns that into a decision.
3. A Membership Table
The shape this is most used for — a fixed group of accepted values, checked against as things arrive:
supported = {".sv", ".v", ".vhd"}
print(".sv" in supported)
print(".py" in supported)
print(len(supported))True
False
3Three accepted extensions, and the check is one readable line per question. Compare the alternative from Chapter 4.10 — name.endswith((".sv", ".v", ".vhd")) — which answers a different question about how a string ends. This one asks whether an exact value is in a known group.
Every output in this section is a boolean or a count: deterministic, reproducible, and completely independent of any ordering.
4. Getting At the Elements
Chapter 7.1 showed that a set has no positions:
items = {"clk", "reset_n"}
print(items[0])TypeError: 'set' object is not subscriptableSo if you want to do something with each element, indexing is not available. Walking the collection is — and the syntax is the one Chapter 5.4 established, unchanged:
sigs = {"valid", "clk", "reset_n"}
for s in sorted(sigs):
print(s)clk
reset_n
validOne pass per element, the loop variable referring to one element at a time. Everything Chapter 5.4 said about the loop variable being a name, about the indented body, and about zero elements meaning zero passes is true here without modification.
But look carefully at what that loop was actually given. It is not looping over sigs — it is looping over sorted(sigs), and the next section is why.
5. The Order Is Not Yours
A set can be looped over directly:
failed = {"burst", "reset", "timeout"}
for name in failed:
print(name)This runs, and prints each of the three names on its own line. This chapter will not tell you what order they come out in, and not out of caution — because there is no answer to give.
Here is the measurement. That exact three-line program was run in twelve separate Python processes, and the output was compared:
| Runs | Distinct orders observed |
|---|---|
| 12 | 5 |
Five different orders, from identical code, on the same machine, with the same Python. Among them were burst, reset, timeout, reset, timeout, burst and timeout, reset, burst.
Nothing was random and nothing was broken. Python does not promise an order for a set, and in the absence of a promise the order falls out of internal details that are free to differ between runs.
The fix is the one Chapter 7.1 introduced, and it is one word:
failed = {"burst", "reset", "timeout"}
for name in sorted(failed):
print(name)burst
reset
timeoutThat output is reproducible. sorted() returns a new list in a defined order, the loop walks that list, and the set is untouched.
Here is that second case, where the result is deliberately independent of order:
sigs = {"valid", "clk", "reset_n"}
total = 0
for s in sigs:
total += len(s)
print(total)15Five plus three plus seven, in whatever order those additions happened. Addition does not care, so neither does the answer — 15 every time.
And when the question is just how many, no loop is needed at all:
sigs = {"valid", "clk", "reset_n"}
print(len(sigs))36. Membership Inside a Loop
Combining the two halves of the chapter gives a genuinely useful shape — checking a group of required things against what was actually seen:
required = {"clk", "reset_n"}
observed = {"clk", "reset_n", "valid"}
for r in sorted(required):
print(r, r in observed)clk True
reset_n TrueEach required signal, and whether it turned up. sorted(required) keeps the report in a stable order; r in observed is the per-item check.
Chapter 7.5 answers the whole question in a single line without a loop at all, which is usually what you want. Seeing the long version first is worth it, because it makes clear what that one line is actually doing.
An empty set simply runs the body zero times, exactly as an empty list did:
none_failed = set()
for x in none_failed:
print(x)
print("done")doneNothing failed, so nothing printed, and the script carried on. No special handling was needed — the same property Chapter 5.4 pointed out for lists.
7. Do Not Change a Set While Looping Over It
Chapter 5.4 gave this rule for lists, where breaking it silently skipped elements. On a set, Python catches you:
s = {"a", "b", "c"}
for x in s:
s.add(x + "!")RuntimeError: Set changed size during iterationA clear RuntimeError naming exactly what happened. That is a better outcome than the list version, which produced wrong results quietly — but the rule is the same and it is worth carrying as one habit rather than two:
Chapter 7.3 is about changing a set deliberately, outside any loop.
8. Predict, Run, Observe
Predict all seven before reading on. Every answer is a boolean, a count, or routed through sorted().
tags = {"ERROR", "WARN", "INFO"}
print("ERROR" in tags)
print("error" in tags)
print("ERR" in tags)
print("DEBUG" not in tags)
print(len(tags))
print(sorted(tags))
print(("ERROR" in tags) and ("DEBUG" in tags))The verified output:
True
False
False
True
3
['ERROR', 'INFO', 'WARN']
False"error" fails on case and "ERR" fails because membership compares whole elements — neither is new, both are worth re-confirming on a set.
sorted(tags) is alphabetical and reproducible, and it prints with square brackets because it is a list, not a set.
The last line is False: the first half is True, the second is False, and and needs both. Chapter 3.5's short-circuit rule applies unchanged.
Now the question this chapter is really testing. Given:
for t in tags:
print(t)What order will the three lines appear in? The correct answer is that the question has no answer. If you predicted alphabetical, you predicted sorted(). If you predicted insertion order, you predicted a list. A set promises neither.
9. Exercises
Exercise 1 — ask the membership questions. For supported = {".sv", ".v", ".vhd"}, write expressions answering: is .vhd supported, is .py not supported, how many extensions are supported, and what the supported list looks like in a stable order.
Exercise 2 — predict. Given names = {"clk", "reset_n", "valid"}, say True, False or the exact value for each, then verify:
(a) "clk" in names (b) "CLK" in names (c) "cl" in names (d) "ready" not in names (e) len(names) (f) sorted(names)
Exercise 3 — the order question. A colleague's script prints a set of failing tests and they report that the report "comes out in a different order sometimes". Explain in three sentences why, say whether it is a bug in their code, and give the one-word change that fixes it.
Exercise 4 — which loop. For each task, say whether you would loop over the set directly or over sorted(set), and why:
(a) print a list of failures for a human to read
(b) add up the lengths of every name
(c) write the names into a file another script will compare against a stored copy
(d) check each name against a second collection and count how many matched
Exercise 5 — list or set for this check. You must test ten thousand incoming names against a group of five thousand known-bad names, once per incoming name. Say which collection you would hold the known-bad names in and why, in two sentences, without quoting any timings.
Exercise 6 — the empty case. Explain in two sentences why a loop over an empty set needs no special handling, and what that is worth in a script that reports failures.
Exercise 7 — mutation during iteration. Predict what happens, run it, write down the exact error, and give a one-sentence rule you would tell a colleague:
s = {"a", "b"}
for x in s:
s.remove(x)Exercise 8 — reconstruct the long form. Chapter 7.5 will answer "are all required signals present?" in one line. Using only this chapter, write the loop that answers it for required = {"clk", "reset_n"} and observed = {"clk", "valid"}, printing each required name and whether it was observed.
10. Summary
inandnot inwork on a set exactly as on a list, produce abool, compare whole elements and are case-sensitive.- The answers are identical; the work is not. A list checks its elements one after another; a set computes where the value would be and looks there — which is what hashability is for.
- For a handful of elements the difference does not matter. It matters for large collections checked repeatedly. No timings, no notation: measure the real program if it ever decides anything.
- The better reason to use a set is meaning — a group of distinct values where membership is the question.
- A set has no positions, so a loop is the only way to reach every element. The syntax is Chapter 5.4's, unchanged.
- The order of that loop is not promised. The same three-element set iterated in twelve processes produced five different orders.
- Loop over
sorted(my_set)whenever the order of your output matters; loop over the set directly only when it genuinely does not. sorted()returns a list, which is why its output prints with square brackets.- An empty set runs the body zero times and needs no special handling.
- Do not add or remove elements while looping — on a set this raises
RuntimeError: Set changed size during iteration.
The next chapter changes a set on purpose.
Where this fits
Part of the Python curriculum.
