โ† back to journal

In Python, a set literal beats a tuple for "in" from two items up

2026-10-15 ยท Kit Wickham

People write if method in ("GET", "HEAD"): because a tuple of two feels too small to need a set. In Python 3.14, method in {"GET", "HEAD"} was as fast or faster in almost every case I measured. The tuple won only when the value was the very same object as the tuple's first item, which strings parsed from input never are.

The set costs nothing to build

The compiler turns a set literal of constants on the right of in into a frozenset constant. dis shows LOAD_CONST frozenset({'a', 'b'}), so no set is built per call. The tuple is a constant too. What's left to compare is one hash lookup against a left-to-right scan.

The test

Literal tuples and sets of 1 to 32 constants, looked up with timeit (2 million calls, best of 9, minus the cost of loading the variable alone), the median of 4 separate runs one after another, in Python 3.14.8 in a container pinned to one CPU. I tested three kinds of key. int: small ints, which Python caches, so the probe is the same object as the constant. same str: an interned string, the same object as the literal (as with an identifier or another literal). equal str: an equal string built at runtime, as from parsing a request or a file. Each was probed for the first item, the last item and a miss. Times are nanoseconds per test.

keyitemstuple, firsttuple, lasttuple, missset, any
int12.22.35.64.6โ€“5.0
int22.35.89.14.6โ€“4.9
int42.313.116.54.6โ€“5.0
int82.326.730.24.9โ€“5.1
int322.31161184.6โ€“5.0
same str12.22.25.43.7โ€“3.9
same str22.26.38.23.7โ€“3.8
same str42.115.014.53.8โ€“3.9
same str82.232.026.53.6โ€“3.9
same str322.11251283.8โ€“4.0
equal str16.56.55.44.1โ€“5.7
equal str26.710.68.43.7โ€“5.8
equal str46.619.014.53.8โ€“5.8
equal str86.535.726.53.6โ€“5.7
equal str326.51311283.9โ€“5.8

Run-to-run spread was at most 1.5 ns in any cell.

What it shows

The set is flat: 3.6 to 5.8 ns at every size. A miss is the cheapest case, since the probe's hash lands on an empty slot. A hit on an equal but different string costs about 2 ns more than a hit on the same object, because the set has to compare the characters after the identity check fails.

The tuple costs about 3.5 to 4 ns for each item it passes. Its one win is a hit on the first item when the probe is that same object: 2.2 ns, against the set's 3.7 to 5. Python's comparison checks identity before calling ==, so that case is a pointer compare. Move the hit to the second of two items, or miss, and the tuple is already slower. With strings built at runtime even the first-item hit loses (6.5 against 5.7), because the identity check fails and a full string compare follows.

So what

Write x in {"a", "b"} for membership tests against constants, even short ones. The tuple only pays off for a value you know arrives as the identical object and almost always matches the first item, and then it saves 1.5 to 3 ns. The bigger point: at 2 or 3 items none of this is worth a code review comment. It starts to add up with longer lists: at 8 items a tuple miss costs about 7 times a set lookup, and at 32 about 30 times.

Method: one machine, a 40-line timeit script, CPython 3.14.8 (default build, no JIT) in a container. Constant literals only. A set built from a variable at runtime is a different question, since building it costs far more than any of these lookups.