DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

Python Sets and Set Operations

python sets solve a specific, common problem elegantly: you need a collection of unique items, and you need to check membership or compare collections quickly. Lists can technically be forced into this role, but sets are purpose-built for it — and python set operations give you the same union, intersection, and difference logic you might remember from a math class, applied directly to your data.

What is a Set in Python?

A set is an unordered collection of unique items, where every item must be of an immutable type (numbers, strings, tuples — the same requirement covered for dictionary keys in the tuples article).

unique_numbers = {1, 2, 3, 4}

Creating sets

# Curly brace literal syntax
fruits = {"apple", "banana", "cherry"}

# The set() constructor, converting from another iterable
letters = set("hello")
print(letters)   # {'h', 'e', 'l', 'o'} — note: duplicates automatically removed

That second example is worth pausing on: "hello" has five characters, but the resulting set has only four unique ones — the repeated l was automatically collapsed. Duplicate removal isn't something you have to do manually; it's built directly into what a set is.

One quirk worth knowing early: you can't create an empty set with {} — that syntax creates an empty dictionary instead. Use set() for an empty set:

empty_set = set()      # correct
not_a_set = {}          # this is actually an empty dict
Key traits

Two properties define how sets behave, and both matter practically:

  • Unordered — sets don't preserve insertion order the way lists and dictionaries do, and you can't access an item by position (my_set[0] doesn't work — there's no index 0 to speak of).

  • Backed by a hash table — this is why sets exist in the first place. Checking whether an item is in a set is extremely fast, technically described as O(1) — meaning it takes roughly the same amount of time no matter how large the set is. Checking membership in a list, by contrast, is O(n) — Python has to check items one by one, so it gets slower as the list grows. For any code doing repeated membership checks (if x in collection:) against a large collection, this difference is genuinely significant.

Union: Combining Sets

The | operator and union() method

Union combines two sets into a new set containing every unique element from both:

set_a = {1, 2, 3}
set_b = {3, 4, 5}

print(set_a | set_b)          # {1, 2, 3, 4, 5}
print(set_a.union(set_b))     # {1, 2, 3, 4, 5} — identical result

Both approaches produce the same result — | is a compact operator syntax, .union() is the equivalent method, and which one you reach for is mostly a matter of style.

Real-world framing

A natural use case: merging two mailing lists without ending up with duplicate emails.

list_a = {"[email protected]", "[email protected]"}
list_b = {"[email protected]", "[email protected]"}

combined = list_a | list_b
print(combined)
# {'[email protected]', '[email protected]', '[email protected]'}

Even though [email protected] appeared in both original lists, it only appears once in the result — which is exactly the behavior you'd want from a deduplicated merge.

Union is symmetric

A | B produces the same result as B | A — order doesn't matter for union. You can also chain more than two sets together at once:

set_a = {1, 2}
set_b = {2, 3}
set_c = {3, 4}

print(set_a | set_b | set_c)   # {1, 2, 3, 4}

Intersection and Difference

Intersection: what's common to both
pyth {"Alex", "Sam", "Jordan", "Taylor"}
aws_devs = {"Sam", "Jordan", "Casey"}

print(python_devs & aws_devs)                 # {'Sam', 'Jordan'}
print(python_devs.intersection(aws_devs))     # {'Sam', 'Jordan'} — identical result

This is a genuinely practical use case worth noting explicitly: finding candidates who satisfy multiple requirements at once — here, developers who know both Python and AWS — by intersecting two sets of names.

Difference: what's in one but not the other
pyth {"Alex", "Sam", "Jordan", "Taylor"}
aws_devs = {"Sam", "Jordan", "Casey"}

print(python_devs - aws_devs)                # {'Alex', 'Taylor'}
print(python_devs.difference(aws_devs))      # {'Alex', 'Taylor'} — identical result

This finds people who know Python but not AWS — a different, meaningful question from the intersection above.

Difference is not symmetric

Unlike union, order genuinely matters for difference:

print(python_devs - aws_devs)   # {'Alex', 'Taylor'} — in python_devs, not in aws_devs
print(aws_devs - python_devs)   # {'Casey'} — in aws_devs, not in python_devs

A - B and B - A ask two entirely different questions and will generally produce two entirely different results.

A visual way to think about it

