Lesson 9: Tuples and Sets ā Why Does Python Have THREE Ways to Store a List?
š Full written solution: https://timepasshub2539-hue.github.io/leetcode-python/python-lesson-09-tuples-and-sets.html š» Code on GitHub: https://github.com/timepasshub2539-hue/leetcode-python/tree/main/python-lesson-09-tuples-and-sets Chapters: 0:00 Why Does Python Have THREE Ways to Store a List of Things? 0:12 What Is a Tuple 0:23 What Is a Set 0:37 Key Vocabulary 0:53 Creating a Tuple 1:05 Why Use a Tuple 1:16 Unpacking a Tuple 1:29 Creating a Set 1:44 Set Operations 2:00 Fast Membership Checks 2:17 The List-in-a-Set Trap 2:33 Tuple vs List vs Set 2:50 My Honest Take 3:05 Recap Python gives you three ways to store a group of items ā list, tuple, and set ā and this lesson finally explains why. We cover tuple creation and unpacking, set operations and fast membership checks, and the sneaky 'list-in-a-set' trap that crashes beginner code. You'll walk away knowing exactly when to reach for each collection type, plus my honest take on which one gets overused. #Python #PythonTutorial #LearnPython #PythonForBeginners #CodingLesson Watch next: - Lesson: The Bug That Gets Everyone (Lists vs Tuples Explained): https://youtu.be/N-aP3biAzDs - Common Mistake: Indentation #shorts: https://youtu.be/lMEgNpMpZpw - Code That Reads Like English Still Breaks You #shorts: https://youtu.be/jgHbTSHJax8
Introduction
Every Python interview eventually circles back to one deceptively simple question: "Why didn't you use a set here instead of a list?" It sounds like a style nitpick, but it isn't. Picking the wrong container doesn't just make your code look less polished ā it can silently produce wrong results or quietly tank your program's performance as the input grows.
This is the kind of thing interviewers test because it separates someone who can make code work from someone who understands why it works. Lists, tuples, and sets all hold groups of values, but they encode three completely different promises about that data: can it change, does order matter, and are duplicates allowed. Once you internalize those three questions, choosing the right structure becomes automatic instead of something you second-guess mid-interview.
We'll rebuild tuples and sets from the ground up ā creation, unpacking, the operations that make each one shine ā and dig into a bug that trips up almost every beginner at least once: trying to put a list inside a set.
Problem Overview
Python gives you three built-in ways to store a collection of items: list, tuple, and set. On the surface they look interchangeable ā all three can hold multiple values, all three can be looped over, all three support things like len(). But underneath, each one makes a different trade-off:
- List ā ordered, changeable (mutable), duplicates allowed.
- Tuple ā ordered, unchangeable (immutable), duplicates allowed.
- Set ā unordered, changeable, duplicates automatically removed.
The task isn't to memorize these three bullet points ā it's to understand why those trade-offs exist and how to recognize, from the shape of your data, which one you actually need.
Example
Suppose you're modeling a single point on a map:
point = (40.7128, -74.0060) # tuple: a fixed coordinate pair
This never needs to change ā a coordinate is a coordinate. A tuple signals that intent directly.
Now suppose you're tracking which user IDs have already voted in a poll:
voted = set()
voted.add(101)
voted.add(203)
voted.add(101) # ignored, 101 is already there
print(voted) # {203, 101} ā order not guaranteed, no duplicate
The second add(101) does nothing. No error, no warning ā it just vanishes, because a set's entire job is to guarantee uniqueness.
And a list, for comparison, is what you reach for when the collection needs to grow, shrink, and hold duplicates in a specific order:
scores = [88, 92, 88, 75] # order and duplicates both matter here
Intuition
Here's how an engineer actually arrives at the right structure, instead of defaulting to a list out of habit.
Start by asking three questions about the data:
- Will this data ever need to change after creation? If no, that's a strong signal for a tuple. Locking it down isn't a limitation ā it's documentation. Anyone reading
point = (x, y)instantly knows this is a fixed record, not something to mutate mid-program. - Do I care about order, or do duplicates even make sense? If you're just tracking "have I seen this before," order is irrelevant and duplicates are actively unwanted. That's exactly what a set is built for.
- Am I going to check membership over and over? This is the question people miss most often. A list checks membership by walking through every item one at a time ā O(n). A set uses hashing to jump almost directly to the answer ā O(1) on average. If you're doing
if x in collectioninside a loop, a set will beat a list every single time as the data grows.
The mental model: a list is your everyday flexible container. A tuple is that same ordered idea, frozen solid. A set throws away order entirely in exchange for uniqueness and near-instant lookups.
Brute Force Solution
There isn't really a "brute force vs optimal" split here in the algorithmic sense ā this is a data structure selection problem, not a search problem. But it's useful to frame it that way, because the "brute force" instinct is: use a list for everything.
Idea: Store every collection as a list, regardless of whether it needs to change, whether order matters, or whether duplicates should be allowed.
Algorithm:
- Create a list.
- Append values as needed.
- Check membership with
in. - Deduplicate manually with loops or extra tracking if needed.
Advantages:
- Simple, one structure to remember.
- Works for every use case, technically.
Disadvantages:
- No protection against accidental mutation of "fixed" data.
- Manual deduplication is extra code you have to write and maintain.
- Membership checks are slow ā O(n) per check.
- Code doesn't communicate intent. A reader can't tell if a list is meant to change or not.
Time complexity: O(n) per membership check, O(n) for manual dedup. Space complexity: O(n).
Optimal Solution
The optimal approach is to match the structure to the data's actual constraints:
| Question | Answer | Structure |
|---|---|---|
| Does it need to change? | No | Tuple |
| Does it need to change? | Yes | List or Set |
| Do duplicates matter, and does order matter? | Yes to both | List |
| Do duplicates matter, and does order matter? | No to both | Set |
| Will I check membership repeatedly? | Yes | Set (hashing gives O(1) lookups) |
Tuple creation and unpacking
person = ("Maya", 29)
name, age = person # unpacking ā order lines up exactly
The one-item tuple trap: (5) is just the integer 5 in parentheses. You need a trailing comma ā (5,) ā to actually make it a tuple. Python has no other way to tell the difference.
Set creation and operations
empty = set() # NOT {} ā that creates an empty dict
a = {1, 2, 3}
b = {2, 3, 4}
a | b # union: {1, 2, 3, 4}
a & b # intersection: {2, 3}
a - b # difference: {1}
The hashing rule
Sets (and dictionary keys) require every element to be hashable ā meaning Python can compute a fixed "barcode" for it that never changes. Numbers, strings, and tuples are hashable because they're immutable. Lists are not, because their contents can change after the hash would have been computed, which would break the whole lookup mechanism.
bad_set = {[1, 2]} # TypeError: unhashable type: 'list'
good_set = {(1, 2)} # fine ā tuples are hashable
Python Code
def unique_pairs(records: list[tuple[str, int]]) -> set[tuple[str, int]]:
"""Return unique (name, age) records using a set for dedup."""
return set(records)
def shared_users(group_a: list[int], group_b: list[int]) -> set[int]:
"""Return user IDs present in both groups using set intersection."""
return set(group_a) & set(group_b)
def fast_lookup_demo(candidates: list[int], target: int) -> bool:
"""Membership check backed by a set for O(1) average lookup."""
seen = set(candidates)
return target in seen
Code Walkthrough
unique_pairsconverts a list of tuples into a set. Tuples are hashable, so this works and automatically drops duplicate records ā no manual loop needed.shared_usersconverts both lists to sets and uses&for intersection, replacing what would otherwise be a nested loop comparing every element of one list against every element of the other.fast_lookup_demobuilds a set once, then does the membership check within. That single line is O(1) on average instead of O(n), because the set hashestargetand jumps straight to its bucket instead of scanning.
Dry Run
Take shared_users([1, 2, 3, 4], [3, 4, 5, 6]).
set(group_a)ā{1, 2, 3, 4}set(group_b)ā{3, 4, 5, 6}&compares hash buckets rather than scanning element by element: both sets contain3and4.- Result:
{3, 4}.
Compare that to the list version, which would need a nested loop checking every element of group_a against every element of group_b ā 16 comparisons instead of near-instant hash lookups.
Complexity Analysis
- Set membership check: O(1) average time, because hashing maps a value directly to a bucket instead of scanning. Worst case O(n) if many values collide into the same hash bucket, but Python's hash design makes this rare in practice.
- Set intersection/union/difference: O(min(len(a), len(b))) roughly, since it iterates the smaller set and checks membership in the other.
- List membership check: O(n), since every element must be checked in the worst case.
- Space: O(n) for all three structures ā a set needs extra space internally for its hash table, but this is still linear in the number of elements.
These are correct because they follow directly from the underlying implementation: sets are backed by hash tables, lists are backed by contiguous arrays.
Alternative Solutions
frozensetā an immutable version of a set. Useful when you need set operations (union, intersection) but also need the result to be hashable itself, such as using a group of items as a dictionary key.- Dictionary instead of a set ā if you catch yourself building a set just to check "does this key exist," but you also need to store a value associated with each key, that's a sign you actually need a
dict, not aset. This is one of the most common misuses called out in the video.
Edge Cases
- Empty collection:
set()for an empty set ā{}gives you an empty dict instead, a classic trap. - Single-item tuple:
(5,)requires the trailing comma;(5)is just an integer. - Duplicate values in a set: silently ignored, no error.
- Unhashable elements: attempting
{[1, 2]}raisesTypeErrorimmediately. - Very large collections: this is exactly where the O(1) vs O(n) membership check gap becomes visible in real runtime.
Common Mistakes
- Using
{}expecting an empty set ā it's actually an empty dict. - Forgetting the trailing comma for a single-element tuple.
- Trying to store a list inside a set (or as a dict key) and getting a
TypeError. - Assuming a set preserves insertion order ā it doesn't guarantee any order.
- Using a list for repeated membership checks in a hot loop instead of converting to a set first.
- Reaching for a set when you actually need key-value pairing, which calls for a dictionary instead.
Interview Questions
- Why are tuples hashable but lists are not?
- When would you choose a
frozensetover a regular set? - What's the time complexity of checking membership in a list vs. a set, and why?
- How would you find duplicates in a list using a set?
- Why can a tuple be used as a dictionary key but a list cannot?
Similar Problems
- LeetCode 217 ā Contains Duplicate: directly tests set-based deduplication and membership checks.
- LeetCode 349 ā Intersection of Two Arrays: a direct application of set intersection.
- LeetCode 1 ā Two Sum: commonly solved using a dictionary (a natural extension once you understand hashing and sets).
- LeetCode 242 ā Valid Anagram: tests understanding of counting and set/dict tradeoffs.
Key Takeaways
Lists, tuples, and sets aren't interchangeable conveniences ā each one encodes a promise about your data. A tuple says "this is fixed." A set says "I only care about uniqueness and fast lookups, not order." A list says "I need full flexibility." Choosing correctly makes your code both correct and fast, and it makes the intent obvious to the next person reading it ā including future you.
Watch the Video
For the full walkthrough with live coding, including the list-in-a-set crash happening in real time, watch the video here: {LINK}
About the Series
This lesson is part of the Daily Python LeetCode series, where every video breaks down one core Python concept or interview problem ā building the intuition first, then the code ā so you understand why a solution works, not just how to type it out.
Call To Action
If this cleared up something that's been silently confusing you about Python's data structures, tap like on the video ā it's the easiest way to tell me it landed. Subscribe on YouTube so you don't miss the next lesson, and subscribe to the Substack for the full written breakdown delivered straight to your inbox. Drop a comment if you've been bitten by the list-in-a-set trap before, and share this with a fellow developer who defaults to lists for everything.
The solution
# s = {[1, 2]} -> error, lists are unhashable
s = {(1, 2)} # tuples work insteadReady to try it yourself? Solve Hashmap problems with instant feedback in the practice sandbox.
Practice now ā


