Sets
Sets
A set holds unique hashable values. The boring default is a set when the question is membership or overlap (which tables are free, which allergens collided), not when order or duplicates matter.
Mental model
{3, 11, 12} is a set. set() is the empty set — {} is an empty dict. Values must be hashable. A set itself is mutable and not hashable. frozenset is the immutable twin; it can be a dict key or a member of another set.
Operators:
| Op | Meaning |
|---|---|
a \| b |
union — in either |
a & b |
intersection — in both |
a - b |
difference — in a not b |
a ^ b |
symmetric difference — in one, not both |
x in a |
membership |
<= / < are subset. >= / > are superset. Methods (union, intersection, add, discard, remove) exist too. remove raises KeyError if missing; discard does not.
Iteration order is not a contract. Sort when you print.
Worked examples
Case 1: Unique tables on the shift
Save as unique_tables.py.
# unique_tables.py
def main():
landed = [12, 3, 12, 11, 3]
tables = set(landed)
print(sorted(tables))
print(12 in tables)
tables.add(4)
tables.discard(99)
print(sorted(tables))
if __name__ == "__main__":
main()Run:
uv run python unique_tables.pyOutput:
[3, 11, 12]
True
[3, 4, 11, 12]
set(landed) dropped duplicate 12 and 3. discard(99) is a no-op. remove(99) would raise.
Case 2: Operators for two shifts
# shift_overlap.py
def main():
lunch = {3, 11, 12}
dinner = {12, 14, 15}
print("either ", sorted(lunch | dinner))
print("both ", sorted(lunch & dinner))
print("lunch only", sorted(lunch - dinner))
print("one side", sorted(lunch ^ dinner))
print("lunch subset of either", lunch <= (lunch | dinner))
if __name__ == "__main__":
main()Run:
uv run python shift_overlap.pyOutput:
either [3, 11, 12, 14, 15]
both [12]
lunch only [3, 11]
one side [3, 11, 14, 15]
lunch subset of either True
Read & as “conflict” when both shifts claimed the same table. Read - as “only lunch.”
Case 3: Allergens as a set, frozenset as a key
# allergens.py
def main():
soup = frozenset({"dairy", "gluten"})
tea = frozenset()
cake = frozenset({"dairy", "nuts"})
by_item = {
soup: "soup",
tea: "tea",
cake: "cake",
}
guest = {"dairy"}
for tags, name in by_item.items():
hit = set(tags) & guest
if hit:
print(name, "hits", sorted(hit))
else:
print(name, "ok")
if __name__ == "__main__":
main()Run:
uv run python allergens.pyOutput:
soup hits ['dairy']
tea ok
cake hits ['dairy']
A plain set cannot be a dict key (TypeError: cannot use 'set' as a dict key). frozenset can. frozenset() is the empty tag set for tea. Converting with set(tags) lets you & against the mutable guest set.
The trap
Building a set of dicts (tickets), or expecting a set to remember insert order in your output.
# set_of_tickets.py
def main():
tickets = [{"id": 7}, {"id": 8}]
print(set(tickets))
if __name__ == "__main__":
main()Run:
uv run python set_of_tickets.pyOutput (the process exits non-zero):
Traceback (most recent call last):
File "set_of_tickets.py", line 8, in <module>
main()
File "set_of_tickets.py", line 4, in main
print(set(tickets))
TypeError: cannot use 'dict' as a set element (unhashable type: 'dict')
Sets hash their members. Dicts (and lists) do not hash. Store ids: set(t["id"] for t in tickets). If you need unique tickets as whole records, pick a key (id) or use a dict keyed by id.
{1, 2, 3} looks ordered in a lucky print. Do not write tests that == a printed order. sorted(the_set) is the boring print.
The boring rule
- Set: unique membership. List: sequence. Dict: named fields.
- Use
| & - ^andin. Sort for display. discardwhen missing is fine.removewhen missing is a bug.frozensetfor keys and for set-of-sets.- Empty set is
set(), never{}.
Try this
- In
unique_tables.py, calltables.remove(99)instead ofdiscardand read theKeyError. - Add a
brunchset and print tables that appear in all three shifts (lunch & dinner & brunch). - Change
allergens.pysoguest = {"nuts"}and confirm only cake hits.