Tuples and sets, and the cost of asking is this in there
A tuple is a list that cannot change
point = (12.9716, 77.5946)
point[0] # 12.9716
point[0] = 0 # TypeError: 'tuple' object does not support item assignmentRound brackets, or often no brackets at all — the comma is what makes a tuple:
row = "ADD-01", "rice", 340The one piece of syntax to remember: a single-item tuple needs a trailing comma. (5) is just the number five in brackets; (5,) is a one-element tuple. This causes a specific, confusing bug when a function expects a tuple and receives a number.
Unpacking is where tuples earn their place:
lat, lon = point
name, qty, price = row
a, b = b, a # swap, with no temporary variable
first, *rest = [1, 2, 3, 4] # first = 1, rest = [2, 3, 4]Functions that return several values return a tuple, and you unpack it on arrival. That is why divmod(17, 5) gives (3, 2).
When to prefer a tuple
Use a tuple when the number of items is fixed and their positions mean something — a coordinate, a date triple, a database row. Use a list when items are the same kind of thing and the count varies.
There is also a mechanical reason. Tuples are hashable, so they can be dictionary keys and set members. Lists cannot:
routes = {("BLR", "DEL"): 1740} # fine
routes = {["BLR", "DEL"]: 1740} # TypeError: unhashable type: 'list'A hash is computed from the contents. If the contents could change, the stored hash would go stale and the key would become unfindable, so Python forbids mutable objects as keys rather than letting you create a corrupt dictionary.
A set holds unique items and answers one question fast
tags = {"python", "ai", "python"}
tags # {'ai', 'python'} — the duplicate is gone
"ai" in tags # True
tags.add("data")
tags.discard("ai") # remove() raises if absent; discard() does notSets have no order. Printing one twice in the same run gives the same order; across runs with strings it can differ, because string hashing is randomised per process for security. Never rely on the order of a set.
Set arithmetic reads like the mathematics:
a = {1, 2, 3}
b = {3, 4}
a | b # union {1, 2, 3, 4}
a & b # intersection {3}
a - b # difference {1, 2}
a ^ b # in one but not both {1, 2, 4}"Which customers are in both files" is one operator, not a nested loop.
The performance difference is not small
x in some_list compares x against each element until it finds a match. Average cost grows with the length of the list. x in some_set computes one hash and looks in one bucket, at effectively constant cost regardless of size.
Measure it:
python3 -m timeit -s "xs=list(range(100000))" "99999 in xs"
python3 -m timeit -s "xs=set(range(100000))" "99999 in xs"On an ordinary laptop the list is roughly 500–900 microseconds and the set roughly 0.03 microseconds. That is four orders of magnitude. A script that checks 10,000 items against a 100,000-item list does ten seconds of work; against a set it does a few milliseconds.
If your code has a loop containing if item in big_list, converting big_list to a set once, before the loop, is often the entire optimisation.
Deduplication, and the thing it costs you
unique = set(names) # loses the order
unique = list(dict.fromkeys(names)) # keeps first-seen orderDictionaries have preserved insertion order since Python 3.7, so dict.fromkeys is the standard order-preserving dedupe. Reach for it whenever the output is going in front of a person, because a shuffled list of names looks like a bug.
Sets also cannot contain lists, for the same hashability reason as dictionary keys. A set of coordinates works if they are tuples and fails if they are lists — a real and confusing error the first time.
The empty-set trap
{} is an empty dictionary, not an empty set. The literal for an empty set is set(). There is no shorter form, because the braces were taken by dictionaries first.
Try this now
Take two lists of email addresses from two files. In three lines, find the ones in both, the ones only in the first, and the total number of distinct addresses. Then write the same thing with nested loops and compare how long each is to read.
The one thing to keep
A set answers membership in constant time where a list scans, so converting a large list to a set once before a loop can turn seconds into milliseconds.
Before you move on
A script loops over 50,000 order IDs and, for each, checks `if order_id in cancelled` where `cancelled` is a list of 80,000 IDs. It takes about 40 seconds. Which change addresses the actual cost?
Pick the one you would defend. Nobody sees your answer.