A set is a collection of unique items where the dominant operation is fast membership: "is this value already in here?". If your code keeps asking that question against a growing pile of things, a set answers it in constant time, while a list answers it by walking every item. This lesson covers what a set is, the two ways to create one (including the empty-set gotcha), the rule about which kinds of items are allowed, and the small handful of operations you'll use the most: len, in, and iteration.
A set is a mutable collection of distinct, hashable values, with no defined order. Two ideas matter in that sentence: uniqueness (the same value can't appear twice) and fast membership (x in s is O(1) on average). Everything else about sets follows from those two properties.
Three values inside curly braces, separated by commas, with no colons. That's a set literal. The order you see when printing isn't guaranteed to match the order you wrote, and it can change between Python versions or even between runs in some cases. Sets don't track insertion order the way dictionaries do.
The fast-membership property is the reason sets exist. Asking "is "wireless" in this set?" hashes the value and jumps to one slot, regardless of whether the set has 3 items or 3 million. Asking the same question of a list scans the list element by element until it finds a match or runs off the end.
The cyan box is the lookup we're doing. The set path on top hashes the value once and jumps straight to the bucket. The list path on the bottom has no shortcut: it has to compare each item to the target until it finds a hit. With three items the difference is invisible. With a million items, the set finishes in roughly the same time, while the list does a million times more work in the worst case.
Cost: value in some_set is O(1) on average. value in some_list is O(n). If you're doing repeated membership checks against the same collection, build a set once and check against it.
A set is the right tool when the question your code keeps asking is "have I seen this before?" or "is this value in the allowed group?". Tags on a product, unique customer email addresses, order IDs that have already been processed, all of those fit a set naturally.
There are two ways to create a set. You'll write the first one most of the time, and the second one is the only way to get an empty set.
{...}A set literal is curly braces around a comma-separated list of values. No colons, no key-value pairs, just values.
The set literal looks similar to a dictionary literal, but the contents tell Python which one you mean. If there are colons (key: value), it's a dictionary. If there are bare values separated by commas, it's a set.
If you write the same value twice in a literal, the set silently keeps just one copy. That's the uniqueness property showing up at construction time, not an error.
The duplicate "electronics" is dropped. The set has three elements, not four. Python doesn't warn you about the repeat; it just keeps one.
set() Constructor and the Empty-Set GotchaHere's the part that trips up almost every Python beginner: {} is not an empty set. It's an empty dictionary, because dictionaries got the curly braces first. To make an empty set, you have to use the set() constructor.
This catches people the first time they try to start an empty set and add things to it. s = {} followed by s.add("electronics") would raise AttributeError: 'dict' object has no attribute 'add', which is a confusing message until you realize s was a dictionary the whole time. The fix is to write s = set().
The set() constructor also accepts any iterable and turns its values into a set. This is how you build a set from data you already have.
Five items go in, three come out. Two "Wireless Mouse" entries collapse into one, and two "USB Cable" entries collapse into one. This is the simplest way to deduplicate a list: wrap it in set(...). If you want a list back at the end, wrap that in list(...):
The order of the resulting list isn't guaranteed to match the input order, because the set in the middle threw the order away. If you need both uniqueness and the original order, there's a small trick using a dictionary; for now, just know that set(...) is the deduplication tool and that order is the trade-off.
set() works on any iterable, not just lists. Strings are iterables of characters, so passing a string to set() gives you a set of its unique characters:
The word has 11 characters but only 9 distinct ones (e and c each appear twice). The output order is whatever Python's hashing happens to produce. This same trick works on tuples, ranges, generator expressions, and anything else that can be iterated over.
A set isn't allowed to hold just anything. Its members must be hashable, the same rule that applies to dictionary keys. Hashable means the value has a stable hash for its lifetime, which in practice means it has to be immutable.
| Type | Allowed in a Set? | Example |
|---|---|---|
str | Yes | "electronics" |
int | Yes | 42 |
float | Yes | 3.14 |
bool | Yes | True |
tuple of hashables | Yes | ("US", "NYC") |
frozenset | Yes | frozenset({"a", "b"}) |
list | No | [1, 2] raises TypeError |
dict | No | {"a": 1} raises TypeError |
set | No | {1, 2} raises TypeError |
tuple containing a list | No | (1, [2]) raises TypeError |
Strings are the most common element type. Numbers are next. Tuples show up when you want a composite element, like ("US", "NYC") to represent a country-and-city pair. The rest of the table is the "no" list: lists, dictionaries, and other sets all carry mutable state, so they can't live inside a set.
The locations set holds tuples. Each tuple is two strings, both hashable, so the tuple itself is hashable, so it's allowed as a set element. The set doesn't care that the tuples have a structure; it just hashes them and stores them.
Putting a list (or anything mutable) inside a set fails loudly:
The error message is the same one you'd see when using a list as a dictionary key, for the same reason. If you wanted those two strings as a single group inside a set, you'd convert the list to a tuple first: favorites = {("Mouse", "Cable")}.
A tuple stops being hashable the moment it contains an unhashable value. (1, 2, 3) is fine. (1, [2, 3]) is not, because the list inside it is mutable.
The tuple isn't the problem on its own; the list inside it is. The error message blames the list, which is the actual offender.
There's a sibling type called frozenset which is an immutable set. Because it can't be changed, it's hashable, which means a frozenset can be an element of another set, or a key in a dictionary. The answer to "can I have a set of sets?" is "yes, if the inner sets are frozensets".
Three properties define how a set behaves day to day. We've touched on each one already; this section pulls them together so the behavior is in one place.
Unique. A set never holds two equal values. Adding a value that's already there is a no-op:
Both sets have two elements. The repeated values in the second literal collapse silently. This is the property you rely on when you use set(...) to deduplicate a list.
Mutable. A set can grow and shrink after it's created. The set as a whole is mutable (through methods like add, remove, and discard), even though its elements have to be immutable. A common stumble is conflating those two: the set itself can be modified, but the things you put inside it can't be modified.
Unordered. A set has no defined position for its elements. There's no s[0], no "first" or "last", and no guarantee about iteration order.
Don't write code that depends on a particular order coming out of a set. If you need order, convert to a list and sort it, or store the values in a different structure in the first place.
The lack of indexing is what catches people next, after the empty-set gotcha. Trying to grab "the first item" of a set is a TypeError:
"Subscriptable" is Python's term for "supports the [] operation". Lists and dictionaries are subscriptable; sets are not. If you need to look at the elements of a set, you iterate over them.
The three operations you'll do most often on a set are: count it, check whether something's in it, and walk through it. All three have the syntax you already know from lists and other collections.
Length with `len`. len(s) returns the number of elements in the set, in O(1) time (Python keeps a counter, it doesn't recount).
Four strings go into the literal, but the duplicate "alice@shop.com" is dropped, so the length is 3. This is one of the cleanest ways to answer "how many distinct customers placed orders today?" if you have a flat list of emails to start with.
Membership with `in` and `not in`. This is the operation sets are best at. Both in and not in return a boolean, and both are O(1) on average.
This is the pattern that makes sets shine. Imagine you're processing a stream of incoming orders and you want to skip any order you've already handled. You keep a set of seen IDs, and for each new order you check membership before doing the work. The check is constant time, no matter how big the set gets.
Five incoming orders, three processed, two skipped. The set tracks what we've already seen, and the in check is what makes the skip decision fast. (set.add is the method that grows the set; the meaning is clear enough here.)
Cost: Replacing if order_id in some_list with if order_id in some_set is one of the most reliable speedups in Python code. A list in check is O(n) and gets slower as the list grows. A set in check is O(1) and stays flat. If the same membership check runs inside a loop, the difference compounds.
Iteration with `for`. A for loop over a set visits each element once. The order isn't defined, but every element is visited exactly once.
Use iteration when you want to do something with every element, not when you need a specific position. If you need a sorted order for display, convert to a sorted list:
sorted(tags) returns a new list of the set's elements in sorted order. The set itself is untouched, and you get a predictable order to iterate over.
You've now seen four built-in collection types. The question worth asking before reaching for any of them is "what's the dominant operation?".
| Question | Use a... |
|---|---|
| Do I need fast "is this in the collection?" checks? | set |
| Do I need uniqueness with no other structure? | set |
| Is the order of items what I care about? | list |
| Do I need to look things up by a label or ID? | dict |
| Is this a fixed, small record where each position has a known meaning? | tuple |
| Will I keep adding the same value and want to ignore duplicates? | set |
| Will I iterate over the collection in a specific order? | list |
A few concrete examples make the choice obvious:
The cart wants a list because order matters and a customer can buy two of the same thing. The tags want a set because the question being asked is "does this product have the bestseller tag?", which is pure membership, and a duplicate tag would be a bug. The prices want a dict because the question is "what's the price of this product?", which is lookup by label. The coordinate wants a tuple because the shape is fixed.
A common beginner pattern is using a list to track "have I seen this?" with if x in seen: ... seen.append(x). That works, but it's slow once the list grows, because every in check walks the list. Swapping the list for a set is almost always the right move:
Both functions produce the same result, and both preserve insertion order. The first one does O(n) work per check, so processing a million emails would take roughly a million times more work in the worst case. The second one keeps the membership check at O(1), and the total runtime stays linear. The list is still there in the fast version, but only to remember the order; the set is doing the "have I seen this?" work.
10 quizzes