Python Part I · Module 12 · Comprehensions
Set Comprehensions
A regression over four hundred runs produces four hundred simulator names, and there are three of them. The same is true of the severities in a log, the blocks touched by a change list, and the owners of a set of failing tests — the interesting answer is which distinct values appear, not how many times each one did. Chapter 7.1 gave you the collection for that; this chapter gives you the one-line way to build it. The syntax is the third and last bracket, and it comes with one piece of punctuation that genuinely does not make sense and simply has to be memorised.
Intermediate14 min readPythonPart IFundamentalsComprehensionsSets
Part I · Module 12 · Chapter 12.5 · Set Comprehensions
1. The Question a List Cannot Answer Cleanly
results = [
{"test": "axi_smoke", "sim": "vcs", "status": "PASS"},
{"test": "axi_burst", "sim": "vcs", "status": "FAIL"},
{"test": "reset_seq", "sim": "xrun", "status": "FAIL"},
{"test": "cdc_check", "sim": "vcs", "status": "PASS"},
{"test": "power_seq", "sim": "questa", "status": "FAIL"},
]
sims = [r["sim"] for r in results]
print(sims)
print(len(sims))['vcs', 'vcs', 'xrun', 'vcs', 'questa']
5Five entries for three simulators. Which simulators did this regression use? is answered in there somewhere, buried under repetition.
2. The Set Form
results = [
{"test": "axi_smoke", "sim": "vcs", "status": "PASS"},
{"test": "axi_burst", "sim": "vcs", "status": "FAIL"},
{"test": "reset_seq", "sim": "xrun", "status": "FAIL"},
{"test": "cdc_check", "sim": "vcs", "status": "PASS"},
{"test": "power_seq", "sim": "questa", "status": "FAIL"},
]
sims = {r["sim"] for r in results}
print(len(sims))
print(sorted(sims))3
['questa', 'vcs', 'xrun']Curly braces, no colon. Five results in, three distinct simulators out — Chapter 7.1's uniqueness, applied as the collection is built rather than afterwards.
The anatomy is Chapter 12.1's, unchanged: output, source, optional filter. Only the brackets differ.
results = [
{"test": "axi_smoke", "sim": "vcs", "status": "PASS"},
{"test": "axi_burst", "sim": "vcs", "status": "FAIL"},
{"test": "reset_seq", "sim": "xrun", "status": "FAIL"},
]
failing_sims = {r["sim"] for r in results if r["status"] == "FAIL"}
print(sorted(failing_sims))['vcs', 'xrun']3. The Canonical Use
The names of the tests that failed, with no duplicates even if a test appears more than once:
results = [
{"test": "axi_smoke", "status": "PASS"},
{"test": "axi_burst", "status": "FAIL"},
{"test": "reset_seq", "status": "FAIL"},
{"test": "axi_burst", "status": "FAIL"},
]
names = {r["test"] for r in results if r["status"] != "PASS"}
print(len(names))
print(sorted(names))2
['axi_burst', 'reset_seq']Four results, three failures, two distinct failing tests — axi_burst failed twice, perhaps at two seeds, and appears once.
This is where the set form earns its place over the list form. A list would have reported three, and three is the answer to a different question: how many runs failed, not how many tests are broken. Both are useful; they are not the same number, and the brackets are how you say which you meant.
4. Why No Example Here Prints the Set Itself
Every result above went through sorted() before printing, and that is deliberate.
Chapter 7.2 measured this: a three-string set iterated in all six possible orders across twelve fresh interpreters, and Chapter 10.3 Section 5 repeated the measurement with the same outcome. A set has no dependable order, so printing one directly produces output that differs between runs.
results = [{"sim": "vcs"}, {"sim": "xrun"}, {"sim": "questa"}]
sims = {r["sim"] for r in results}
print(len(sims))
print(sorted(sims))
print("vcs" in sims)3
['questa', 'vcs', 'xrun']
TrueThose three lines are all safe: a count, a sorted list, and a membership test. None depends on the order the set happens to store things in.
5. {} Is an Empty Dictionary
Here is the punctuation that has to be memorised:
print(type({}))
print(type({1, 2}))
print(type({"a": 1}))
print(type(set()))<class 'dict'>
<class 'set'>
<class 'dict'>
<class 'set'>{} is an empty dictionary, not an empty set. Dictionaries got the braces first and kept them; a set with nothing in it has no literal at all, and set() is the only way to write one.
Inside a comprehension the rule from Chapter 12.2 is what disambiguates — the colon:
results = [{"test": "a", "status": "PASS"}, {"test": "b", "status": "FAIL"}]
as_set = {r["test"] for r in results}
as_dict = {r["test"]: r["status"] for r in results}
print(type(as_set).__name__, sorted(as_set))
print(type(as_dict).__name__, as_dict)set ['a', 'b']
dict {'a': 'PASS', 'b': 'FAIL'}| Written | Type |
|---|---|
{} | dict (empty) |
set() | set (empty) |
{x for x in xs} | set |
{k: v for k in xs} | dict |
{1, 2, 3} | set |
A missing colon turns an intended dictionary into a set of its keys, and nothing raises — Chapter 12.2 Section 2 flagged it, and this is the other half of the same hazard.
6. Deduplication Happens After the Transform
The order of operations matters and is easy to get backwards. A set comprehension transforms each item first, then discards duplicates among the results — not among the inputs.
names = ["axi_smoke", "axi_burst", "ahb_smoke"]
prefixes = {n.split("_")[0] for n in names}
print("source items :", len(names))
print("distinct prefixes:", len(prefixes), sorted(prefixes))source items : 3
distinct prefixes: 2 ['ahb', 'axi']Three distinct names produced two distinct prefixes, because axi_smoke and axi_burst both become "axi". The inputs were all different; the outputs were not.
Taken further, the collapse can be total:
names = ["axi_smoke", "axi_burst", "ahb_smoke"]
lengths = {len(n) for n in names}
print("distinct lengths:", len(lengths), sorted(lengths))distinct lengths: 1 [9]Three names, one value — all three happen to be nine characters long.
That is the right behaviour and it is the point of the form: you asked for the distinct lengths, not the distinct names. But it means the result's size tells you about the outputs, not the inputs, and reading it as a count of source items is a mistake. When you want both numbers, take both:
names = ["axi_smoke", "axi_burst", "ahb_smoke"]
print("names :", len(names))
print("prefixes:", len({n.split("_")[0] for n in names}))names : 3
prefixes: 27. Set Elements Must Be Hashable
Chapter 7.1 established that a set can only hold hashable values, and a comprehension does not relax that:
rows = [["a"], ["b"]]
s = {row for row in rows}TypeError: unhashable type: 'list'Lists are mutable and therefore unhashable, so a set of lists is impossible however it is built. The comprehension reports it at the first item, naming the type.
This matters when deduplicating records rather than scalars. A list of dictionaries cannot go into a set either — but the field you want distinct values of almost always can:
results = [
{"test": "axi_smoke", "sim": "vcs"},
{"test": "axi_burst", "sim": "vcs"},
]
sims = {r["sim"] for r in results}
print(sorted(sims))['vcs']The output expression pulls out a string, and strings are hashable. If you need distinct whole records, a tuple of the fields that identify one is the usual answer — Chapter 6.1's fixed record, which is hashable.
8. Compared With What You Already Had
Three ways to get the distinct simulators, all correct:
results = [{"sim": "vcs"}, {"sim": "vcs"}, {"sim": "xrun"}]
a = {r["sim"] for r in results}
b = set([r["sim"] for r in results])
c = set()
for r in results:
c.add(r["sim"])
print(sorted(a), sorted(b), sorted(c))
print(a == b == c)['vcs', 'xrun'] ['vcs', 'xrun'] ['vcs', 'xrun']
True- The set comprehension builds the set directly, in one pass.
set([...])builds a full list first and then discards the duplicates — correct, one pass more than necessary, and it buries the intent at the end of the line.- The loop is Chapter 7.3's
.add()and is the right choice when the body needs to do anything else.
The comprehension wins on saying what it means. {...} announces distinct values at the first character; set([...]) announces it after the reader has already parsed a list comprehension.
9. Counting Distinct Values
The most common thing done with the result is len():
results = [
{"test": "a", "sim": "vcs"}, {"test": "b", "sim": "vcs"},
{"test": "c", "sim": "xrun"}, {"test": "d", "sim": "vcs"},
]
print("runs :", len(results))
print("simulators:", len({r["sim"] for r in results}))
print("tests :", len({r["test"] for r in results}))runs : 4
simulators: 2
tests : 4Three numbers that mean three different things, from the same four records. len({...}) is the idiomatic way to ask how many distinct — and the fact that it reads as one unit is the argument for the comprehension over the two-step build.
10. Predict, Run, Observe
nums = [1, 2, 2, 3, 3, 3]
# A
print("A:", len([n for n in nums]), len({n for n in nums}))
# B
print("B:", sorted({n * 2 for n in nums}))
# C
print("C:", type({}).__name__, type({1}).__name__, type(set()).__name__)
# D
words = ["clk", "rst", "clk"]
print("D:", sorted({w for w in words if len(w) == 3}))The verified output:
A: 6 3
B: [2, 4, 6]
C: dict set set
D: ['clk', 'rst']A — six items, three distinct. The whole chapter in one line.
B — the transformation happens first and duplicates collapse afterwards, so three values out, not six.
C — {} is a dict; {1} and set() are sets.
D — both words are three characters, and the duplicate clk appears once.
11. Exercises
Exercise 1 — distinct values. From a results list, build the set of distinct statuses and print its size and its sorted contents.
Exercise 2 — two numbers. For a list where one test appears twice, print how many runs failed and how many distinct tests failed. Say why they differ and which you would put in a summary.
Exercise 3 — the brackets. Without running, give the type of each: {}, set(), {1}, {"a": 1}, {x for x in [1]}, {x: 1 for x in [1]}. Then verify all six.
Exercise 4 — the missing colon. Write a dictionary comprehension, delete the colon and its value, and print the type and contents of both. Explain what you now have.
Exercise 5 — order. Build a set of four test names and print it directly, three times in three separate runs. Record what you see. Then print it sorted and say what changed.
Exercise 6 — filter and dedup. Build the set of distinct simulators used by the failing runs only. Say which clause did which job.
Exercise 7 — three ways. Produce the same set using a set comprehension, set() around a list comprehension, and a loop with .add(). Say which you would ship and why, in two sentences.
Exercise 8 — count distinct. Given a list of log lines each starting with a severity, print how many distinct severities appear, using one expression.
12. Summary
- A set comprehension uses curly braces with no colon and builds a set of distinct values.
- The anatomy is unchanged — output, source, optional filter. Only the brackets differ.
- Deduplication is the job it exists for, applied as the collection is built rather than afterwards.
- Distinct tests and failing runs are different numbers, and the brackets are how you say which you meant.
{}is an empty dictionary, not an empty set.set()is the only way to write an empty set.- The colon disambiguates a dict comprehension from a set comprehension, and omitting it raises nothing.
- A set has no dependable order — Chapter 7.2 measured all six possible orders in twelve runs — so sort before reporting one.
- Counting and membership are order-independent and safe without sorting.
- Deduplication happens after the transform, so the result's size describes the outputs, not the inputs.
- Set elements must be hashable, so a set of lists or dictionaries is a
TypeError— pull out a scalar field, or use a tuple. len({...})is the idiomatic "how many distinct", and reads as one unit.set([...])is correct but says what it means a clause too late; a loop with.add()is right when the body does more.
The next chapter puts one comprehension inside another, and finds the point where that stops being a good idea.
Where this fits
Part of the Python curriculum.
