Python Sets

A set in Python is an unordered collection of unique, hashable elements. Sets are built on the same hash-table technology as dictionaries, which makes checking membership (x in my_set) and removing duplicates extremely fast compared to lists. They also give you direct access to classic mathematical set operations — union, intersection, difference, and symmetric difference — which makes them the right tool whenever you care about “which items are present” rather than “in what order and how many times.”

Overview / How Sets Work

Internally, a Python set is implemented as a hash table, exactly like a dictionary that only stores keys (no values). When you add an item to a set, Python computes hash(item) and uses that hash to decide which “bucket” the item belongs in. This is why every element you put into a set must be hashable — its hash value must stay constant for its lifetime. Immutable types like int, float, str, bool, and tuple (if it only contains hashable items) are hashable. Mutable types like list, dict, and set itself are not hashable, so they cannot be stored inside a set.

Because membership is determined by hashing rather than by scanning every element, checking item in my_set runs in average O(1) time, versus O(n) for a list. Adding an item that already exists is silently ignored — sets never contain duplicates, which makes them the standard tool for deduplication.

A crucial detail: sets are unordered. The position of an element inside the hash table depends on its hash value, not on the order you inserted it. Python also randomizes the hash of strings (and bytes) by default for security reasons (to prevent hash-flooding attacks), so the printed order of a set containing strings can differ between separate runs of the same program. Never rely on set iteration order — if you need a predictable order, convert to a sorted list with sorted(my_set).

Sets themselves are mutable (you can add and remove elements), but Python also provides an immutable sibling, frozenset, which is hashable and can therefore be used as a dictionary key or nested inside another set.

Syntax

There are three common ways to create a set:

Form Example Notes
Set literal {1, 2, 3} Curly braces with comma-separated items. Duplicates are removed automatically.
set() constructor set([1, 2, 2, 3]) Builds a set from any iterable (list, tuple, string, dict keys, etc.).
Set comprehension {x * 2 for x in range(5)} Same idea as a list comprehension, but produces a set.
Empty set set() Not {} — that creates an empty dict.

Common methods you will use constantly:

Method / Operator Purpose
s.add(x) Add a single element (no-op if already present).
s.update(iterable) Add every element from another iterable.
s.remove(x) Remove x; raises KeyError if not present.
s.discard(x) Remove x if present; does nothing otherwise (safe version of remove).
s.pop() Remove and return an arbitrary element.
s.clear() Remove all elements.
a | b or a.union(b) Elements in a or b (or both).
a & b or a.intersection(b) Elements in both a and b.
a - b or a.difference(b) Elements in a but not in b.
a ^ b or a.symmetric_difference(b) Elements in exactly one of a or b.
a.issubset(b) True if every element of a is in b.
a.issuperset(b) True if a contains every element of b.

Examples

Example 1: Creating a set and basic operations

fruits = {"apple", "banana", "cherry"}
fruits.add("orange")
fruits.add("apple")  # already present, ignored

print(sorted(fruits))
print(len(fruits))
print("banana" in fruits)
print("grape" in fruits)

Output:

['apple', 'banana', 'cherry', 'orange']
4
True
False

Adding "apple" a second time has no effect because sets silently discard duplicates. Membership tests (in) and len() work exactly as you’d expect. Notice we print sorted(fruits) rather than fruits directly — the actual iteration order of a string-based set is not guaranteed to match insertion order.

Example 2: Union, intersection, difference, and symmetric difference

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

print(sorted(a | b))  # union
print(sorted(a & b))  # intersection
print(sorted(a - b))  # difference: in a but not b
print(sorted(b - a))  # difference: in b but not a
print(sorted(a ^ b))  # symmetric difference

Output:

[1, 2, 3, 4, 5, 6, 7, 8]
[4, 5]
[1, 2, 3]
[6, 7, 8]
[1, 2, 3, 6, 7, 8]

These four operators map directly onto the mathematical operations of the same name. a | b merges everything together, a & b keeps only what’s shared, a - b keeps what’s exclusive to a, and a ^ b keeps everything that is not shared — it’s equivalent to (a | b) - (a & b).

Example 3: A realistic use case — deduplicating and comparing tags

article_1_tags = ["python", "coding", "tutorial", "python", "beginner"]
article_2_tags = ["python", "advanced", "tutorial", "tips"]

tags_1 = set(article_1_tags)
tags_2 = set(article_2_tags)

shared_tags = tags_1 & tags_2
unique_to_1 = tags_1 - tags_2

print(sorted(tags_1))
print(sorted(shared_tags))
print(sorted(unique_to_1))

squared_evens = {n ** 2 for n in range(10) if n % 2 == 0}
print(sorted(squared_evens))

Output:

['beginner', 'coding', 'python', 'tutorial']
['python', 'tutorial']
['beginner', 'coding']
[0, 4, 16, 36, 64]

