Tuples and Sets
A tuple is a pair you should not change. A set holds unique names, like allowed tools. Empty set is set(), not {}.
A tuple is a row that cannot change after you make it. You write it with parentheses: (tool, args). Use a tuple for a pair you should not edit: a parsed tool name plus its arguments, a (ok, body) result, a point that should stay still.
A set is a bag of unique names. Unique means each name appears once. A set has no order. Use a set for an allowlist: the tools the agent is allowed to run. Membership tests (name in allowed) are the point.
A set drops duplicates. Use it as an allowlist. Keep the step log as a list so order stays.
Unique names, no extrasLists stay for logs. Tuples stay for fixed pairs. Sets stay for uniqueness and membership. If you mix those jobs, you will lose order or gain accidental duplicates.
Tuples: a pair you do not edit
You can read a tuple by index, like a list. You cannot append. You cannot replace an item. pair[0] = "read" raises TypeError. That is the point.
Unpack means split the pair into two names:
pair = (class="tok-s">"search", class="tok-s">"weather nyc")
tool, args = pair
print(tool)
print(args)
print(pair[0])The number of names must match. a, b = (1, 2, 3) raises ValueError. a, b, c = (1, 2) also fails. Unpack only when you know the length.
A function that gives back two values is really giving back a tuple. return ok, hits is return (ok, hits). The caller writes ok, hits = call_search(q).
A one-item tuple needs a comma: (3,) is a tuple. (3) is just the number 3 with parentheses. That surprise bites people who write (name) and think they made a tuple.
You cannot change the tuple's slots. If a slot holds a list, that inner list can still change. For tool names and argument text, this does not come up. Do not put a mutable log inside a “frozen” tuple and expect the log to freeze.
Sets: unique names
Write a set with curly braces: {"search", "read"}. An empty set is set(), not {}. {} is an empty dict (the next lesson). type({}) is dict. type(set()) is set. Print both once so the trap sticks.
Duplicates disappear. {"search", "search"} is just {"search"}. set(["search", "read", "search"]) is two names.
"search" in allowed asks if that name is allowed. That is the allowlist check.
| Code | Meaning | |
|---|---|---|
set() | Empty set | |
{"search", "read"} | Two allowed names | |
name in allowed | Is this name allowed? | |
set(raw) | Unique names from a list | |
left - right | In the left set, not the right | |
left & right | In both sets | |
| `left | right` | In either set |
allowed.add("write") | Add a name (changes the set) |
Sets have no order. Do not use allowed[0]. That is a TypeError. Print with sorted(allowed) if you want a stable order. sorted returns a list. The set is unchanged.
Do not use a set as the step log. You would lose order and repeated steps. Both matter in a log. Keep the log as a list. Use a set at the gate: "is this tool allowed?"
allowed = {class="tok-s">"search", class="tok-s">"read"}
print(class="tok-s">"write" in allowed) class="tok-c"># False
raw = [class="tok-s">"search", class="tok-s">"read", class="tok-s">"search"]
print(sorted(set(raw))) class="tok-c"># ['read', 'search']
print(sorted(set(raw) - allowed)) class="tok-c"># extra names not allowedadd changes the set in place, like append on a list. allowed.add("write") returns None. Use the set after you add. Frozen sets exist (frozenset) when you need a set that cannot change. You do not need them to write an allowlist constant. A normal set assigned once at the top is enough for this track.
A tuple can be a dict key because it does not change. A list cannot. You will rarely need that in an agent, but it explains why a parsed (name, q) pair is a tuple if you want to count unique calls with a Counter. Convert with tuple(row) only when the row itself should not grow.
When you load tool names from a config list, duplicates happen. allowed = set(raw_names) is the cleanup. Then name in allowed is the gate. Print sorted(allowed) at startup so a human can audit the gate. Do not print a set directly if you need a stable test; order is not promised.
A tuple of one tool name is (name,) with a comma. Without the comma you have just the name. If unpacking fails with “not enough values,” you probably forgot the comma or you unpacked a string into two names: "ab" unpacks to 'a', 'b' because a string is a row of characters. Tool names should stay strings. Unpack tuples you built, not random text.
Common mistakes
- Writing
{}for an empty set. - Indexing a set.
- Using a set as a transcript.
- Unpacking the wrong number of values.
- Forgetting the comma in a one-item tuple.
- Comparing two sets with
==when you meant “same names” — that part is actually fine;==on sets ignores order. Do not compare a set to a list with==and expect True.
Run to execute this in your browser. Nothing is sent to a server.
Add "write" to allowed in the editor and run again. blocked should shrink. That is the allowlist working.
How agents use this
A tool call can be a pair (name, args) you should not edit after you parse it. Unpack it, look up the function, call it. If you later mutate args while retrying, you no longer know what the model asked for. Keep the original pair. Build a new args dict if you must fill defaults.
A set of allowed names is a safety gate. If the model asks for a tool that is not in the set, you reject it. You do not run it. You append an observation like “unknown tool.” Keep the step log as a list. Use the set only to check names.
When you load a config list of tools that might contain duplicates, sorted(set(names)) is a clean print and a unique gate. When you compute “tools the model used that we never allowed,” that is a set difference. Those two lines are half of a simple policy.
Check your understanding