If you picture two overlapping circles (a Venn diagram):

  • Union is everything covered by either circle, combined.

  • Intersection is only the overlapping middle region.

  • Difference (A - B) is the part of circle A that doesn't overlap with circle B.

Keeping that mental picture handy makes the distinction between these three operations far easier to remember than the operator symbols alone.

Symmetric Difference and Set Comparisons

Symmetric difference: in either, but not both
set_a = {1, 2, 3}
set_b = {2, 3, 4}

print(set_a ^ set_b)                        # {1, 4}
print(set_a.symmetric_difference(set_b))    # {1, 4} — identical result

Symmetric difference is essentially union minus intersection — everything that's in at least one of the sets, but excluding whatever's shared by both. In the example above, 2 and 3 are in both sets, so they're excluded; 1 and 4 each belong to only one set, so they're included.

issubset() and issuperset()

These check containment relationships rather than combining sets — they answer "is one set entirely contained within another?":

small = {1, 2}
large = {1, 2, 3, 4}

print(small.issubset(large))     # True — every element of small is in large
print(large.issuperset(small))   # True — large contains every element of small

You can also use <= and >= as operator equivalents for issubset() and issuperset() respectively, though the explicit method names tend to read more clearly.

Practical example: comparing config file versions

A genuinely useful real-world pattern: comparing two versions of a configuration file's keys (or any structured data) to see exactly what changed.

old_c {"host", "port", "timeout", "debug"}
new_c {"host", "port", "timeout", "retries", "log_level"}

added = new_config_keys - old_config_keys
removed = old_config_keys - new_config_keys
unchanged = old_config_keys & new_config_keys

print(f"Added: {added}")         # Added: {'retries', 'log_level'}
print(f"Removed: {removed}")     # Removed: {'debug'}
print(f"Unchanged: {unchanged}") # Unchanged: {'host', 'port', 'timeout'}

This single pattern — difference in both directions plus intersection — is a compact, readable way to summarize exactly what changed between two versions of anything represented as a set.

Modifying Sets and frozenset

Mutating methods
fruits = {"apple", "banana"}

# add() — adds a single item
fruits.add("cherry")
print(fruits)   # {'apple', 'banana', 'cherry'}

# update() — adds multiple items from another iterable
fruits.update(["date", "fig"])
print(fruits)   # {'apple', 'banana', 'cherry', 'date', 'fig'}
remove() vs. discard()

Both remove a specific item, but they behave differently when that item isn't present:

fruits = {"apple", "banana"}

fruits.remove("banana")    # removes it, no error
fruits.remove("grape")
# KeyError: 'grape' — remove() raises an error if the item isn't found

fruits.discard("grape")    # does nothing, no error — discard() is safe either way

Use remove() when you expect the item to definitely be present and want an error if that assumption turns out to be wrong. Use discard() when you just want the item gone if it exists, and don't care whether it was there in the first place.

frozenset: the immutable variant

frozenset is exactly what it sounds like — a set that can't be modified after creation, which (echoing the same logic covered for tuples) makes it hashable and therefore usable as a dictionary key or as an element inside another set:

team_a = frozenset(["Alex", "Sam"])
team_b = frozenset(["Jordan", "Casey"])

matchups = {team_a: "Group 1", team_b: "Group 2"}
print(matchups[team_a])   # Group 1

A regular, mutable set can't be used as a dictionary key for the same reason a list can't — its contents could change after being used as a key, which would break the dictionary's internal consistency. frozenset sidesteps that entirely by removing the ability to change it in the first place, making it a natural fit for fixed collections like a team roster or a locked-in configuration.

Set comprehensions

Sets support a comprehension syntax mirroring list comprehensions, letting you build a set concisely from an expression and a condition:

even_numbers = {n for n in range(10) if n % 2 == 0}
print(even_numbers)   # {0, 2, 4, 6, 8}

This produces a set directly, with duplicate results (if any arose from the expression) automatically collapsed — the same deduplication behavior as any other set.

Wrap-up: when sets are the right tool

Sets are the right choice specifically for deduplication, fast membership testing, and comparing collections against each other — union, intersection, difference, and the containment checks covered above. They're deliberately not the right tool when order matters or when you need to access items by position — for those situations, a list (covered earlier in this series) remains the better fit.

PREVIOUSNEXT LESSON