skip to content

Sets and Frozensets

Unordered collections of unique hashable elements plus the algebra over them. Interviewers reach for sets on dedup and membership problems and expect the O(1) contains that a list cannot offer.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

12

How do you deduplicate a list in Python while preserving first-seen order?

level: juniorimportance: must knowfreq 60%

answer

  1. Two obvious ways to drop duplicates
  2. One of them throws the order away
  3. Reach for a mapping, not a set
  4. Keys only, values all None
  5. Insertion order guaranteed since 3.7

basics

~10 s

list(dict.fromkeys(items)) keeps the first occurrence of each element in its original position, because dict iteration order is guaranteed insertion order since Python 3.7. set(items) also removes duplicates but iterates in an arbitrary order.

solid answer

~40 s

Use `list(dict.fromkeys(items))`. `dict.fromkeys` builds a dict whose keys are the elements, so equal elements collapse into one key, and since Python 3.7 a dict's iteration order is a language guarantee: insertion order. What survives is therefore the **first** occurrence of each element, in the slot where it first appeared. `set(items)` collapses duplicates just as well but iterates in hash-table order, which is arbitrary and, for strings, differs between processes because of hash randomization. Both need hashable elements and both are O(n); the naive `if x not in result` list scan is O(n squared) and only defensible for a handful of items. If you want the result sorted, sort it explicitly afterwards instead of hoping a set comes out ordered.

code

pycon · 5 lines
pycon
>>> items = ["b", "a", "b", "c", "a"]
>>> list(dict.fromkeys(items))
['b', 'a', 'c']
>>> sorted(set(items))
['a', 'b', 'c']

go deeper

for a junior

Be ready to write the one-liner from memory and say what it returns for a short list. Know that a set removes duplicates but gives no order promise, and that both approaches need hashable elements.

for a middle

Explain the mechanics: fromkeys builds a dict of keys with None values, a key's position is fixed at first insertion, and dict order has been a language guarantee since 3.7. Contrast O(n) with the O(n squared) list-scan version.

for a senior

Show where the idiom stops: unhashable elements, deduplication by a derived field, and equal-but-not-identical elements where keeping the first one silently discards a different payload. Flag set-iteration order as a source of non-reproducible output.

for a principal

Own the reproducibility argument: any pipeline whose output ordering comes from set iteration is unstable across processes because of hash randomization, and that instability shows up as flaky tests and non-deterministic artefacts. Make ordered dedupe the house default.

## The two candidate tools Deduplicating a sequence means collapsing elements that are **equal**, and Python decides equality for hashed containers with the pair `__hash__` and `__eq__`. Both tools below therefore require hashable elements. What separates them is the order of what comes out. `set(items)` builds a hash set. Iterating a set walks its internal table, so the output order is a function of each element's hash value and of the table's resize history, not of the order you inserted. For small integers this is deceptive: `hash(n) == n` for small ints, so `set([3, 1, 2])` frequently iterates ascending and people conclude that sets sort. They do not. For strings, hash randomization is on by default (seeded per process, pinnable with the `PYTHONHASHSEED` environment variable), so the same program can print a different order on the next run. Any code whose output depends on set iteration order is a flaky test waiting to happen. `dict.fromkeys(items)` is a classmethod that builds a dict whose keys are the elements and whose values are all `None` (an optional second argument supplies a different fill value). Duplicate elements collapse into one key. Since Python 3.7 a dict's iteration order is a documented **language guarantee** of insertion order; CPython 3.6 already behaved that way, but only as an implementation detail nobody was allowed to rely on. So `list(dict.fromkeys(items))` is the canonical order-preserving dedupe, and it is one expression. ```python >>> list(dict.fromkeys(["b", "a", "b", "c"])) ['b', 'a', 'c'] ``` The `list(...)` wrapper matters: iterating a dict yields its keys, but a dict is not a list, so callers expecting a sequence want the conversion. ## Why the *first* occurrence wins A key's position in a dict is fixed the first time it is inserted. Re-assigning a key that already exists updates its value but does **not** move it to the end. `dict.fromkeys` re-assigns `None` over `None`, so every later duplicate is a no-op for both the value and the position. You keep the first element you saw, where you first saw it. That distinction only becomes visible when "equal" does not mean "identical". `1`, `True` and `1.0` are all equal and share a hash, so they collapse into a single key, and it is whichever one appeared first that is kept: ```python >>> dict.fromkeys([1.0, True]) {1.0: None} ``` The same applies to any class with a custom `__eq__` and `__hash__`: two objects that compare equal but carry different payloads will leave you holding the first one. If you need the last one instead, feed the reversed sequence and reverse the result, or key the survivors explicitly. ## Cost Both `set` and `dict.fromkeys` are a single pass with O(1) average hashing per element, so both are O(n) time and O(k) extra space for k unique elements. The idiom people reach for before they know either one is ```python out = [] for x in items: if x not in out: out.append(x) ``` which is O(n squared), because `in` on a list is a linear scan. It is fine for ten items and a genuine production hazard for a hundred thousand. `dict.fromkeys` does allocate a full dict rather than a set, so each unique element costs a table entry plus a value slot; that overhead is real but almost never the deciding factor next to correct ordering. ## Iterables and iterators Both accept any iterable, including a generator, and both consume it exactly once. `list(dict.fromkeys(line.strip() for line in stream))` is a complete deduplicating read of a stream. Because the whole result is materialized, memory scales with the number of **unique** elements, not with the input length, which is why this idiom survives inputs far larger than the output. ## Where the one-liner stops working Two cases break it. If elements are unhashable, such as `dict` or `list` instances, both tools raise `TypeError` and you need a hashable stand-in key. And if you want to deduplicate by a *derived* value, keeping the first record per user id rather than per whole record, `dict.fromkeys` cannot express it at all, because it keys on the entire element; that case needs an explicit `seen` set inside a loop. ## Interview framing Interviewers ask this to see whether you reach past the obvious `set()`. Saying "`set(items)`" is a correct answer to a question that was not asked. The strong answer states the ordering guarantee, names the version it became a guarantee, and mentions that the elements must be hashable either way.

  • Does list(dict.fromkeys(items)) keep the first or the last occurrence of a duplicate, and why?
    The first. A dict key's position is fixed on its first insertion; assigning to an existing key updates the value but never moves the key. `dict.fromkeys` writes `None` over `None` for every later duplicate, so both the value and the position are unchanged. To keep the last occurrence instead, deduplicate the reversed sequence and reverse the result.
  • Why does iterating a set of small integers often look sorted while a set of strings does not?
    Small integers hash to themselves, so they usually land in table slots in ascending order and iterate that way by accident. String hashing is randomized per process by default, so a set of strings iterates in an order that changes between runs. Neither is a guarantee, and code that relies on either is fragile.
  • What does list(dict.fromkeys([1, True, 1.0])) return?
    `[1]`. The three values compare equal and share a hash, so they collapse into one key, and the first one inserted is the one kept. This is the same reason a list containing both `0` and `False` deduplicates down to whichever came first, and it is a real source of surprise when a sequence mixes bools with numbers.

