Python Sets: Operations, Math Sets, frozenset

Complete Python set tutorial covering creation, set operations (union, intersection, difference), methods, frozenset, and real uses. Tested on Python 3.14.6 with set theory visualized.

“Sets are the hidden powerhouse. Most beginners reach for a list when a set would do the same job ten times faster.”

Luciano Ramalho, Fluent Python

Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 18 minutes

A Python set is the built-in collection for storing unique items with no order. Picture the bunch of keys you carry around. You only ever keep one copy of each key, and they jingle around in your pocket in no fixed order. A set works the same way: add the same value twice and the second copy just vanishes, and you cannot ask for “the third item” because a set has no positions. Under the hood a set is a hash table, which is the same trick a dictionary uses, so checking “is this value in here?” stays fast no matter how big the set grows.

That speed is the whole reason sets exist. Checking if an item is in a set is O(1), which means the cost stays flat whether the set holds 10 values or 10 million. A list has to walk through its items one by one, so the same check is O(n) and gets slower as the list grows. If you have ever written if x in some_huge_list and watched it crawl, this is your fix: put the values in a set first.

You land on this page from a search, so here is the deal. This is a reference. The cheat sheet below lists every set operation and method in one place, then each group gets short, tested examples you can copy. The trade-off you accept for the speed: no duplicates, no indexing, and no guaranteed order. Keep that in mind and a set becomes the first thing you reach for whenever uniqueness or a fast lookup matters.

Set Cheat Sheet

Most people land here looking for one specific thing, so here is the whole map up front. Every operator and method a set gives you, in one table. Skim it, grab what you need, and read the matching section below for a tested example. Where an operator and a method do the same job, both are listed on the same row.

OperatorMethodWhat it doesQuick example
|.union()All items from both sets{1, 2} | {2, 3} gives {1, 2, 3}
&.intersection()Only items in both sets{1, 2} & {2, 3} gives {2}
-.difference()Items in the first, not the second{1, 2} - {2, 3} gives {1}
^.symmetric_difference()Items in one set but not both{1, 2} ^ {2, 3} gives {1, 3}
<=.issubset()Is every item of A also in B?{1} <= {1, 2} gives True
>=.issuperset()Does A contain all of B?{1, 2} >= {1} gives True
n/a.isdisjoint()Do the sets share nothing?{1}.isdisjoint({2}) gives True
n/a.add(x)Add one items.add("x")
n/a.update(iterable)Add many items at onces.update([1, 2, 3])
n/a.remove(x)Delete x, error if missings.remove("x")
n/a.discard(x)Delete x, no error if missings.discard("x")
n/a.pop()Remove and return an arbitrary itemitem = s.pop()
n/a.clear()Empty the sets.clear()
n/alen(s)How many itemslen({1, 2, 3}) gives 3
n/ax in sFast O(1) membership test2 in {1, 2} gives True
Every operator has a method twin. The operators need sets on both sides, but the methods accept any iterable (a list, a tuple, even a string).

The “n/a” in the Operator column means there is no symbol shortcut, so you call the method by name. The next sections work through each group with code you can run as you read.

Creating Sets

Building a set is like taking attendance for a workshop: however many times someone signs the sheet, they get exactly one seat. In the example below, three people named Rahul, Anvi, and Aditi register, and two of them accidentally hit submit twice. Watch the duplicates disappear.

📄 create_sets.py: four ways to build a set

# Literal syntax
fruits = {"apple", "banana", "mango", "apple"}  # duplicate dropped
print(f"Fruits: {fruits}")
print(f"Length: {len(fruits)}")

# From a list (deduplication)
names = ["Rahul", "Anvi", "Rahul", "Aditi", "Anvi"]
unique_names = set(names)
print(f"Unique: {unique_names}")

# From a string
chars = set("mississippi")
print(f"Unique chars: {chars}")

# Empty set: use set(), NOT {} (that builds an empty dict!)
empty = set()
print(f"Type of set(): {type(empty)}")
print(f"Type of {{}}: {type({})}")

▶ Output (item order will vary on your machine)

Fruits: {'banana', 'mango', 'apple'}
Length: 3
Unique: {'Anvi', 'Rahul', 'Aditi'}
Unique chars: {'i', 'm', 's', 'p'}
Type of set(): <class 'set'>
Type of {}: <class 'dict'>

