Sets: Beginner
Slack computes shared channel membership between any two users in milliseconds by taking the intersection of their respective channel sets, a calculation that runs in constant time regardless of how many channels either person belongs to. With hundreds of millions of messages processed daily across millions of workspaces, doing this with list lookups would be impossibly slow, requiring linear scans for every comparison. Sets make the operation instant because membership testing is O(1), not O(n). The add, remove, and membership operations you will learn in this lesson are the same building blocks Slack and every major collaboration platform rely on to serve membership data at scale.
What is a Set?
Distinguish sets from lists
Sets vs Lists Fundamentals
Lists preserve order and allow duplicates. When you add items to a list, they stay in the order you added them. You can access items by their position using indexing: list[0] gives you the first item, list[1] gives you the second, and so on. Lists allow the same value to appear multiple times. A list like [1, 1, 2, 2, 3] is perfectly valid and maintains all five elements.
- Ordered: [1, 2, 3] stays in that order
- Allows duplicates: [1, 1, 2, 2] is valid
- Access by index: list[0] returns first item
- Slower membership test: O(n) time complexity
- Preserves insertion order
- Unordered: order is not guaranteed
- No duplicates: {1, 2} only, no repeats
- No indexing: set[0] raises an error
- Fast membership test: O(1) time complexity
- No concept of order
The notation O(n) means that checking if an item is in a list takes time proportional to the list size. If the list has n items, you might need to check all n items in the worst case. A list with a million items requires up to a million comparisons. The notation O(1) means that checking if an item is in a set takes constant time regardless of set size. Whether the set has ten items or ten million items, the lookup takes approximately the same amount of time.
> A set automatically removes duplicates. Pick the built-in that counts unique elements, and the keyword that tests membership in constant time.
data = {3, 1, 4, 1, 5, 9, 2, 6, 5} print((data)) print(5 data)
The O(1) membership test is the defining advantage of sets. It comes from hashing: Python converts each element into a number that directly points to its storage location, so no scanning is needed. This makes sets the right tool for membership checks in any performance-sensitive code.
Creating Sets
Build sets from any iterable
Using Curly Braces
The type() function confirms that these objects are sets. Python's set type is a built-in type, meaning it is always available without importing anything. Sets are as fundamental to Python as lists, dictionaries, and tuples.
The Empty Set Problem
There is one critical exception to the curly brace syntax that trips up many Python programmers. You cannot create an empty set with empty curly braces. When Python sees {}, it interprets this as an empty dictionary, not an empty set. This behavior exists for historical reasons: dictionaries were added to Python before sets, and {} was already established as the dictionary literal syntax.
This quirk catches even experienced Python developers. If you write code that initializes a variable with {} and later tries to use set methods like add(), you will get an AttributeError because dictionaries do not have an add() method. The error message might be confusing because you thought you had a set.
Sets from Other Collections
The set() constructor is versatile. It can convert any iterable object into a set. An iterable is anything you can loop over: lists, tuples, strings, ranges, and even other sets. This conversion automatically removes any duplicate values, which is often exactly what you want.
Strings are iterable in Python, meaning you can loop over their characters. When you pass a string to set(), Python treats it as a sequence of characters and creates a set containing each unique character. The string "mississippi" has eleven characters total, but only four unique letters: m, i, s, and p. The set contains exactly these four characters.
> You have a list [3, 1, 2, 1] with a duplicate value. Pick a constructor to convert it and see how set, list, and tuple each handle duplicates and ordering differently.
result = ([3, 1, 2, 1]) print(type(result)) print(result)
The constructor you choose determines everything about the resulting collection: whether it keeps duplicates, whether it maintains order, and whether it supports fast membership checks. set() is unique in that it both removes duplicates and provides O(1) lookups.
A common pattern is to convert a list to a set and back: list(set(my_list)). This deduplicates a list in one step, though the output order may differ from the input since sets do not guarantee ordering.
Automatic Duplicate Removal
Deduplicate data with add and update
Counting Unique Values
Ordered Duplicate Removal
This approach iterates through the original list once. For each item, it checks whether the item has been seen before by checking the set. If not seen, it adds the item to both the seen set (for fast future lookups) and the unique_ordered list (to preserve order). If already seen, it skips the item. The first occurrence of each item is preserved in its original position.
In Python 3.7 and later, dictionaries preserve insertion order. The dict.fromkeys() method creates a dictionary where each item becomes a key (with None as the value). Since dictionary keys must be unique, duplicates are automatically eliminated while order is preserved. Converting back to a list gives you the deduplicated, ordered result.
Adding Elements to Sets
Sets are mutable, meaning you can add elements after creation. Python provides two methods for adding elements: add() for single elements and update() for adding multiple elements at once. Understanding when to use each method helps you write cleaner, more efficient code.
The add() Method
The .add() method inserts exactly one element into the set. If the element already exists, the set remains unchanged and no error is raised. The .add() method modifies the set in place and returns None (not the modified set).
Building Sets with Loops
The update() Method
The .update() method adds multiple elements from any iterable (list, tuple, string, set, or other iterable). This is more concise and often more efficient than calling add() repeatedly in a loop.
- Adds exactly one element
- set.add("item")
- Use for single additions
- Argument must be hashable
- Adds multiple elements
- set.update([a, b, c])
- Use for bulk additions
- Argument must be iterable
> Build a set one element at a time. Duplicates are silently ignored. Pick the method that inserts a single element, and the built-in that counts how many unique items remain.
colors = set() colors.("red") colors.add("blue") colors.add("red") print((colors))
Sets silently ignore duplicate insertions. Calling add() with a value that already exists is a no-op: the set remains unchanged and no error is raised. This makes sets ideal for collecting unique items in a loop without explicit duplicate checking.
For bulk additions, update() accepts any iterable: a list, tuple, another set, or even a string, which adds each character individually. If you need to add a list as a single element, convert it to a tuple first since lists are unhashable and cannot be stored in a set.
Removing Elements from Sets
Remove elements and test membership
Python provides several methods for removing elements from sets: remove(), discard(), pop(), and clear(). Each behaves differently and is suited for different situations. Understanding these differences helps you choose the right method and avoid unexpected errors.
remove() vs discard()
Both remove() and discard() delete a specific element from the set. The critical difference is what happens when the element does not exist. The remove() method raises a KeyError exception if the element is not found, while discard() silently does nothing.
In this example, discarding "mango" had no effect because mango was not in the set. No error was raised, and the set remained unchanged. If we had used remove("mango") instead, Python would have raised a KeyError exception, potentially crashing our program if we did not handle it.
- Raises KeyError if element missing
- Use when element MUST exist
- Fails fast on programming bugs
- Good for required elements
- Silent if element is missing
- Use when element MIGHT exist
- Safe for uncertain removal
- Good for optional cleanup
> A set {"apple", "banana"} does not contain "grape", but you try to remove it anyway. Pick a removal method to see which one handles the missing element gracefully.
fruits = {{"apple", "banana"}} fruits.("grape") print(fruits)
The pop() Method
The .pop() method removes and returns an arbitrary element from the set. Because sets are unordered, you cannot predict which element will be removed. This method is useful when you need to process elements one by one and do not care about the order, or when you need to empty a set while examining each element.
Calling pop() on an empty set raises a KeyError. Always ensure the set is not empty before popping, either by checking its length or using a while loop as shown above.
Clearing a Set
The .clear() method removes all elements from a set, leaving it empty. This is useful when you want to reset a set for reuse without creating a new set object.
Membership Testing
The in operator checks whether an element exists in a set. This operation is one of the primary reasons to use sets: membership testing in sets is extremely fast, with O(1) time complexity. This makes sets ideal for situations where you need to check existence frequently.
The expression "alice" in allowed_users returns True because "alice" is a member of the set. The expression "eve" in allowed_users returns False because "eve" is not in the set. The not in operator returns the logical opposite: True if the element is absent, False if present.
Why Sets Are Fast
List-to-Set for Fast Lookup
A common optimization pattern is to convert a list to a set when you need to perform many membership tests against it. The conversion has a one-time cost proportional to the list size, but each subsequent lookup is O(1). If you perform enough lookups, the time saved far exceeds the conversion cost.
In real applications, valid_codes_list might contain thousands or millions of entries loaded from a database or configuration file. If you needed to validate millions of user inputs against this list, using a set instead of a list could reduce validation time from hours to seconds.
The code below has a bug related to membership testing. The developer tried to use the in operator with a list literal instead of a set, losing the O(1) performance advantage. Fix it to use a set.
> This code checks membership using a list, which requires scanning every element. Switching to a set gives O(1) lookups instead of O(n).
Functional but slow: list uses O(n) lookup instead of O(1)
Converting a list to a set is one of the most common and impactful performance optimizations in Python. The change is a single word in the source code, but it can reduce the time complexity of membership checks from O(n) to O(1), making code that scanned thousands of items per check effectively instant.
Sets work for membership testing because hashing gives each element a predictable storage address. When you check item in my_set, Python computes the hash of the item and checks one location directly, without scanning any other elements.
What Can Be in a Set?
Identify which types sets accept
Mutable types like lists, dictionaries, and regular sets cannot be set elements because their hash values would change if modified. Python raises a TypeError if you try to add an unhashable type to a set.
Notice that True was "added" but the set size did not change. This is because Python considers True and 1 to be equal (and they have the same hash). Since 42 is already in the set and True equals 1 not 42, True just maps to the same slot. Mutable types like lists trigger a TypeError immediately.
Common Mistakes to Avoid
Mistake 1: {} vs set()
The most common set mistake is trying to create an empty set with empty curly braces {}. Python interprets this as an empty dictionary, not an empty set. This mistake often leads to AttributeError exceptions later when you try to use set methods.
- empty = {}
- Creates a dictionary!
- type(empty) returns dict
- empty.add("x") raises AttributeError
- empty = set()
- Creates a set!
- type(empty) returns set
- empty.add("x") works correctly
Mistake 2: Expecting Order
Mistake 3: Indexing Sets
You cannot access set elements by index. Sets have no concept of "first element" or "element at position 2" because they have no order. Trying to index a set with square brackets raises a TypeError.
Using sorted() gives you a predictable ordering every time, unlike converting to an unsorted list where the order could vary. If you need indexed access often, store your data in a list instead of a set.
Mistake 4: In-Loop Mutation
Adding or removing elements from a set while iterating over it can cause unexpected behavior or RuntimeError exceptions. If you need to modify a set based on its contents, iterate over a copy instead.
> This code uses {} to create what it thinks is an empty set, but Python interprets {} as an empty dictionary. The .add() call then fails.
AttributeError: 'dict' object has no attribute 'add'
Practical Examples
- You need to count unique items from a collection with duplicates
- You are validating input against a list of allowed values
- You want to find duplicates by comparing list and set lengths
- You are tracking which items you have already processed
- You need fast membership testing across many repeated lookups
Example 1: Duplicate Values
Example 2: Validating Input
Example 3: Tracking Items
> You are a data analyst at Mailchimp deduplicating email addresses collected from three separate campaign upload files before running a bulk re-engagement send, ensuring no subscriber receives the same message twice and that every address meets basic hashability requirements.
in operator checks whether a specific email was already captured before deciding whether to include it from a new source list.in is O(1) - extremely fast regardless of set sizeCollections that guarantee uniqueness
- Category
- Python
- Difficulty
- beginner
- Duration
- 55 minutes
- Challenges
- 3 hands-on challenges
Topics covered: What is a Set?, Creating Sets, Automatic Duplicate Removal, Removing Elements from Sets, What Can Be in a Set?
Lesson Sections
- What is a Set? (concepts: pySets)
A set is an unordered collection of unique elements. These two properties define what makes a set different from other collection types like lists and tuples. Understanding both properties is essential for using sets correctly. The word "unordered" means that sets do not maintain any particular sequence for their elements. Unlike lists, where the first item you add stays first and the last item stays last, sets make no guarantees about element order. When you iterate over a set or print it, the
- Creating Sets (concepts: pySets)
Python provides two main ways to create sets. You can use curly braces with elements inside, similar to how you write dictionary literals but without key-value pairs. Alternatively, you can use the set() constructor function, which can convert other iterables into sets. Each approach has specific use cases and limitations that you should understand. Using Curly Braces The most common and concise way to create a set with initial elements is using curly braces. Place your elements inside the brace
- Automatic Duplicate Removal (concepts: pySets)
The automatic duplicate removal behavior of sets is one of their most powerful and useful features. Sets eliminate duplicates both during creation and when adding new elements. This happens silently, without errors or warnings. Understanding this behavior allows you to write cleaner, more concise code. Even though we specified "Alice" three times and "Bob" twice in the set literal, the resulting set contains each name exactly once. Python processes the elements in order, adding each one to the s
- Removing Elements from Sets (concepts: pySets)
remove() vs discard() Both approaches handle missing elements gracefully. The if-check approach is explicit, while discard() handles it silently. Choose based on whether you want your code to acknowledge the absence or ignore it entirely. Try choosing different removal methods below to see how each one behaves when the element is missing from the set. The pop() Method The exact order in which elements are popped depends on Python's internal implementation and can vary between different runs or P
- What Can Be in a Set? (concepts: pySets)
Not everything can be an element of a set. Set elements must be hashable, which generally means they must be immutable (unchangeable after creation). This requirement exists because sets use hashing to organize elements internally. If an element could change after being added, the set would not be able to find it anymore because its hash value would be different. Notice that the coordinate set shows only four points even though we specified five. The point (0, 0) was specified twice but only app