Sets support the classic mathematical operations, each with an operator and
an equivalent method: union (|), intersection (&),
difference (-), and symmetric difference (^, items in exactly one
set).
a = {1, 2, 3}
b = {2, 3, 4}
a | b # {1, 2, 3, 4} union — in either
a & b # {2, 3} intersection — in both
a - b # {1} difference — in a, not b
a ^ b # {1, 4} symmetric difference — in one, not both
a.union(b) # method form, accepts any iterable
a.intersection([2, 3]) # b can be a list here
The operator forms require both operands to be sets, while the method forms accept any iterable. Use them for fast "what's common / unique / missing" questions instead of nested loops.
A set is backed by a hash table, so x in s is average O(1) — it
hashes x and checks one bucket. A list has no such index, so x in lst is
O(n) — it scans elements one by one until it finds a match or reaches the
end.
big_list = list(range(1_000_000))
big_set = set(big_list)
999_999 in big_list # O(n) — scans up to a million items
999_999 in big_set # O(1) — single hash lookup
For repeated membership checks over a large collection, converting to a set first is a huge win. The trade-off is that sets are unordered and elements must be hashable. Rule of thumb: if you mostly ask "is X in here?", use a set, not a list.
Wrapping a list in set() removes duplicates instantly, since a set can't
hold repeated values. The catch is that a set is unordered, so this doesn't
preserve the original order.
nums = [3, 1, 2, 3, 1]
unique = list(set(nums)) # e.g. [1, 2, 3] — order NOT guaranteed
# order-preserving dedup (dict keys are unique AND ordered since 3.7):
ordered = list(dict.fromkeys(nums)) # [3, 1, 2]
Use set() when you only care about the distinct values; when order
matters, use dict.fromkeys(), which keeps first-seen order thanks to
guaranteed dict ordering. Both require the elements to be hashable.
add(x) inserts an element (a no-op if it's already present). To delete,
remove(x) raises KeyError if the element is missing, while
discard(x) removes it silently if present and does nothing otherwise.
s = {1, 2, 3}
s.add(2) # already there — no change
s.add(4) # {1, 2, 3, 4}
s.remove(4) # {1, 2, 3}
s.remove(99) # KeyError — not in set
s.discard(99) # no error, no change
s.pop() # removes and returns an arbitrary element
Choose discard when "remove if it's there" is the intent (no need to guard
with a membership check), and remove when a missing element is genuinely an
error you want surfaced. pop() removes an arbitrary element since sets are
unordered.
A frozenset is the immutable version of set — it supports all the
read operations (union, intersection, membership) but has no add/remove.
Because it's immutable, it's hashable, so it can be a dict key or an
element of another set.
fs = frozenset([1, 2, 3])
fs.add(4) # AttributeError — immutable
fs & {2, 3, 4} # frozenset({2, 3}) — set ops still work
{fs: "a group"} # usable as a dict key
{frozenset({1, 2}), frozenset({3, 4})} # a set OF sets
The classic use is a set of sets: regular sets are unhashable, so the inner ones must be frozensets. Also use a frozenset for a constant collection you want to guarantee can't be mutated. Reach for it whenever you need a set-like value that must be hashable.
A set comprehension builds a set in one expression with
{expr for item in iterable}, automatically deduplicating the results. It's
the set sibling of list and dict comprehensions, with curly braces and no
key:value pair.
squares = {n * n for n in range(-3, 4)}
# {0, 1, 4, 9} — note 9 appears once even though -3 and 3 both map to it
words = ["Hi", "hi", "HEY"]
lowered = {w.lower() for w in words} # {'hi', 'hey'}
It's ideal when you want unique transformed values in a single readable step.
Watch out that {} alone is an empty dict, not an empty set — use set()
for an empty set. Use a set comprehension when both transformation and
deduplication are the goal.
With set(), not {} — curly braces with nothing inside create an
empty dict. {} only becomes a set literal when it contains elements.
type({}) # <class 'dict'>
type(set()) # <class 'set'>
type({1, 2}) # <class 'set'>
Rule of thumb: remember {} is a dict; always use set() for an empty set.
Only hashable (effectively immutable) objects — numbers, strings, tuples
of hashables, frozensets. Lists, dicts, and sets are unhashable and raise
TypeError. That's also why sets can't contain other (mutable) sets, but
can contain frozensets.
{1, "a", (2, 3)} # fine
{[1, 2]} # TypeError: unhashable type: 'list'
{frozenset({1, 2})} # fine -> frozenset is hashable
Rule of thumb: only immutable/hashable values belong in a set; convert lists to tuples or sets to frozensets first.
Neither. Sets are unordered and don't support indexing or slicing —
s[0] raises TypeError. Iteration order is an implementation detail you
shouldn't rely on. If you need order, sort into a list (sorted(s)) or keep a
separate list.
s = {3, 1, 2}
s[0] # TypeError: 'set' object is not subscriptable
sorted(s) # [1, 2, 3] -> get a defined order
Rule of thumb: treat sets as bags for membership/uniqueness, not as ordered sequences.
Use <=/.issubset() and >=/.issuperset(); the strict </>
require proper (not equal) relationships. .isdisjoint() checks for
no common elements without building an intersection.
{1, 2} <= {1, 2, 3} # True (subset)
{1, 2} < {1, 2} # False (not proper)
{1, 2}.isdisjoint({3, 4}) # True
Rule of thumb: use the operator/method forms for readable set-relation checks;
isdisjoint is cheaper than a & b when you only need "do they overlap".
The operators (|, &, -, ^) return a new set; the augmented forms
(|=, &=, -=, ^=) and named methods (update, intersection_update,
…) mutate in place. Also, operators require both operands to be sets,
while the methods accept any iterable.
a = {1, 2}
a | [3] # TypeError: needs a set
a.union([3]) # {1, 2, 3} -> method takes any iterable
a |= {3} # in-place -> a == {1, 2, 3}
Rule of thumb: use methods when the other operand is an arbitrary iterable; use operators for set-to-set expressions.
a ^ b (or a.symmetric_difference(b)) returns elements in exactly one
of the two sets — everything except their intersection. It's the set
equivalent of "XOR" and is handy for finding what changed between two
collections.
old, new = {1, 2, 3}, {2, 3, 4}
old ^ new # {1, 4} -> removed 1, added 4
Rule of thumb: use ^ to find items that differ between two sets (added or
removed but not common).
Yes for lookups — both give O(1) average membership via hashing. The
difference is mutability: frozenset is immutable and hashable, so it can
be a dict key or a set element and is safe to share. set supports
add/remove but can't be hashed.
cache = {frozenset({1, 2}): "result"} # frozenset as dict key
{frozenset({1}), frozenset({2})} # set of frozensets
Rule of thumb: use set for mutable working data, frozenset when you need
an immutable, hashable set (keys, set elements, constants).
A plain set() loses order. Since dict preserves insertion order (3.7+), use
dict.fromkeys(seq) to dedupe while keeping first-seen order, then convert
to a list.
items = [3, 1, 3, 2, 1]
list(dict.fromkeys(items)) # [3, 1, 2] -> order preserved
list(set(items)) # order undefined
Rule of thumb: set() to dedupe when order doesn't matter; dict.fromkeys
when it does.
add, remove, and in are O(1) average (O(n) worst case with bad
hashes). Union/intersection/difference are roughly O(len of the smaller/
larger set). This is why sets beat lists for membership and dedup on large
data.
x in s -> O(1) avg
s.add(x) -> O(1) avg
a & b -> O(min(len(a), len(b)))
a | b -> O(len(a) + len(b))
Rule of thumb: prefer sets when you do many membership tests or set algebra; lists for ordered, indexable sequences.
s.copy() or set(s) make a shallow copy — a new set holding the same
element objects. Since set elements must be immutable/hashable, shallow is
usually all you need; there's no nested-mutation concern like with lists.
a = {1, 2, 3}
b = a.copy()
b.add(4) # a unchanged -> {1, 2, 3}
Rule of thumb: set(s)/s.copy() is enough for sets; deep copy is rarely
relevant because elements are immutable.
More Data Structures interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.