What happened here: Both duplicate “apple” entries collapsed into one, and the string set kept a single copy of each letter. Notice the order of the items. Python does not sort a set or keep insertion order, so the items can come out in any order, and that order can even change the next time you run the script. That is normal, so never count on it. The one trap that bites everyone: {} is an empty dict, not an empty set. When you want an empty set, write set().

Set Operations: Union, Intersection, Difference

Symmetric Difference A ^ B .symmetric_difference()Difference A B .difference()Intersection A and B .intersection()Union A | B .union()A only{1, 2}A B{3, 4}B only{5, 6}Result: {1, 2, 3, 4, 5, 6}A only{1, 2}A B{3, 4}B only{5, 6}Result: {3, 4}A only{1, 2}A B{3, 4}B only{5, 6}Result: {1, 2}A only{1, 2}A B{3, 4}B only{5, 6}Result: {1, 2, 5, 6}Python Sets: Union, Intersection, Difference, and Symmetric Difference

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

The chart above splits two sets, A (with 1, 2, 3, 4) and B (with 3, 4, 5, 6), into three slices: items only in A, items in both, and items only in B. Then it shows what each of the four operations keeps. Union grabs everything, intersection keeps just the shared middle, difference keeps A minus the overlap, and symmetric difference keeps both ends but drops the middle. If the maths words feel dry, think of two friends comparing music playlists: the songs they both have is the intersection, everything across both phones is the union, and “songs only I have” is the difference.

These four ideas come straight from school maths, so the colours map one to one onto the |, &, -, and ^ operators you are about to use. For real data, picture a small office where developers named Rahul, Niranjan, Viraj, and Pravin write Python, and a few of them also work on the JavaScript side with Anvay and Aviraj.

📄 operations.py: who codes in which language, using operators

python_devs = {"Rahul", "Niranjan", "Viraj", "Pravin"}
js_devs = {"Viraj", "Anvay", "Aviraj", "Pravin"}

# Union: everyone on either team
all_devs = python_devs | js_devs
print(f"Union: {all_devs}")

# Intersection: people who know both languages
both = python_devs & js_devs
print(f"Both: {both}")

# Difference: Python devs who do not do JS
python_only = python_devs - js_devs
print(f"Python only: {python_only}")

# Symmetric difference: one language but not the other
exclusive = python_devs ^ js_devs
print(f"Exclusive: {exclusive}")

▶ Output (item order will vary on your machine)

Union: {'Pravin', 'Rahul', 'Viraj', 'Anvay', 'Niranjan', 'Aviraj'}
Both: {'Pravin', 'Viraj'}
Python only: {'Niranjan', 'Rahul'}
Exclusive: {'Rahul', 'Anvay', 'Niranjan', 'Aviraj'}

Every operator above has a method twin that does the exact same thing: | is .union(), & is .intersection(), - is .difference(), and ^ is .symmetric_difference(). So why keep both? The methods take any iterable, so python_devs.union(["Vinay", "Anvay"]) works even though the argument is a list. The operators are stricter: both sides must already be sets, or Python raises a TypeError. Reach for the operator when your data is already sets (it reads cleaner), and the method when one side is a list, tuple, or other iterable.

Modifying Sets

Sets are mutable, so you can add and remove items after you build one. Think of the contacts app on your phone: save the same number a second time and you still have just one entry for that person. Below, a project roster starts with Rahul and Viraj, a new teammate named Sardar joins, and people come and go. Here are the five methods you will actually use, all in one script so you can see how a set changes step by step.

📄 modifying.py: add, remove, discard, pop, update

team = {"Rahul", "Viraj"}

# Add one item
team.add("Sardar")
print(f"After add: {team}")

# Add a duplicate: no error, nothing changes
team.add("Rahul")
print(f"After dup add: {team}")

# remove(): deletes the item, raises KeyError if it is missing
team.remove("Viraj")
print(f"After remove: {team}")

# discard(): safe delete, stays quiet if the item is missing
team.discard("Nonexistent")
print(f"After discard: {team}")

# pop(): removes and returns an arbitrary item (you do not pick which)
popped = team.pop()
print(f"Popped: {popped}")

# update(): add several items at once from any iterable
team.update(["Niranjan", "Pravin", "Vinay"])
print(f"After update: {team}")

▶ Output (item order, and which item pop removes, will vary)

After add: {'Sardar', 'Viraj', 'Rahul'}
After dup add: {'Sardar', 'Viraj', 'Rahul'}
After remove: {'Sardar', 'Rahul'}
After discard: {'Sardar', 'Rahul'}
Popped: Sardar
After update: {'Rahul', 'Niranjan', 'Vinay', 'Pravin'}

