Skip to content

Sets & Frozensets Interview Questions & Answers

16 questions Updated 2026-06-18 Share:

Python interview questions on set operations, O(1) membership vs lists, deduplication, add/discard/remove, frozensets, and set comprehensions.

Read the in-depth guidePython Sets and Frozensets Explained — Set Operations, O(1) Membership, and Dedup(opens in new tab)
16 of 16

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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel