JJoeven

Curriculum/Python

Unpacking

Split a pair into names, peel the rest of a list, merge dicts with **, and unpack a tool result without losing defaults.

beginner20 min18 / 37

Unpacking means “take a row apart and give each piece a name.” You already pass a dict into a call with **args. This lesson also splits lists, merges dicts, and unpacks what a tool returns.

The point is readable names. name, args = pair is clearer than pair[0] and pair[1] everywhere. If the row is the wrong length, unpacking crashes loudly. That is better than silently using the wrong slot.

Split a pair into names
searchargs

name, args = pair. Then call the function with those names.

Split a pair into names

Agents unpack at three edges: a parsed (name, args) pair, a merged settings dict, and sometimes an (ok, data) result. Get those three right and you will stop writing pair[0] in five places.

What you will learn

  • a, b = pair
  • first, *rest = row
  • {a, b} to merge dicts
  • **kwargs in calls
  • Swap two names
  • Unpack a tool result

Split a pair

If a value has two parts, you can name both at once.

python
pair = (class="tok-s">"search", {class="tok-s">"q": class="tok-s">"weather"})
name, args = pair
print(name)
print(args)

The number of names must match the number of parts. a, b = ["think"] raises ValueError. a, b, c = pair also fails. Check len if the data comes from a model. After a parser you trust, unpack freely.

You can unpack a list the same way. Tuples are the usual pair type because they signal “do not append.”

Nested unpacking exists: (tool, (q, k)) = .... Skip it until the shape is boring and tested. Nested unpacking of messy JSON is a gift to bugs.

Unpacking a string is legal and usually wrong: a, b = "ab" sets a to "a". Tool names should stay whole strings. If you see a ValueError: too many values to unpack, you may have unpacked text by accident.

First and the rest

A star on the left keeps leftover items in a list.

python
steps = [class="tok-s">"think", class="tok-s">"act", class="tok-s">"observe"]
first, *rest = steps
print(first)  class="tok-c"># think
print(rest)   class="tok-c"># ['act', 'observe']

This is handy for a transcript: the first message vs everything after. rest is always a list, even if it has one item or zero items. head, tail = ["only"] sets tail to [].

You can also write *start, last = steps to peel the last item. Useful for “final answer vs the path.” Do not peel from both ends in one line if you cannot say the lengths out loud.

PatternMeaningAgent use
a, b = pairTwo names from a pairtool name plus args
first, *rest = rowFirst item, then a list of the restsystem prompt vs later turns
*start, last = rowAll but last, then lastpath vs final answer
{a, b}Merge dicts; b wins on clashesdefaults, then model args
fn(**d)Unpack a dict into named inputscall_tool
left, right = right, leftSwaprare; same “right side first” rule

A starred list in a call is the positional cousin of *: fn(row) turns a list into ordered arguments. You will use this less than ** for tools, because tool inputs have names.

Swap

Python reads the right side first, then binds the names. That is why swap needs no extra box.

python
left = class="tok-s">"user"
right = class="tok-s">"assistant"
left, right = right, left
print(left, right)

The right side builds a tuple (right, left) using the old values. Then unpacking writes the new names. You will rarely swap roles in an agent. The same rule explains why step, budget = step + 1, budget - 1 works: right side first.

Merge dicts

{a, b} copies a, then copies b on top. If both have the same key, the later dict wins.

python
base = {class="tok-s">"model": class="tok-s">"tiny", class="tok-s">"temp": 0}
extra = {class="tok-s">"temp": 0.2, class="tok-s">"max_tokens": 64}
merged = {**base, **extra}
print(merged)

temp becomes 0.2. Agent use: start with defaults, then overlay the model’s arguments. The original base is unchanged. You built a new dict. That is important. Do not mutate the default dict you keep for the next call.

Later Python also has base | extra for dict merge. {base, extra} is enough here and works the same idea: new dict, later keys win.

If merge order is wrong, defaults overwrite the model. {args, defaults} would ignore the model’s temp. You almost never want that. Write defaults first, overlay second. Print the merged dict in a test. That test has no model. It still saves you.

Unpack in a call, and unpack a result

In a call, **d turns a dict into keyword arguments.

A tool can return a pair. Unpack that pair the same way.

python
def call_search(q):
    ok = True
    hits = [class="tok-s">"doc 0 about " + q]
    return ok, hits

ok, hits = call_search(class="tok-s">"rain")
print(ok, hits)

fn(**args) raises TypeError if the dict has a key the function does not accept. Check the keys before you unpack. A small allowlist of argument names is a good filter: build a new dict with only q and k, then unpack that.

Returning a dict is often clearer than a pair: {"ok": True, "hits": ...}. Then you do not unpack; you .get. Pairs are fine when the two fields are obvious and stable.

Walkthrough: filter keys, then unpack

Models send extra keys. A strict search(q, k=3) will TypeError on {"q": "rain", "k": 1, "pretty": True}. Filter first.

python
def only(keys, data):
    out = {}
    for key in keys:
        if key in data:
            out[key] = data[key]
    return out

raw = {class="tok-s">"q": class="tok-s">"rain", class="tok-s">"k": 1, class="tok-s">"pretty": True}
print(only((class="tok-s">"q", class="tok-s">"k"), raw))

Then search(**only(("q", "k"), raw)) is safe. Unknown keys stay out. Missing required keys still raise, which is what you want when q never arrived.

fn(args) vs fn(**args) is a different bug. Without stars, the whole dict becomes the first positional argument. search then sees q as a dict and concatenates badly, or raises. The star is the difference between “one mapping” and “named inputs.”

What goes wrong

  • Wrong number of names (ValueError).
  • fn(args) vs fn(**args).
  • Mutating base instead of merging into a new dict.
  • Unpacking a dict like a pair: a, b = {"q": 1, "k": 2} unpacks keys, not values, and the order is not a pair you chose. Do not do that.
  • Unpacking a string into characters.
  • {args, defaults} so defaults win and the model is ignored.
  • Unpacking before you know parse succeeded: ** on an error string.

_ as a throwaway name is a convention: ok, _ = call_search(q) when you do not need hits. It still must match the length. It does not mean “ignore extras.” For extras you need *rest.

Do not mix args in a def with rest in assignment in your head: one collects extras in a function, the other peels a row. The star is the same character. The place is the meaning.

Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

Change the first step in the list and run again. first should follow. rest should still be a list. Compare merged with wrong order: only the first should let temp stay 0.2.

How agents use this

A tool call is often a pair: name plus args. Unpack it, then call fn(args). Merge default settings with the model’s dict using {defaults, *args}. Split a transcript with first, rest when the first row is special. If a tool returns (ok, data), unpack it so the loop can branch on ok.

Defaults should sit on your side, not in the model’s JSON. If the model omits temp, the merge keeps 0. If the model sends temp, the overlay wins. That is a policy you can test without a model: merge two dicts, assert the result.

Be strict at the call boundary. Unpack only after the parser promised a dict of args. If parse failed, do not ** an error string. The TypeError would hide the parse error. Branch on ok first, then unpack.

Filter unknown keys at the executor. Passing them blindly is how a renamed field becomes a crash in the loop. A small only(("q", "k"), args) is more honest than a **kwargs sink that swallows typos.

When the first transcript row is a system message, first, rest = messages lets you always send first and slice rest[-8:]. That is unpack plus slice, not a new idea. If the list is empty, unpacking first, rest raises ValueError. Guard with if not messages: return. Empty logs are a real startup state.

Check your understanding

After first, *rest = ["think", "act", "observe"], what is rest?