What happened here: Adding “Rahul” a second time did nothing, which is the whole point of a set. The two that trip people up are remove() and pop(). remove() throws a KeyError if the item is not there, so use discard() when you are not sure. And pop() does not let you choose which item leaves, because a set has no order. In this run it removed “Sardar”, but on your machine it might remove a different name. If you need a specific item gone, name it with remove() or discard() instead.

Set Comparisons

Three methods answer “how do these two sets relate?” without you having to loop. It is the question you ask before cooking: does my kitchen already have every ingredient this recipe needs? If yes, the recipe is a subset of your kitchen. Is one set fully inside another? Do they share anything at all? Each returns a plain True or False.

📄 comparisons.py: subset, superset, disjoint

a = {1, 2, 3, 4, 5}
b = {2, 3}
c = {8, 9}

print(f"b subset of a: {b.issubset(a)}")       # True (or b <= a)
print(f"a superset of b: {a.issuperset(b)}")    # True (or a >= b)
print(f"a and c disjoint: {a.isdisjoint(c)}")   # True (no overlap)
print(f"a and b disjoint: {a.isdisjoint(b)}")   # False (overlap: 2, 3)

▶ Output

b subset of a: True
a superset of b: True
a and c disjoint: True
a and b disjoint: False

What happened here: A subset means every item of b is also in a, and here {2, 3} fits inside {1, 2, 3, 4, 5}, so it is True. Superset is just the same check the other way round. The handy one is isdisjoint(): it tells you the two sets share nothing. Sets a and c have no common items, so they are disjoint, while a and b overlap on 2 and 3, so they are not. These all return booleans, so the order issue from earlier sections does not apply.

Set Comprehensions

A set comprehension builds a set in one line, the same way a list comprehension builds a list, except you wrap it in curly braces. You get the deduplication for free, so it is perfect when you want the unique values out of something. It works like collecting autographs at a cricket match: no matter how many times the same player walks past you, you end up with one autograph per player.

📄 set_comp.py: build a set in a single line

# Unique first letters
names = ["Rahul", "Niranjan", "Viraj", "Vinay", "Pravin", "Prathamesh"]
first_letters = {name[0] for name in names}
print(f"First letters: {first_letters}")

# Squares of the even numbers from 1 to 10
even_squares = {n**2 for n in range(1, 11) if n % 2 == 0}
print(f"Even squares: {even_squares}")

▶ Output (item order will vary on your machine)

First letters: {'N', 'R', 'P', 'V'}
Even squares: {64, 100, 4, 36, 16}

What happened here: Six names start with only four distinct letters (two start with P, two with V), and the set quietly kept one of each. The second comprehension squared 2, 4, 6, 8, and 10. Notice the result is not sorted: 64 shows up before 4. A set never sorts for you, so if you want the numbers in order, wrap the result in sorted().

frozenset: The Immutable Set

A regular set cannot be a dictionary key or live inside another set, because Python needs keys to be hashable (stable) and a normal set can change at any time. A frozenset is a set that has been locked: same operations and same speed, but you cannot add or remove items. That lock is exactly what makes it hashable, so it can be a key. Think of it like a printed boarding pass versus a note you can scribble on. Once printed, the boarding pass cannot change, and that is precisely why the gate scanner trusts it.

📄 frozenset_demo.py: a locked set you can use as a dict key

# A normal set cannot be a dict key or a set item.
# A frozenset can, because it is locked (immutable) and hashable.

permissions = frozenset(["read", "write"])
print(f"Frozen: {permissions}")

# Trying to change it fails: frozensets have no .add()
try:
    permissions.add("execute")
except AttributeError as e:
    print(f"Error: {e}")

# Now use frozensets as dictionary keys
role_perms = {
    frozenset(["read"]): "viewer",
    frozenset(["read", "write"]): "editor",
    frozenset(["read", "write", "admin"]): "admin",
}
print(f"Role: {role_perms[frozenset(['read', 'write'])]}")

▶ Output (item order will vary on your machine)

Frozen: frozenset({'read', 'write'})
Error: 'frozenset' object has no attribute 'add'
Role: editor

What happened here: Calling .add() on a frozenset raised an AttributeError, because the method simply does not exist on a locked set. That same lock is what let each frozenset act as a dictionary key. We looked up the set of read and write permissions and got back “editor”. This is a clean way to map a bunch of permissions, or any group of values, to a single label.

