Skip to content

Step 1 of 8 · Reading · ~3 min

Learn

Functions

Arguments and Scope

After this lesson you can design signatures with positional-only, keyword-only and variable-length parameters, forward arguments with * and **, avoid the shared-default bug, and predict which variable a name refers to.

In Python Fundamentals you wrote def with plain defaults and keyword calls. This lesson covers how Python matches arguments to parameters in the hard cases, and how it looks names up.

The five parameter kinds

python
('div', (), None, {})
('p', ('intro', 'big'), 'top', {'title': 'Hi'})
('p', ('intro', 'top'), None, {})
  • name before the / is positional-only (3.8+): the caller cannot pass it by keyword. tag(name="div") raises a TypeError (missing argument).
  • *classes collects every extra positional argument into a tuple.
  • id after *classes is keyword-only: it can never be filled positionally. In the third call "top" was swallowed by *classes and id stayed None, with no error.
  • **attrs collects unmatched keyword arguments into a dict; a named parameter is never in it.

A bare * (as in def box(w, *, h)) makes keyword-only parameters without collecting anything.

Unpacking at the call site

In a call the stars do the reverse: * spreads an iterable into positional arguments, ** a dict into keyword arguments.

python

That is exactly tag("p", "intro", "big", id="top", title="Hi").

Defaults are evaluated once

A default expression runs when def executes, not on each call. With a mutable default, every call shares one object:

python
['a']
['a', 'b']
(['a', 'b'],)

The function object stores that list. The fix is a sentinel: default to None and build the object inside.

python

Now log("a") then log("b") give ['a'] then ['b']. Immutable defaults are safe.

Arguments are shared references

Mutating through a parameter changes the caller's object; rebinding the parameter does not.

python

Name lookup: LEGB

Names are looked up in the Local scope, any Enclosing function, the Global (module) scope, then Built-ins; the first hit wins. A name is local to a function if the function assigns to it anywhere.

python

This raises UnboundLocalError: balance -= x assigns, so balance is local, and it is read before it has a value. Reading a global needs no declaration; rebinding one needs global:

python

For a variable of an enclosing function, use nonlocal:

python

Without nonlocal, add raises the same UnboundLocalError. .append on a global list needs no declaration: it never rebinds the name.

Common mistakes

  • A mutable default such as notes=[]: the second call returns the first call's items, so one order's fees show up in the next.
  • Leaving a known option out of the signature: with tax only in **fees, it is one more extra and gets added as a flat fee.
  • Assigning to a global without global: += raises UnboundLocalError; plain = silently builds a new local and the module-level value never changes.
  • Summing successive percentages: discounts of 10 and 5 applied one after another take 14.5% off, not 15%.

Your exercise

Write price with *discounts, a keyword-only tax, a notes parameter and **fees, and call it with * and ** unpacking. Keep a list out of the default, or fees from one order will appear on the next.

Up nextHigher-Order FunctionsFunctions

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…