Sets

A set is an unordered collection of unique, hashable elements, built for fast membership testing and classic set algebra.

A set is a mutable, unordered collection of unique elements. Internally backed by a hash table, sets give average O(1) membership testing and add/remove, and provide the mathematical set operations — union, intersection, difference, symmetric difference — as both operators and methods. Because a set has no fixed order or indexing, it trades positional access for speed and uniqueness.

Creating Sets

Sets are created with curly-brace literals or the set() constructor applied to an iterable. There is no empty-set literal: {} creates an empty dict, not an empty set.

nums = {1, 2, 3}
from_list = set([1, 2, 2, 3])    # {1, 2, 3} -- duplicates collapse
empty: set[int] = set()          # NOT {} -- that's an empty dict
letters = set("hello")           # {'h', 'e', 'l', 'o'}

Uniqueness and Hashability

A set silently discards duplicates: each distinct value appears at most once, with equality (==) and hash() together determining distinctness. Every element of a set must be hashable, so a set can hold ints, strings, tuples of hashables, and frozensets, but never lists, dicts, or other sets.

s = {1, 1, 2, 2, 3}
print(s)              # {1, 2, 3}

bad = {[1, 2], 3}      # TypeError: unhashable type: 'list'
ok = {(1, 2), 3}        # fine -- tuple of ints is hashable

Membership Testing

The in operator is the primary reason to reach for a set: membership testing is average O(1), versus O(n) for a list, because it uses the element's hash to jump directly to a bucket rather than scanning.

allowed = {"admin", "editor", "viewer"}
"editor" in allowed    # True -- O(1) average
"guest" in allowed      # False

Adding and Removing Elements

add() inserts a single element. remove() deletes an element and raises KeyError if it's absent; discard() does the same but is silent if the element isn't there. pop() removes and returns an arbitrary element (sets have no defined order, so 'arbitrary' really means undefined which one).

s = {1, 2, 3}
s.add(4)          # {1, 2, 3, 4}
s.discard(99)      # no error even though 99 isn't present
s.remove(2)         # {1, 3, 4}
s.remove(99)         # KeyError: 99
item = s.pop()        # removes & returns some element -- order is undefined
s.clear()               # {}

Union, Intersection, Difference

Sets support the core operations of set algebra, available as both operators and equivalent methods (the methods accept any iterable, not just another set).

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a | b   # union():        {1, 2, 3, 4, 5, 6}
a & b   # intersection():  {3, 4}
a - b   # difference():     {1, 2}  -- in a but not b
b - a   # difference():      {5, 6}  -- in b but not a

a.union(b)
a.intersection(b)
a.difference(b)

Symmetric Difference

The symmetric difference is the set of elements in exactly one of the two sets — everything except their overlap.

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a ^ b                       # {1, 2, 5, 6}
a.symmetric_difference(b)     # same result

Subset and Superset

<=/issubset() and >=/issuperset() test containment relationships between sets; < and > give the strict (proper) versions. isdisjoint() tests whether two sets share no elements.

small = {1, 2}
big = {1, 2, 3}

small <= big          # True -- subset
small < big             # True -- proper subset (small != big)
big >= small             # True -- superset
small.issubset(big)       # True
{1, 2}.isdisjoint({3, 4})   # True -- no common elements

In-Place Update Operators

|=, &=, -=, and ^= mutate a set in place with the corresponding operation, mirroring update(), intersection_update(), difference_update(), and symmetric_difference_update().

a = {1, 2, 3}
a |= {4, 5}     # a becomes {1, 2, 3, 4, 5} -- same object, mutated
a &= {2, 3, 4}   # a becomes {2, 3, 4}

Set Comprehensions

A set comprehension builds a set with the same {expr for item in iterable} syntax as a list comprehension, using curly braces; duplicates produced by the expression collapse automatically.

unique_lengths = {len(word) for word in ["cat", "dog", "bird", "ox"]}
print(unique_lengths)   # {2, 3, 4}

Edge Cases and Ordering

A set has no guaranteed order: iterating twice may (rarely) print elements in different orders, and you cannot index or slice a set. CPython's actual iteration order is an implementation detail driven by hash values and insertion history — never rely on it.