Methods Head to Head

A few set methods look alike and get mixed up constantly. Here are the pairs people search for, side by side, so you can pick the right one without guessing.

remove() vs discard()

MethodItem is presentItem is missingUse it when
remove(x)Deletes xRaises KeyErrorYou expect x to be there and want a loud failure if it is not
discard(x)Deletes xDoes nothing, stays quietYou just want x gone and do not care whether it existed

remove() vs pop()

MethodYou pick the item?What it returnsUse it when
remove(x)Yes, you name xNothing (None)You know exactly which item to drop
pop()No, Python picks oneThe item it removedYou want any one item out and do not care which

set() vs dict.fromkeys() for deduplication

ApproachKeeps order?SpeedUse it when
list(set(items))NoFastestYou only need the unique values and order does not matter
list(dict.fromkeys(items))Yes, first-seen orderSlightly slowerYou need unique values in their original order

The “Python way” tip: when you only want unique items and do not care about order, set() is the cleanest choice. The moment order matters, switch to dict.fromkeys(), which you will see in action in the next section.

Real-World Use Cases

Enough syntax. Here are four jobs a set does better than anything else, the kind that show up in real code every week.

📄 real_world.py: four set patterns from real code

# 1. Remove duplicates from a list (keep original order with dict.fromkeys)
raw = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
unique_ordered = list(dict.fromkeys(raw))
print(f"Unique ordered: {unique_ordered}")

# 2. Find common tags between posts
post1_tags = {"python", "tutorial", "beginner", "lists"}
post2_tags = {"python", "tutorial", "intermediate", "functions"}
common = post1_tags & post2_tags
print(f"Common tags: {common}")

# 3. Fast membership testing
valid_extensions = {".py", ".js", ".ts", ".html", ".css"}
filename = "app.py"
ext = "." + filename.split(".")[-1]
print(f"Valid file: {ext in valid_extensions}")

# 4. Find missing required fields
required = {"name", "email", "password"}
submitted = {"name", "email"}
missing = required - submitted
print(f"Missing fields: {missing}")

▶ Output (the Common tags order will vary on your machine)

Unique ordered: [3, 1, 4, 5, 9, 2, 6]
Common tags: {'tutorial', 'python'}
Valid file: True
Missing fields: {'password'}

What happened here: Four common jobs, four one-liners. Pattern 1 strips duplicates while keeping the first-seen order, which a plain set cannot do. Pattern 2 uses & to find the tags two posts share. Pattern 3 is the speed win this whole post is about: ext in valid_extensions is an O(1) check, so it stays fast even with thousands of allowed extensions. Pattern 4 uses - to spot which required fields a form left out, which is a tidy way to validate user input. The required - submitted trick alone has saved many a sign-up form.

Common Mistakes

Mistake 1: Using {} for an empty set

🚫 Builds an empty dict, not a set

empty = {}            # this is a dict
print(type(empty))    # <class 'dict'>

✅ Use set() for an empty set

empty = set()         # this is a set
print(type(empty))    # <class 'set'>

Why: Curly braces were dictionaries first, so Python keeps {} meaning “empty dict” for backward compatibility. There is no empty-set literal, so you have to spell it out as set(). This one bites beginners because the code runs fine, then a later .add() on what you thought was a set fails.

Mistake 2: Expecting a set to stay in order

Sets have no order, full stop. If you need unique items and their original order, use list(dict.fromkeys(items)) instead. Never count on set iteration order, because it can change from one run to the next, which is exactly why every “Output” block in this post warns that the item order will vary on your machine.

Best Practices

  • DO use sets for membership testing: if item in my_set is O(1)
  • DO use set operations for finding commonalities and differences
  • DO use discard() over remove() when unsure if item exists
  • DO use frozenset when you need an immutable, hashable set
  • DON’T rely on set order, because it is never guaranteed
  • DON’T use {} for an empty set, since that builds a dict instead

Conclusion

A set stores unique items, has no order, and gives you O(1) membership tests. It does the four math operations (union, intersection, difference, symmetric difference) through both operators and methods. Reach for one whenever uniqueness matters or you need a fast in check. When you need a set that cannot change, so it can be a dictionary key, use a frozenset. The one rule to carry forward: a set never keeps order, so wrap it in sorted() or use dict.fromkeys() when order matters.

Next up: Choosing the Right Data Structure, where list, tuple, dict, and set go head to head so you always know which one to reach for. And if you want the full roadmap, from basics all the way to AI/ML, browse the Python + AI/ML tutorial series home.