Converting the tag lists to sets removes duplicates for free (notice "python" only appears once in tags_1 even though the source list had it twice). The intersection instantly tells you which tags two articles share, and the difference tells you what’s unique to one. The last line shows a set comprehension: {n ** 2 for n in range(10) if n % 2 == 0} builds a set of squares of the even numbers from 0 to 9, following the same pattern as a list comprehension but with curly braces.

Under the Hood

When Python evaluates a set literal or a call to set(), it allocates a hash table sized to comfortably hold the elements without too many collisions. For each element, it computes a hash and finds a slot; if a different element already hashes to a colliding slot, Python probes for the next available slot (open addressing). Because slot position depends on hash value and table size — and the table can be resized as more elements are added — the order you observe when iterating a set has no relationship to insertion order and can even change after the set grows.

This is also why an element must be immutable: if a mutable object (like a list) changed after being inserted, its hash would change, and the set would no longer be able to find it in the table — the internal structure would become corrupted. Python protects you from this by refusing to hash unhashable (mutable) types at all, raising TypeError immediately.

Set operations like union and intersection are implemented by iterating over the smaller set and checking membership in the larger one, which is why they run in roughly O(min(len(a), len(b))) average time rather than O(len(a) * len(b)) — far faster than the nested loops you’d need with lists.

Common Mistakes

Mistake 1: Using {} for an empty set

{} is the literal for an empty dictionary, not an empty set. This is one of the most common Python gotchas.

empty_dict = {}
empty_set = set()

print(type(empty_dict))
print(type(empty_set))

Output:

<class 'dict'>
<class 'set'>

Always use set() when you need an empty set. There is no literal shorthand for it, precisely because {} was already claimed by dictionaries.

Mistake 2: Putting an unhashable object inside a set

Trying to store a mutable object such as a list inside a set raises a TypeError at runtime, because lists cannot be hashed:

my_set = {1, 2, [3, 4]}  # TypeError: unhashable type: 'list'

The fix is to use an immutable substitute, such as a tuple, wherever you need a “list-like” value inside a set:

my_set = {1, 2, (3, 4)}
print(sorted(my_set, key=str))

Output:

[1, 2, (3, 4)]

Mistake 3: Assuming sets preserve insertion or sorted order

Because iteration order depends on hashing and internal table layout (and string hashing is randomized per process by default), code that assumes list(my_set) will come back in the order you inserted things — or in sorted order — is not reliable. If order matters, either keep a separate list, use a dict (which does preserve insertion order in modern Python) with dummy values, or explicitly call sorted() when you need a deterministic sequence.

Best Practices

  • Use a set whenever you need to test membership frequently or remove duplicates — it is dramatically faster than a list for both.
  • Always create an empty set with set(), never {}.
  • Prefer discard() over remove() when the element might not be present, to avoid an unwanted KeyError.
  • Use frozenset when you need an immutable, hashable set — for example, as a dictionary key or as an element of another set.
  • Reach for the operator forms (|, &, -, ^) when both operands are sets for readability; use the method forms (union(), intersection(), etc.) when you want to combine a set with any iterable, since the methods accept any iterable while the operators require both sides to be sets.
  • Never assume set iteration order is meaningful; call sorted() explicitly whenever a stable, predictable order is required (for display, tests, or serialization).
  • Use set comprehensions ({expr for x in iterable}) instead of building a list and converting it, when you know you only want unique results.

Practice Exercises

  • Exercise 1: Given words = ["cat", "dog", "cat", "bird", "dog", "fish"], write code that prints the number of distinct words and a sorted list of those words.
  • Exercise 2: You have two sets of student IDs enrolled in two different courses, course_a and course_b. Write code to print (as sorted lists): students enrolled in both courses, students enrolled in only course_a, and students enrolled in exactly one of the two courses.
  • Exercise 3: Write a set comprehension that produces the set of all remainders you get when dividing each number from 1 to 50 by 7 (i.e. n % 7 for each n). Print the result as a sorted list, and explain in a comment why the result has at most 7 elements no matter how large the input range is.

Summary

  • A set is an unordered collection of unique, hashable elements, backed by a hash table.
  • Create sets with {...}, set(iterable), or a set comprehension — but always use set() for an empty set, never {}.
  • Membership tests and deduplication are average O(1) and O(n) respectively, much faster than the list equivalents.
  • Elements must be hashable (immutable); mutable types like list or dict cannot be stored in a set and will raise TypeError.
  • Union (|), intersection (&), difference (-), and symmetric difference (^) give you full mathematical set operations.
  • Set iteration order is not guaranteed and can vary between runs — use sorted() when order matters.
  • frozenset is the immutable, hashable counterpart to set, useful as a dict key or a nested set element.