Two more containers round out the basics, and each fills a job a list does poorly. A set stores unique items and throws duplicates away; a tuple is an ordered, immutable record - like a list you cannot change after building it.
tags = {"python", "code", "python"} # {'python', 'code'}
point = (3, 4) # a fixed pair
A list keeps every value in order and lets you edit it, which is wonderful until you do not want repeats, or until a value should be locked in place. Use a set to remove repeats or test membership in a flash; use a tuple for things that belong together and should not be edited - a coordinate, a fixed row of values, several results handed back from one function.
flowchart TD
A["[1, 1, 2, 3, 3]"] --> S["set(...)"]
S --> B["{1, 2, 3}"]
style S fill:#3776ab,color:#fff
Sets: uniqueness and fast membership
A set's defining trick is that it holds each value at most once. Wrap a list in set(...) and the repeats simply vanish, which is the easiest way to dedupe anything. The other gift is speed: testing x in some_set is effectively instant, even when the set holds millions of items, because a set looks items up by hash rather than scanning one by one. A list has to check every element until it finds a match.
Sets are mutable - you grow and shrink them with .add() to insert and .remove() or .discard() to delete. The difference matters: .discard() stays quiet if the item is absent, while .remove() raises an error. The price for all that hashing magic is that a set is unordered (you cannot rely on the order you visit its items, and s[0] raises a TypeError) and it only accepts hashable values.
tags = ["python", "code", "python", "fun", "code"]
unique = set(tags)
print(sorted(unique))
print(len(unique))
print("python" in unique)
point = (3, 4)
x, y = point
print("x =", x, "y =", y)
Set operations: combining groups
Because sets model mathematical sets, they come with the operations you remember from that class. The union a | b holds everything in either set; the intersection a & b holds only what appears in both; the difference a - b holds what is in a but not b. Each builds a fresh set and leaves the originals alone.
These turn awkward loops into a single expression. "Which tags do these two articles share?" becomes set(tags_a) & set(tags_b). "Which customers bought but never left a review?" becomes buyers - reviewers. Whenever a question is really about overlap or difference, reach for a set before you reach for a loop.
flowchart LR
A["{1, 2, 3}"] --> U["union<br/>everything in either"]
B["{3, 4, 5}"] --> U
U --> R1["{1, 2, 3, 4, 5}"]
A --> I["intersection<br/>only the overlap"]
B --> I
I --> R2["{3}"]
style U fill:#3776ab,color:#fff
style I fill:#b45309,color:#fff
a = {1, 2, 3}
b = {3, 4, 5}
print("union:", a | b) # {1, 2, 3, 4, 5}
print("intersection:", a & b) # {3}
print("difference:", a - b) # {1, 2}
print(4 in b) # True - fast membership
Tuples: ordered and immutable
A tuple is the fixed cousin of a list. Once you build (3, 4), you can read point[0] and ask for len(point), but you cannot reassign a position - there is no .append() and no in-place sort. That rigidity is a feature, not a limitation: a tuple promises its values will not change, which makes it safe to use as a dictionary key or a set member (lists cannot serve either role, because they are mutable). Tuples are also how a function returns several values at once - return low, high packs two results into one tuple the caller can unpack.
point = (3, 4)
print(point[0]) # 3
print(len(point)) # 2
# point[0] = 5 # TypeError - tuples are immutable
flowchart LR
T["tuple ('Ada', 36, 'London')"] --> P["unpack into names"]
P --> N1["name = 'Ada'"]
P --> N2["age = 36"]
P --> N3["city = 'London'"]
style P fill:#3776ab,color:#fff
Unpacking, and choosing the right container
Tuples shine with unpacking - assigning each position to its own name in a single line, which beats indexing for readability:
name, age, city = ("Ada", 36, "London")
Picking the container comes down to what you need. Reach for a list when order matters and the contents will change. Reach for a set when you want uniqueness or fast membership tests. Reach for a tuple when the values form a fixed record that should not move - a coordinate, an RGB colour, a pair of results. Matching the container to the job makes your intent obvious to the next reader, instead of leaving them to guess why you reached for a list yet again.
Why some values can live in a set and others cannot
A set and a dictionary key share one rule: the value must be hashable, meaning Python can compute a single fixed number that identifies it. That number is the index the set uses to file the value away. A hash only works when the value can never change, because a change would move it to a different slot and leave the old lookup stranded.
That is why an immutable value qualifies and a list does not. A list can be appended to or reordered at any moment, so its hash would be a moving target. A tuple, locked at creation, keeps a stable hash - which is why it passes the same test a list fails.
Write unique_sorted(values) that returns a sorted list of the unique values. (Example: unique_sorted([3, 1, 2, 3, 1]) → [1, 2, 3].)
def unique_sorted(values):
# use set(...) to dedupe, then sorted(...)
pass
set(values)removes the duplicates.sorted(...)returns a sorted list.
def unique_sorted(values):
return sorted(set(values))
Write in_both(list_a, list_b) that returns a sorted list of the values that appear in BOTH lists. Use set intersection (&) to find the overlap.
def in_both(list_a, list_b):
# set(list_a) & set(list_b), then sorted(...)
pass
set(list_a) & set(list_b)keeps only the shared values.Wrap that in
sorted(...)to return a list, not a set.
def in_both(list_a, list_b):
return sorted(set(list_a) & set(list_b))
What does this print? A tuple is unpacked into three names.
person = ("Ada", 36, "London")
name, age, city = person
print(name)
print(age)
print(city)
Unpacking assigns position by position.
name ← 'Ada', age ← 36, city ← 'London'.
single() is meant to return a one-item tuple containing the number 42, but return (42) just returns the bare number - parentheses group an expression, they do not make a tuple. Add the trailing comma that tells Python this really is a tuple.
def single():
return (42)
A one-item tuple needs a comma after the value.
Write (42,) - the comma is what makes the tuple, not the parentheses.
def single():
return (42,)
A function can pack several values into one tuple and let the caller unpack them. Write min_max(numbers) that returns the smallest and largest values as a tuple (low, high) - return them separated by a comma, with no parentheses. Assume numbers is a non-empty list.
def min_max(numbers):
# return two values separated by a comma
pass
min(numbers)andmax(numbers)give the two ends.Return them as
low, high- Python packs that into a tuple automatically.
def min_max(numbers):
return min(numbers), max(numbers)
Recap
- A set holds unique, unordered items - wrap a collection in
set(...)to dedupe, and test membership withinat hash speed. - Sets combine with
|(union),&(intersection), and-(difference). - A tuple is ordered and immutable - safe as a dictionary key, ideal for fixed records.
- Unpack a tuple into names in one line:
name, age = record. - Mind the traps:
(5,)not(5)for a one-item tuple, and sets hold only hashable values.
Next you will store key-value pairs in dictionaries, where tuples often serve as keys.