CPython implementation detail: set iteration order depends on the elements' hash values and the table's internal layout, and can even change between runs when hash randomization is enabled for strings; it is not a stable or portable ordering.

s = {3, 1, 2}
s[0]              # TypeError: 'set' object is not subscriptable
for x in s:         # order is not guaranteed by the language
    print(x)

Practical Usage

Sets excel at: removing duplicates from a collection, fast membership checks (permission lists, visited-node tracking), and computing overlaps between groups (e.g. common tags, shared followers).

visited: set[str] = set()
def visit(node: str) -> bool:
    if node in visited:
        return False        # already seen
    visited.add(node)
    return True

emails = ["a@x.com", "b@x.com", "a@x.com"]
unique_emails = set(emails)

Key Takeaways

Sets are unordered, mutable collections of unique, hashable elements.

Use set(), not {}, for an empty set — {} creates a dict.

Membership testing (in) and add/remove are average O(1), far faster than a list's O(n) scan.

Union (|), intersection (&), difference (-), and symmetric difference (^) implement standard set algebra.

issubset/issuperset/isdisjoint test containment relationships between sets.

Sets have no indexing and no reliable iteration order.

empty = {} # this is a dict, not a set!

Curly braces default to dict syntax; the only way to write an empty set literal is the set() constructor.

s.remove(missing_value) # raises KeyError s.discard(missing_value) # silent if absent

Use discard() when the element's presence isn't guaranteed and a missing element shouldn't be an error.

tags = {["python", "web"]} # TypeError tags = {("python", "web")} # tuple is hashable

Set elements must be hashable; lists aren't, but the equivalent tuple is.

Why does {} not create an empty set, and what should you write instead?

Curly braces are dict syntax by default, so {} is an empty dict; write set() to create an empty set.

Why is membership testing on a set faster than on a list?

A set is backed by a hash table: it computes the element's hash to jump nearly directly to the right bucket (O(1) average), while a list must scan elements sequentially with == (O(n)).

What's the difference between remove() and discard() on a set?

remove() raises KeyError if the element isn't present; discard() does nothing if the element is absent, making it the safer choice when presence isn't guaranteed.

Given a = {1,2,3} and b = {2,3,4}, what do a - b, b - a, and a ^ b each produce?

a - b is {1} (in a only), b - a is {4} (in b only), and a ^ b is {1, 4} (in exactly one of the two, i.e. the union of both one-sided differences).

Why can't a set contain a list as an element?

Set elements must be hashable so the set can compute a stable hash to place them; lists are mutable and therefore unhashable, since their hash would have to change if their contents changed.

Is set iteration order guaranteed by the Python language?

No. Unlike dicts (which guarantee insertion order since 3.7), sets have no ordering guarantee at all; any apparent order in CPython is an implementation detail tied to hashing, not something to rely on.

How would you find the elements common to three different sets?

Chain intersections, e.g. a & b & c, or use set.intersection(a, b, c) / a.intersection(b, c), all of which return only elements present in every one of the sets.

Write a function that returns True if two lists share at least one common element, using sets.

def share_element(a: list[int], b: list[int]) -> bool: return not set(a).isdisjoint(b) print(share_element([1, 2, 3], [4, 5, 3])) # True print(share_element([1, 2], [3, 4])) # False isdisjoint() returns True when there's no overlap; negating it answers 'do they share anything', and it's more efficient than converting both to sets and checking a nonempty intersection.

What does this print?

First it prints {1, 2, 3, 4} -- b is an alias of a, so adding to b changes a too. Then it prints {1, 2, 3, 4} again unchanged -- copy() made c an independent set, so adding to c doesn't affect a.

Write a function that removes all duplicate words from a sentence while it's fine to lose the original order, returning a sorted list.

def unique_words(sentence: str) -> list[str]: return sorted(set(sentence.lower().split())) print(unique_words("the cat sat on the mat the cat ran")) # ['cat', 'mat', 'on', 'ran', 'sat', 'the'] split() tokenizes the sentence, set() collapses duplicates, and sorted() imposes a deterministic alphabetical order on the otherwise-unordered set.

a = {1, 2, 3}
b = a
b.add(4)
print(a)
c = a.copy()
c.add(5)
print(a)