Skip to main content
CodeOath
← All posts

Python105 min total · 18 parts

Python Fundamentals for Interviews: Data Structures, Comprehensions, and Gotchas

Part 6 of 18 · ~3 min

args, kwargs, and Unpacking

The watcher is about to grow a second job — emitting metrics, not just printing alerts — and the natural shape for that is a function that accepts whatever the caller wants to attach:

def record_metric(name, *values, **tags):
    print(name, values, tags)

record_metric("request_duration_ms", 118, endpoint="/api/search", status=200)
# request_duration_ms (118,) {'endpoint': '/api/search', 'status': 200}

Nothing about the names args and kwargs is special — call them values and tags, as above, and Python doesn't care in the slightest. What actually does the work is the single and double star in front of the parameter, gathering up whatever positional and keyword arguments weren't otherwise claimed. record_metric is a preview of the pattern decorators are about to lean on constantly: a function that has to accept a call shape it knows nothing about in advance and pass it straight through to something else.

Unpacking in assignment

_, ip, method, path, status, duration = raw.split()   # exact field count, or ValueError
_, ip, method, path, *rest = raw.split()               # *rest absorbs whatever trails safely

That second form is worth noticing: if the gateway team ever adds a seventh field to the log format — a request ID, say — the first version breaks immediately with a ValueError: too many values to unpack, and the second one keeps working, with the new field just sitting unused in rest until you decide to read it.

current_batch, previous_batch = previous_batch, current_batch  # swap which one is "current" on rotation

Nothing on that line runs in the order it visually suggests. Python fully evaluates the right side first — bundling previous_batch and current_batch into one throwaway tuple — before assignment even starts, and only then unpacks that tuple back into current_batch and previous_batch on the left. Because both original values were captured before either name changed, there's no window where one variable has already flipped and the other hasn't.

Forcing how a function gets called

record_metric's signature has a subtle risk: someone could call record_metric(name="dur", 1, 2), or worse, could start passing values positionally today and by keyword next year, and every call site would keep working while quietly meaning something different. Two punctuation marks in a signature let you close that off:

def record_metric(name, *values, unit="ms", **tags):    # unit is keyword-only — after the *values
    ...

record_metric("dur", 1, 2, unit="s", host="gateway")     # fine
record_metric("dur", 1, 2, "s")                            # TypeError — unit can't be passed positionally

def parse_line(raw, /, *, strict=False):                 # raw is positional-only — before the /
    ...

parse_line("198.51.100.7 ...")            # fine
parse_line(raw="198.51.100.7 ...")        # TypeError — raw can't be passed by keyword

Anything before a bare / in the signature must be passed positionally; anything after a bare * must be passed by keyword. parse_line's raw being positional-only means the parameter could later be renamed without breaking any caller who wrote parse_line(raw=...), because no caller is allowed to write that. It's a small piece of API design, not a performance trick, and it's why you'll see it increasingly in the standard library's own function signatures.