skip to content

Set Operations and Set Algebra

The set API and its algebra: adding and removing safely, the four combining operations in operator and method form, containment tests. Interviewers use it where a set turns O(n*m) into O(n+m).

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

questions

4

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

level: juniorimportance: must knowfreq 58%

answer

  1. One of the two is loud
  2. Absent element: error or silence
  3. Both mutate in place, return None
  4. remove asserts presence, discard tolerates
  5. KeyError versus a quiet no-op

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.

solid answer

~40 s

`set.remove(x)` deletes `x` and raises `KeyError` if `x` was not a member; `set.discard(x)` deletes `x` and is a no-op if it was not there. Choosing between them is a statement about your invariants: `remove` asserts the element must have been present, `discard` says either state is fine. Both mutate the set in place and return `None`, so `s = s.discard(x)` silently rebinds `s` to `None` - a common beginner bug that also bites `add` and `update`. `set.pop()` is the third removal: it removes and returns an arbitrary element, and raises `KeyError` on an empty set. Membership and removal both go through hashing and `__eq__`, so removing an equal-but-distinct object removes the stored one.

code

python · 10 lines
python
s = {'A1', 'B2'}

s.discard('C3')          # absent: nothing happens
try:
    s.remove('C3')       # absent: raises
except KeyError as exc:
    print('remove raised', type(exc).__name__, exc)

print(s.add('C3'))       # in-place methods return None
print(sorted(s))

go deeper

for a junior

Recall the one-line rule: remove raises KeyError on a missing element, discard does not, and both return None. Be able to say which you would pick when the element may legitimately be absent.

for a middle

Explain the mechanics around it: add is idempotent, pop removes an arbitrary element and raises KeyError when empty, clear mutates in place where rebinding does not, and removal matches by hash and equality rather than by identity.

for a senior

Show the judgement. Treat the choice as an invariant you are documenting in code, and call out the anti-pattern of try/except KeyError with an empty handler, which is discard written the long way and hides a genuine failure.

for a principal

Own the convention across a codebase: whether missing-element removals are errors or no-ops is a house style worth stating once, because inconsistency here turns real data-integrity bugs into silent no-ops that only surface in downstream reports.

## The two removals A Python `set` is a mutable, unordered collection of hashable elements. It exposes two single-element removals that differ only in what happens when the element is not there: - `s.remove(x)` deletes `x`. If `x` is not a member, it raises `KeyError(x)`. - `s.discard(x)` deletes `x`. If `x` is not a member, it does nothing at all. Both return `None`. Neither has any other behavioural difference: when the element is present they do exactly the same work. ## Choosing between them is choosing an assertion The interesting part of this question is not the API, it is the judgement. `remove` is an assertion that the element was in the set; `discard` is a declaration that either state is acceptable. Reaching for `discard` everywhere because it never throws is the wrong instinct - it converts a broken invariant into a silent no-op, and the bug then surfaces somewhere far away. Reaching for `remove` and then wrapping it in a bare `try`/`except KeyError` that does nothing is `discard` written the long way, and reviewers will say so. A useful rule: if the surrounding code has just proved the element is present, or if its absence means an earlier step failed, use `remove` and let the `KeyError` carry the diagnosis. If you are clearing a token that may or may not still be registered, use `discard`. The guard `if x in s: s.remove(x)` is not wrong, but it is two hash lookups where `discard` does one, and it reads as noise. ## The mutators return None Every in-place set method - `add`, `discard`, `remove`, `update`, `intersection_update`, `difference_update`, `symmetric_difference_update`, `clear` - mutates the receiver and returns `None`. This is the same convention as `list.append` and `list.sort`. The classic beginner failure is ```python seen = set() seen = seen.add('A1') # seen is now None ``` The next `seen.add(...)` fails with `AttributeError: 'NoneType' object has no attribute 'add'`, and the traceback points at the second line, not the first. If you want a new object rather than a mutation, use the non-mutating forms: `s | {x}` builds a new set, `s - {x}` builds a new set without `x`. ## add is idempotent, and that is the point `s.add(x)` on an element already present is a no-op - no error, no duplicate. That idempotence is why a set is the natural structure for accumulating distinct things: you never have to ask whether you already have one. It is the mirror image of `discard`: adding what is there and removing what is not are both quiet. ## pop and clear `s.pop()` removes and returns some element - which one is unspecified, because a set has no order, and you must not rely on it being the smallest, the first inserted, or stable across runs. On an empty set it raises `KeyError`. It is the right tool for a worklist you drain when order does not matter: ```python work = {'A1', 'B2', 'C3'} while work: item = work.pop() ``` `s.clear()` empties the set in place, which is different from `s = set()`: `clear` is visible to every other name bound to the same set object, rebinding is not. ## Removal uses hashing and equality, not identity `s.remove(x)` does not look for the same object you inserted; it hashes `x`, finds the matching slot, and confirms with `==`. So an equal-but-distinct object removes the stored one, which is what you want for strings, tuples and numbers. Two consequences worth naming: `s.remove(1.0)` removes a stored `1`, because `1 == 1.0` and they hash equal; and mutating an element after it is in the set is undefined territory, which is why set elements must be hashable - a rule the frozenset and dict-key material covers in depth. ## Removing during iteration Mutating a set while iterating it raises `RuntimeError: Set changed size during iteration`. Iterate over a snapshot instead, or compute the survivors as a new set: ```python s -= {x for x in s if x.startswith('TMP-')} ``` ## What an interviewer is listening for The complete answer is three beats: the mechanical difference (`KeyError` versus silence), the design reading (`remove` asserts, `discard` tolerates), and the shared gotcha that both return `None` because they mutate in place.

  • What does set.pop() return, and how is it different from list.pop()?
    `set.pop()` removes and returns an arbitrary element - a set has no order, so which element you get is unspecified and must not be relied on. It takes no index argument and raises `KeyError` on an empty set. `list.pop()` defaults to the last element, accepts an index, and raises `IndexError` when empty. Use `set.pop()` only when you are draining the set and genuinely do not care about order.
  • Why does `seen = seen.add(x)` break the next line of code?
    `set.add` mutates the set in place and returns `None`, following the same convention as `list.append`. Assigning its result rebinds the name to `None`, so the element was in fact added but the name no longer points at the set. The next `seen.add(...)` fails with `AttributeError: 'NoneType' object has no attribute 'add'`, and the traceback blames the wrong line. Call `seen.add(x)` as a statement, or use `seen | {x}` when you want a new set.
  • What happens if you call set.discard while iterating over the same set?
    Changing a set's size during iteration raises `RuntimeError: Set changed size during iteration` - the iterator holds a position into the internal table and cannot survive a resize. Iterate over a snapshot (`for x in list(s):` or `for x in s.copy():`) if you must mutate as you go, or build the survivors as a new set with a comprehension and rebind, which is usually clearer and faster.

remove is checking an item off a list and shouting if it was never on it; discard is crossing it out if you happen to see it and moving on either way.

saying these in an interview costs you the question

  • Claims remove and discard differ when the element is present
  • Says discard returns True or False to report success
  • Uses discard everywhere to avoid errors, hiding broken invariants
  • Writes s = s.add(x) or s = s.discard(x)
  • Thinks set.pop() removes the last or the smallest element
  • Assumes removal compares by identity rather than hash and ==

context

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

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