Addaly is in open beta. Things will change, and AI answers can be wrong — check anything that matters.

Python, From Zero, For AI

From your first line of code to your first API call.

Lesson 14 of 898 min

Tuples and sets, and the cost of asking is this in there

A tuple is a list that cannot change

python
point = (12.9716, 77.5946)
point[0]        # 12.9716
point[0] = 0    # TypeError: 'tuple' object does not support item assignment

Round brackets, or often no brackets at all — the comma is what makes a tuple:

python
row = "ADD-01", "rice", 340

The 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:

python
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:

python
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

python
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 not

Sets 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:

python
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:

bash
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.

Time for one membership check as the collection grows02500500025000400000Items in the collectionMicroseconds for one check—— x in some_list– – x in some_setThe list line is straight because in scans until it finds a match: on average half the items. The setline is flat at about 0.03 microseconds however large the set gets, because it hashes once and looksin one bucket. If a loop of yours contains if item in big_list, converting big_list to a set beforethe loop is often the whole optimisation.
Time for one membership check as thecollection grows02500500025000400000Across: Items in the collectionUp: Microseconds for one check—— x in some_list– – x in some_setThe list line is straight because in scans until itfinds a match: on average half the items. The setline is flat at about 0.03 microseconds howeverlarge the set gets, because it hashes once and looksin one bucket. If a loop of yours contains if itemin big_list, converting big_list to a set before theloop is often the whole optimisation.

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

python
unique = set(names)             # loses the order
unique = list(dict.fromkeys(names))   # keeps first-seen order

Dictionaries 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.

No ads. No data sale. No public scores on people. Ever.

© 2026 Addaly