A set is a bag you tip out on the table: everything you put in is there, in no particular arrangement. A dict is a numbered ledger: each new entry gets the next line and never changes line again.

saying these in an interview costs you the question

  • Claims set() preserves the order of the input
  • Uses an if-not-in-list scan on large inputs
  • Thinks sorted(set(x)) restores the original order
  • Calls dict ordering a CPython-only implementation detail today
  • Forgets that elements must be hashable either way
  • Assumes duplicates keep the last occurrence

context

open as a page

Why can't a Python set contain a list, and how does frozenset help?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A set stores its members in a hash table, so every element must be hashable, and lists are mutable and deliberately unhashable. frozenset is an immutable, hashable set, so it can sit inside another set or serve as a dict key.

open as a page

What is the difference between set.discard(x) and set.remove(x) in Python?

level: juniorimportance: must knowfreq 58%

basics

~20 s

Both delete the element when it is present. When it is absent, set.remove(x) raises KeyError while set.discard(x) does nothing and returns None. Use remove when a missing element is a bug, discard when absence is expected.

open as a page

Why does set.union accept a list argument when the | operator does not?

level: middleimportance: must knowfreq 45%

basics

~20 s

The operator forms are defined only between set and frozenset and raise TypeError on anything else, so mixed types fail loudly. The method forms are documented to accept any iterable and convert it as they go, and union, intersection and difference also take several iterables at once.

open as a page

In Python, how do you deduplicate a list of records by one field, keeping the first occurrence?

level: middleimportance: should knowfreq 42%

basics

~20 s

Track the derived key yourself: iterate the records, compute the field, and append a record only when its key is not already in a seen set. dict.fromkeys cannot do this, because it keys on the whole element.

open as a page

How do you deduplicate a list of unhashable items such as dicts or lists in Python?

level: middleimportance: should knowfreq 34%

basics

~20 s

Map each item to a hashable stand-in key and deduplicate on that: a tuple for a list, tuple(sorted(d.items())) or a sorted JSON string for a dict. Keep a seen set of those keys and append the original on a miss.

open as a page

Which methods does frozenset omit that set provides, and why?

level: middleimportance: should knowfreq 40%

basics

~20 s

frozenset drops every in-place mutator: add, clear, pop, remove, discard, update and the three other update variants, plus the augmented operator hooks. Everything that only reads a set survives, and each returns a new frozenset instead of changing the receiver.

open as a page

Why use a frozenset rather than a sorted tuple as a cache key for tags?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Both are hashable keys, but they claim different identities. A frozenset ignores order and collapses duplicates, so it is right when the tags are a true set; a sorted tuple keeps duplicates and requires mutually comparable elements.

open as a page

Why does `sku in stock_list` dominate a pick-list builder's runtime, and what does a set change?

level: seniorimportance: should knowfreq 60%

basics

~20 s

Membership on a list is a linear scan that compares elements one by one, so checking many SKUs against a long list is quadratic overall. Membership on a set is a single hash lookup, constant time on average, turning the pass from O(n*m) into O(n+m) after one O(n) build.

open as a page

Why can `a <= b` and `b <= a` both be False for two Python sets?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

Comparison operators between sets mean subset and superset, not ordering by size or content. Two sets that merely overlap are incomparable, so every one of <, <=, > and >= is False in both directions. Sets form a partial order, not a total one.

open as a page

How do you bound a Python dedupe set that tracks seen keys in a long-running service?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

Give it an eviction policy: pair the set with a collections.deque of the same keys and a maxlen, discarding the key about to fall off before each append. A bounded window means far-apart duplicates get through.

open as a page

Can you rely on the iteration order of a Python set or frozenset?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

No. Sets and frozensets are unordered and not subscriptable; iteration follows hash-table slot order, which depends on element hashes and insertion history. For str elements the hash is salted per process, so order can differ between runs of one script.

open as a page