Practice Exercises

  1. Exercise 1: Find students in both classes using set intersection.
  2. Exercise 2: Find unique characters in a string using a set.
  3. Exercise 3: Build tag-based search: match ALL tags (intersection) or ANY (union).

Frequently Asked Questions

What is a set in Python?

A Python set is an unordered, mutable collection of unique items. You create one with {1, 2, 3} or set(). It drops duplicates automatically and gives you fast O(1) membership testing, so checking x in my_set stays quick no matter how big the set grows.

How do I remove duplicates from a list in Python?

The quickest way is list(set(my_list)), but that loses the original order. To keep the first-seen order, use list(dict.fromkeys(my_list)), which works because dictionaries keep insertion order from Python 3.7 onward.

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

remove(x) raises a KeyError if x is not in the set. discard(x) does nothing when x is missing. Use discard() when you are not sure the item is there and you do not want a crash.

What is a frozenset in Python?

A frozenset is a set you cannot change after you create it. Because it is locked it is also hashable, so unlike a normal set it can be a dictionary key or an item inside another set. It supports all the read-only operations like union and intersection.

Are Python sets ordered?

No. A Python set has no order, and the order items print in can even change between runs. If you need unique items in their original order, use list(dict.fromkeys(items)). If you need them sorted, wrap the set in sorted().

When should I use a set instead of a list in Python?

Use a set when you need unique items or fast membership tests. x in my_set is O(1), while x in my_list is O(n) and slows down as the list grows. Stick with a list when order matters or you need duplicates or indexing.

How do you find common items in two Python sets?

Use the intersection operator & or the .intersection() method: set_a & set_b returns a new set of the items in both. The method also accepts a list or tuple, so set_a.intersection([1, 2, 3]) works too.

Interview Questions on Python Sets

Interviewers rarely ask for definitions. They ask what happens in situations like these.

Q: Why is checking membership in a set O(1) while the same check on a list is O(n)?

A set is backed by a hash table. Python hashes the value, jumps straight to the matching slot, and checks it, so the cost stays flat no matter how many items the set holds. A list has no such index, so Python has to compare items one by one from the front until it finds a match or hits the end. That is why the check on a list gets slower as the list grows.

Q: What happens when you try to add a list to a set, and how do you work around it?

Python raises TypeError: unhashable type: 'list'. Set items must be hashable, and a list is mutable, so its hash could go stale the moment it changes. The fix is to convert the list to a tuple before adding it: s.add((1, 2)) works fine. If the item you want to store is itself a set, convert it to a frozenset for the same reason.

Q: Your code deduplicates user IDs with list(set(user_ids)), and a downstream report now shows rows in a different order on every run. What is happening and how do you fix it?

Set iteration order is not guaranteed, and for strings it can genuinely change between runs because Python randomizes string hashing. The set did its deduplication job, but it shredded the ordering as a side effect. If the report needs the original first-seen order, switch to list(dict.fromkeys(user_ids)). If it just needs a stable order, wrap the set in sorted().

Q: A request handler checks every incoming word against a list of 100,000 banned words, and response times are climbing. What do you change first?

Convert the banned-words list to a set once, at startup, and test against that. Each word in banned_list is an O(n) scan through 100,000 items, so a request with 50 words does up to 5 million comparisons. With a set, each check is O(1), so the per-request cost collapses to roughly 50 hash lookups. The one-time conversion cost is trivial compared to paying the scan on every request.

Q: What is the difference between s.update(t) and s | t?

s.update(t) changes s in place, adding everything from t, and returns None. s | t leaves both sets untouched and returns a brand new set holding the union. There is a second difference: update() accepts any iterable, including a list or tuple, while | requires both sides to already be sets or it raises a TypeError.

Q: A teammate writes cache = {}, and later cache.add(key) crashes with AttributeError: 'dict' object has no attribute 'add'. Explain the bug.

{} creates an empty dictionary, not an empty set, because curly braces belonged to dicts first and Python has no empty-set literal. The code runs fine until the first .add(), since dicts do not have that method. The fix is one line: cache = set(). This bug is sneaky because {1, 2} with items inside really is a set, so the literal syntax only betrays you in the empty case.

Further reading: for the full reference, see the official Python documentation.

Previous: Python: Dictionary Comprehensions & Nested Dictionaries

Next: Python: Choosing the Right Data Structure

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *