List, dict, set, and tuple reference
Every method on Python's four core collections, when to use each type, and their performance characteristics.
Why this matters in AI / ML / GenAI
Choosing the wrong collection is the most common cause of slow Python. Checking membership in a list is a full scan; in a set it is instant. On a million-row dataset that is the difference between seconds and hours.
Choosing the right collection
- list — ordered, mutable, allows duplicates. The default when you need a sequence.
- tuple — ordered, immutable. Use for fixed records and as dictionary keys, which lists cannot be.
- dict — key to value mapping, insertion-ordered since Python 3.7, keys must be hashable.
- set — unordered, unique elements, extremely fast membership tests.
Performance is the deciding factor:
| Operation | list | dict / set |
|---|---|---|
x in collection | O(n) scan | O(1) hash lookup |
| append / add | O(1) | O(1) |
| insert or delete at front | O(n) | — |
If you find yourself writing if item in big_list inside a loop, convert to a set first. Use collections.deque when you need fast appends and pops at both ends.
List and tuple methods
List: append, extend, insert, remove, pop, clear, index, count, sort, reverse, copy.
sort() sorts in place and returns None; sorted() returns a new list. Writing items = items.sort() sets items to None, and it is a classic beginner bug.
append(x) adds one element; extend(seq) adds each element of a sequence. append on a list gives you a nested list.
Tuple has only count and index — everything else would mutate. Tuples unpack cleanly (a, b = pair) and support starred unpacking (first, *rest = items).
Copying: copy() and list(x) are shallow, so nested objects are still shared. Use copy.deepcopy for nested structures.
Dict and set methods
Dict: get, keys, values, items, pop, popitem, update, setdefault, clear, copy, fromkeys.
get(key, default) never raises, which is the safe way to read config. setdefault(key, []) initialises a missing key in one step, though collections.defaultdict is cleaner when you do it repeatedly.
Merge with {**a, **b} or a | b (Python 3.9+); the right side wins on conflicts.
Set: add, remove (raises if missing), discard (silent), pop, union (|), intersection (&), difference (-), symmetric_difference (^), issubset, issuperset, isdisjoint.
Set operations are the fastest way to compare two collections: which users are in both cohorts, which features are missing from the new dataset, which tokens are out of vocabulary.
Note that {} creates an empty dict; use set() for an empty set.
Copy-paste examples
Copy into your own editor, or load one into the compiler below and press Run.
List methods and the sort-in-place trap
sort() returns None. sorted() returns a new list.
items = [3, 1, 4, 1, 5]
items.append(9)
items.extend([2, 6])
items.insert(0, 0)
print("after append/extend/insert:", items)
items.remove(1) # removes the FIRST 1 only
popped = items.pop() # last element
print(f"after remove(1) and pop() -> {popped}:", items)
print("index of 4:", items.index(4), "| count of 1:", items.count(1))
new_list = sorted(items, reverse=True)
print("\nsorted() returns a list:", new_list)
items.sort()
print("sort() mutates in place :", items)
print("sort() returns :", [1, 2].sort(), "<- never assign this")
print("\nappend vs extend:")
a, b = [1, 2], [1, 2]
a.append([3, 4])
b.extend([3, 4])
print(" append:", a)
print(" extend:", b)Tuples, unpacking, and why immutability matters
Tuples can be dict keys; lists cannot.
point = (12.5, 48.2)
x, y = point
print(f"unpacked: x={x} y={y}")
record = ("mini", 0.91, 620, "2026-09-04")
name, score, *rest = record
print("starred unpacking:", name, score, rest)
cache = {}
cache[("mini", "en")] = 0.91
cache[("large", "fr")] = 0.88
print("\ntuple as dict key:", cache[("mini", "en")])
try:
cache[["mini", "en"]] = 0.5
except TypeError as err:
print("list as key ->", err)
print("\ntuple has only two methods:", [m for m in dir(tuple) if not m.startswith("_")])
print("swap without a temp variable:")
a, b = 1, 2
a, b = b, a
print(" a =", a, "b =", b)Dict methods in practice
get for safe reads, setdefault for grouping, | to merge.
config = {"model": "gpt-4.1-mini", "temperature": 0.2}
print("get present :", config.get("model"))
print("get missing :", config.get("top_p"), "(None, no error)")
print("get default :", config.get("top_p", 1.0))
defaults = {"temperature": 0.7, "top_p": 1.0, "max_tokens": 512}
merged = defaults | config # right side wins
print("\nmerged:", merged)
grouped = {}
for model, topic in [("mini", "rag"), ("large", "agents"), ("mini", "eval")]:
grouped.setdefault(model, []).append(topic)
print("\ngrouped with setdefault:", grouped)
config.update({"stream": True, "temperature": 0.5})
print("\nafter update:", config)
print("popped:", config.pop("stream"), "| remaining keys:", list(config.keys()))
print("\niterate items:")
for key, value in merged.items():
print(f" {key:<12} {value}")
print("\ncomprehension filter:", {k: v for k, v in merged.items() if isinstance(v, float)})Set operations for comparing datasets
The fastest way to diff two collections.
train_features = {"tokens", "latency", "model", "region", "user_id"}
serving_features = {"tokens", "latency", "model", "device"}
print("in both :", sorted(train_features & serving_features))
print("training only :", sorted(train_features - serving_features), "<- missing at serving time")
print("serving only :", sorted(serving_features - train_features), "<- unseen by the model")
print("either :", sorted(train_features | serving_features))
print("mismatched :", sorted(train_features ^ serving_features))
print("\nsubset? ", serving_features <= train_features)
print("disjoint?", train_features.isdisjoint({"foo", "bar"}))
s = {1, 2, 3}
s.add(4)
s.discard(99) # silent when missing
print("\nafter add/discard:", s)
try:
s.remove(99)
except KeyError:
print("remove(99) raised KeyError — use discard to be safe")
print("\ndeduplicate while keeping order:", list(dict.fromkeys([3, 1, 3, 2, 1])))
print("{} is a dict:", type({}).__name__, "| set() is a set:", type(set()).__name__)Why the collection type matters for speed
Same logic, very different cost as the data grows.
import time
n = 40_000
haystack_list = list(range(n))
haystack_set = set(haystack_list)
needles = range(0, n, 1000)
start = time.perf_counter()
found_list = sum(1 for x in needles if x in haystack_list)
list_time = time.perf_counter() - start
start = time.perf_counter()
found_set = sum(1 for x in needles if x in haystack_set)
set_time = time.perf_counter() - start
print(f"list scan: {list_time * 1000:8.3f} ms ({found_list} found)")
print(f"set hash: {set_time * 1000:8.3f} ms ({found_set} found)")
print(f"set is roughly {list_time / max(set_time, 1e-9):,.0f}x faster here")
print("\nthe gap grows linearly with the size of the collection")Pick the right collection for each job
Try it — in-browser Python
Change the cohort lists and every set result updates automatically.
Output
Python runs in your browser. First run downloads the runtime.
Press Run (or Ctrl+Enter) to execute.
CPython in WebAssembly. Stdlib works. NumPy, pandas, scikit-learn and Matplotlib load on demand, and charts render below. No input(), no GPU, no network installs.
Takeaways
- Use a set for membership tests — list scans are O(n) and dominate large loops.
- sort() mutates and returns None; sorted() returns a new list.
- dict.get and setdefault avoid KeyError; set operators diff two collections instantly.