Sets are hash tables for membership and algebra. Dict views bring part of that algebra to keys and hashable items already stored in a mapping.
Core answer
Use set for membership, deduplication, and overlap. Use frozenset when the set itself must be hashable. Use dict-key views for schema checks instead of materializing a copy just to subtract keys.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclass@dataclass(frozen=True, slots=True)class PayloadReview: unexpected: set[str] accepted: booldef review(payload: dict[str, object]) -> PayloadReview: allowed = {"order_id", "amount_cents", "currency"} unexpected = payload.keys() - allowed return PayloadReview(unexpected, not unexpected)print(review({"order_id": "ORD-7", "amount_cents": 4200, "debug": True}))Why this design exists
Set algebra names operations that would otherwise become nested loops and ad hoc flags. Dict views reuse the mapping's keyed nature so a payload schema check reads like a set operation instead of a throwaway conversion.
frozenset closes the mutability gap: it gives an immutable hashable set value for composite configuration keys or cached permission groups.
Mechanics and CPython internals
CPython sets use open-addressed hash-table machinery related to dict key lookup. Hash and equality determine membership. Sparse capacity keeps probe chains short, which raises memory cost compared with scanning a compact list. Dict-key views are dynamic views over their mapping; item views are set-like only when their (key, value) pairs are hashable.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclassfrom sys import getsizeof@dataclass(frozen=True, slots=True)class PermissionKey: tenant: str roles: frozenset[str]def build_key(tenant: str, roles: list[str]) -> PermissionKey: return PermissionKey(tenant, frozenset(roles))ids = list(range(1000))lookup = set(ids)print(build_key("TEN-1", ["read", "write"]))print(getsizeof(ids), getsizeof(lookup))print({"a": 1, "b": 2}.keys() & {"b", "c"})Complexity and tradeoffs
Set membership is average O(1) while list membership is worst-case O(n). Set algebra costs scale with operand sizes and implementation strategy, but it expresses intent directly. The tradeoff is memory, loss of duplicates, and the requirement that elements keep stable hash/equality behavior.
Idiomatic patterns and refactoring
Refactor nested membership checks into set operations when the operation is about overlap rather than per-item side effects.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclass@dataclass(frozen=True, slots=True)class RoleDecision: granted: set[str] denied: set[str]def decide(requested: list[str], allowed: set[str]) -> RoleDecision: requested_set = set(requested) return RoleDecision(requested_set & allowed, requested_set - allowed)decision = decide(["read", "delete", "read"], {"read", "write"})print(decision.granted)print(decision.denied)Common mistakes and edge cases
Do not mutate equality-relevant fields of an element after insertion. Do not choose a set when duplicate counts or stable positional order are the requirement. Do not assume a dict item view stays set-like when values become unhashable.
When to use / When NOT to use
Use sets for access checks, deduplication, overlap tests, and immutable permission bundles via frozenset.
Do not use sets as a general list replacement, and do not pay a hash table's memory cost for one short scan on a tiny collection.