WasiLearn Code
Python · Data structures

Tuples and sets

A list is not the only way to hold several values. Two others turn up constantly, each solving a problem lists solve badly: a tuple is a list that cannot change, and a set is a collection with no duplicates and no order.

point = (3, 7)
letters = {"a", "b", "a"}

print(point)
print(sorted(letters))
print(type(point), type(letters))
output
(3, 7)
['a', 'b']
<class 'tuple'> <class 'set'>

The set silently dropped the repeated "a". That is not a bug — it is the whole point of a set.

That sorted() is not decoration either. A set has no order, and the order Python happens to print one in can differ between runs of the same program. Every set in this lesson is printed through sorted() for exactly that reason, and yours should be too whenever the output is going to be read.

Tuples cannot be changed

A tuple uses round brackets and, once made, is fixed.

sizes = ("small", "medium", "large")

print(sizes[0])
print(len(sizes))
print("large" in sizes)
output
small
3
True

Reading works exactly like a list. Writing does not:

sizes = ("small", "medium", "large")
sizes[0] = "tiny"
TypeError: 'tuple' object does not support item assignment

That immutability is the reason to choose one. When a group of values belongs together and must not drift — a coordinate, a colour, a database row, a configuration constant — a tuple says so in the type itself, and any code that tries to modify it fails immediately rather than quietly corrupting your data.

The one-element trap

This is the tuple gotcha, and it is invisible.

not_a_tuple = ("chai")
actually_a_tuple = ("chai",)

print(type(not_a_tuple))
print(type(actually_a_tuple))
print(len(actually_a_tuple))
output
<class 'str'>
<class 'tuple'>
1

Brackets alone do not make a tuple — the comma does. ("chai") is just a string with redundant brackets around it, exactly like (5) is just a number.

The trailing comma looks like a typo and is required. It is also legal on longer tuples, where it makes adding a line in version control a one-line change:

sizes = (
    "small",
    "medium",
    "large",
)
print(len(sizes))
output
3

Brackets are optional in many places, which is why functions returning several values look the way they do:

def min_max(numbers):
    return min(numbers), max(numbers)

result = min_max([4, 9, 2])
low, high = min_max([4, 9, 2])

print(result)
print(low, high)
output
(2, 9)
2 9

That second line is unpacking: the tuple is taken apart into named variables in one step. It is why for key, value in d.items() works, and it is one of the nicest things about Python.

a, b = 1, 2
a, b = b, a
print(a, b)
output
2 1

Swapping two variables without a temporary, because the right-hand side is built as a tuple before anything is assigned.

Sets have no duplicates and no order

A set uses curly brackets and keeps each value once.

orders = ["chai", "coffee", "chai", "lassi", "chai"]
unique = set(orders)

print(sorted(unique))
print(len(unique))
print(len(orders))
output
['chai', 'coffee', 'lassi']
3
5

Removing duplicates is the everyday use, and set() does it in one call. To get a list back, wrap it:

orders = ["chai", "coffee", "chai", "lassi"]

print(sorted(set(orders)))
output
['chai', 'coffee', 'lassi']

sorted matters, because a set has no order at all. You cannot index into one, and the order you see when printing is not something to rely on:

letters = {"c", "a", "b"}
letters.add("d")
letters.discard("a")

print("b" in letters)
print(len(letters))
output
True
3

add puts one item in, discard takes one out without complaining if it was never there. remove does the same but raises KeyError on a missing item — use whichever failure you actually want.

Membership testing is the other reason

Checking whether something is in a list means walking it. Checking a set is effectively instant, however large it gets.

banned_list = ["a", "b", "c"]
banned_set = {"a", "b", "c"}

print("c" in banned_list)
print("c" in banned_set)
output
True
True

Identical answers, very different cost at scale. If you are checking membership repeatedly against a collection of any size, build a set once and test against that.

Set arithmetic

Sets do the operations you drew as overlapping circles at school, and they read well.

monday = {"Ayesha", "Bilal", "Sana"}
tuesday = {"Bilal", "Sana", "Kamran"}

print(sorted(monday & tuesday))
print(sorted(monday | tuesday))
print(sorted(monday - tuesday))
print(sorted(monday ^ tuesday))
output
['Bilal', 'Sana']
['Ayesha', 'Bilal', 'Kamran', 'Sana']
['Ayesha']
['Ayesha', 'Kamran']

& is both days, | is either day, - is Monday only, ^ is exactly one of the two. The named methods intersection, union, difference and symmetric_difference do the same and are worth using when the line is long.

Answering "who came both days?" with two nested loops is ten lines and a bug. It is monday & tuesday.

The empty set problem

empty_dict = {}
empty_set = set()

print(type(empty_dict))
print(type(empty_set))
output
<class 'dict'>
<class 'set'>

{} was taken by dictionaries first, so an empty set has to be written set(). There is no shorter spelling.

What can go in a set

Only hashable values, which in practice means immutable ones. Strings, numbers and tuples are fine. Lists are not.

ok = {(1, 2), "chai", 7}
print(len(ok))
output
3
bad = {[1, 2]}
TypeError: unhashable type: 'list'

The same rule governs dictionary keys, and for the same reason: the collection finds items by hashing them, and a value that can change would go missing the moment it did. This is one of the practical reasons tuples exist — a tuple can be a key or a set member, a list cannot.

Choosing

  • Order matters and it will change → list.
  • A fixed group of related values → tuple.
  • Uniqueness, or fast "is it in here?" → set.
  • Looking things up by name → dictionary.

Next: list comprehensions, which build all of these in a line.

Prefer learning with a teacher?

WasiLearn Academy runs small-group classes covering this material, online and in Karachi.

See the classes