Python Basics: Comparing Membership with set and frozenset
Sets answer questions about membership without retaining duplicate entries. They work well for tags and required features. A frozenset adds an immutable form that can itself be a dictionary key.
These three standalone examples were tested on Linux with Python 3.12.14. They require no third-party packages. Assertions check the results; run without Python’s -O option.
Prepare a scratch directory
You need Bash, Python 3.12 or newer on your PATH, a text editor, and the mktemp, rm, and rmdir utilities. Keep using the same terminal and save each named source file as UTF-8.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Collect unique tags
Save as collect_unique_tags.py:
submitted = ["python", "linux", "python"]
tags = set(submitted)
tags.add("python")
tags.add("testing")
tags.discard("missing")
assert tags == {"linux", "python", "testing"}
assert len(set()) == 0
print("Unique tags:", sorted(tags))
print("Count:", len(tags))
print("Has linux:", "linux" in tags)Run it:
python3 collect_unique_tags.pyOutput:
Unique tags: ['linux', 'python', 'testing']
Count: 3
Has linux: TrueAdding an existing member changes nothing. discard also tolerates a missing member. Sort these strings for a repeatable display; a set does not preserve a meaningful sequence. Use set() for an empty set, because {} creates an empty dictionary.
2. Report missing and unexpected features
Save as compare_feature_sets.py:
required = {"export", "search"}
enabled = {"search", "preview"}
missing = required - enabled
unexpected = enabled - required
assert missing == {"export"}
assert unexpected == {"preview"}
assert required & enabled == {"search"}
print("Missing:", sorted(missing))
print("Unexpected:", sorted(unexpected))
print("Shared:", sorted(required & enabled))
print("All required enabled:", required <= enabled)
print("After enabling required:", sorted(enabled | required))
assert enabled == {"search", "preview"}Run it:
python3 compare_feature_sets.pyOutput:
Missing: ['export']
Unexpected: ['preview']
Shared: ['search']
All required enabled: False
After enabling required: ['export', 'preview', 'search']Difference is directional: left - right keeps members in the left set that are absent from the right set. Thus required - enabled finds missing features, while enabled - required finds unexpected ones. Use left ^ right for members exclusive to either side. Intersection finds shared members, the subset test checks coverage, and union builds the combined membership. These expressions leave enabled unchanged, as the last assertion verifies.
3. Use an unordered combination as a key
Save as cache_frozen_features.py:
selected = {"csv", "pdf"}
frozen = frozenset(selected)
descriptions = {frozen: "combined export"}
selected.add("html")
lookup = frozenset(["pdf", "csv", "csv"])
assert lookup == frozen
assert frozen == frozenset(["csv", "pdf"])
assert "html" not in frozen
print("Lookup:", descriptions[lookup])
print("Frozen features:", sorted(frozen))
print("Mutable selection:", sorted(selected))
combinations = {frozen, lookup, frozenset(["json"])}
assert len(combinations) == 2
print("Distinct combinations:", len(combinations))Run it:
python3 cache_frozen_features.pyOutput:
Lookup: combined export
Frozen features: ['csv', 'pdf']
Mutable selection: ['csv', 'html', 'pdf']
Distinct combinations: 2The key treats order and repeated labels as irrelevant. Converting selected captures its current membership, so adding html later does not alter frozen. frozenset is immutable and hashable; ordinary mutable sets cannot be dictionary keys.
When to use this
Use a list when order or repetition is meaningful. Both set types require hashable members, so a list cannot be a member. A frozenset can represent a group inside another set, as the final example demonstrates.
References
Clean up
In the same terminal, remove only these example files, then return to your original directory and remove the empty scratch directory:
rm -- collect_unique_tags.py compare_feature_sets.py cache_frozen_features.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.