Can you write this from memory?
Write the function header for `add_item` with one parameter named `item`
You know how to write Python's def. But do you hesitate on parameter order? Which comes first—*args or keyword-only arguments? What's that / doing in function signatures? Why does your default list keep growing?
These aren't edge cases—they're the details that cause TypeError: got multiple values for argument at 2 AM.
Parameter order:
def f(positional_only, /, positional_or_keyword, *, keyword_only, **kwargs)
Everything before / is positional-only. Everything after * (or *args) is keyword-only.
Python function parameters follow this strict order:
def f(positional_only, /, positional_or_keyword, *args, keyword_only, **kwargs)
| Position | What it means |
|---|---|
Before / | Positional-only (can't use keyword syntax) |
After /, before * | Positional or keyword (the default) |
After * or *args | Keyword-only (must use name=value) |
**kwargs | Captures remaining keyword arguments |
Example combining everything:
def api_call(endpoint, /, method="GET", *args, headers=None, **options):
# endpoint: positional-only (callers use: api_call("/users"))
# method: positional or keyword
# *args: extra positional arguments
# headers: keyword-only (must be: headers={...})
# **options: remaining keyword arguments
pass
Here's how it bites you:
def append_to(item, target=[]): # BUG!
target.append(item)
return target
append_to(1) # [1]
append_to(2) # [1, 2] — wait, what?
append_to(3) # [1, 2, 3] — it's accumulating!
Why it happens: Default values are evaluated once when the function is defined, not when it's called. The empty list [] is created once and reused for every call that doesn't provide target.
The fix: Use None as a sentinel and create a fresh container inside:
def append_to(item, target=None):
if target is None:
target = [] # New list each time
target.append(item)
return target
This pattern applies to any mutable default: lists, dicts, sets, and custom objects. If you're coming from JavaScript, Python vs JavaScript functions covers how default parameters differ between the two languages.
Put a bare * in your signature to make all following parameters keyword-only:
def connect(*, host, port, timeout=30):
...
# Caller MUST use keyword syntax:
connect(host="localhost", port=8080) # OK
connect("localhost", 8080) # TypeError!
When to use keyword-only:
- Parameters that are easily confused (is it
(width, height)or(height, width)?) - Boolean flags that need context (
force=Trueis clearer than justTrue) - Optional parameters that shouldn't be passed positionally
If you also need *args, keyword-only parameters come after:
def log(message, *args, level="INFO"):
# *args captures extra positional arguments
# level is keyword-only
print(f"[{level}]", message % args)
Put a / in your signature to make all preceding parameters positional-only:
def greet(name, /):
return f"Hello, {name}!"
greet("Alice") # OK
greet(name="Alice") # TypeError: positional-only
When to use positional-only:
- The parameter name is an implementation detail (you might rename it later)
- Keyword syntax would be awkward or confusing
- Matching behavior of built-ins like
len(obj)(you can't writelen(obj=...))
Combining both:
def divmod(a, b, /):
# Matches the built-in: positional-only
return a // b, a % b
TypeError: got multiple values for argument 'x'
You passed a value both positionally AND as a keyword:
def greet(name, message="Hello"):
print(f"{message}, {name}!")
greet("Alice", name="Bob") # TypeError!
# "Alice" goes to 'name', then name="Bob" tries to set it again
Fixes:
- Remove the duplicate:
greet("Alice") - Make
namepositional-only:def greet(name, /, message="Hello")
TypeError: missing 1 required keyword-only argument
A keyword-only parameter without a default wasn't provided:
def connect(*, host, port):
...
connect(host="localhost") # TypeError: missing 'port'
Fix: Pass the required argument: connect(host="localhost", port=8080)
TypeError: takes N positional arguments but M were given
Too many positional arguments:
def add(a, b):
return a + b
add(1, 2, 3) # TypeError: takes 2 but 3 were given
Fixes:
- Pass fewer arguments
- Add
*argsto capture extras:def add(*args): return sum(args) - In methods, remember
selfcounts as the first argument
The Strategy design pattern relies on passing different functions (or lambdas) to change an object's behavior at runtime — a natural fit for Python's first-class functions.
Lambdas are anonymous single-expression functions:
# As a key function
sorted(users, key=lambda u: u.name)
# In higher-order functions
list(map(lambda x: x ** 2, numbers))
# Callbacks
button.on_click(lambda event: print("Clicked!"))
When to prefer def:
- Multiple statements needed
- Complex logic that deserves a name
- You need a docstring or type hints
- Debugging (named functions have better tracebacks)
The closure trap
Lambdas capture variables by reference, not value:
# BUG: all lambdas see i=2 (the final value)
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2]
# FIX: capture current value via default argument
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs] # [0, 1, 2]
Python resolves names by searching four scopes in order: Local, Enclosing, Global, Built-in (LEGB).
Variables assigned inside a function are local by default:
x = 10
def modify():
x = 20 # Creates a LOCAL x, doesn't touch global
print(x) # 20
modify()
print(x) # 10 — global unchanged
To modify a global variable, use the global keyword:
x = 10
def modify():
global x
x = 20 # Now modifies the global
modify()
print(x) # 20
For nested functions, use nonlocal to modify an enclosing scope:
def outer():
count = 0
def increment():
nonlocal count
count += 1
increment()
increment()
return count # 2
General advice: Avoid global when possible—it makes code harder to reason about. Prefer returning values and passing arguments. Rust enforces ownership rules that eliminate many of the scoping bugs Python developers encounter — see Python vs Rust functions for the comparison.
The * and ** operators work in function calls too:
def greet(name, greeting, punctuation):
return f"{greeting}, {name}{punctuation}"
# Unpack a list/tuple as positional arguments
args = ["Alice", "Hello", "!"]
greet(*args) # "Hello, Alice!"
# Unpack a dict as keyword arguments
kwargs = {"name": "Bob", "greeting": "Hi", "punctuation": "?"}
greet(**kwargs) # "Hi, Bob?"
# Combine both
greet(*["Alice"], **{"greeting": "Hey", "punctuation": "."})
This is especially useful for forwarding arguments:
def wrapper(*args, **kwargs):
print("Before")
result = original_function(*args, **kwargs)
print("After")
return result
When to Use Python Function Arguments: defaults, *args, keyword-only, **kwargs
- Reuse logic across files or code paths—functions are how you DRY up code.
- Isolate side effects and make code testable—pure functions are easier to reason about.
- Create flexible APIs with optional parameters, defaults, and *args/**kwargs for extensibility.
- Use positional-only (`/`) for internal parameters that callers shouldn't rely on by name.
- Use keyword-only (`*`) to force clarity when parameters are easily confused.
Check Your Understanding: Python Function Arguments: defaults, *args, keyword-only, **kwargs
Write a function that accepts any number of positional arguments and returns their sum.
Use `*args` to collect positional arguments into a tuple: `def add(*args): return sum(args)`. Mention that *args captures excess positional arguments, not all arguments.
What You'll Practice: Python Function Arguments: defaults, *args, keyword-only, **kwargs
Common Python Function Arguments: defaults, *args, keyword-only, **kwargs Pitfalls
- Mutable default arguments (lists/dicts persist across calls)—use `None` sentinel instead
- Forgetting `return` (function returns `None` implicitly)
- `TypeError: got multiple values for argument`—passing both positionally and by keyword
- Variable scope confusion—assignment creates a local unless you declare `global` or `nonlocal`
- Lambda closure captures variable **by reference**, not value—use default argument to capture
- Forgetting `self` in class methods—first argument receives the instance
- *args must come before keyword-only parameters in the signature
Python Function Arguments: defaults, *args, keyword-only, **kwargs FAQ
What order do parameters go in a function signature?
Positional-only (before `/`), then regular positional-or-keyword, then `*args`, then keyword-only, then `**kwargs`. The full pattern: `def f(a, /, b, *args, c, **kwargs)`.
Why are mutable default arguments a problem?
Defaults are evaluated **once** at function definition time, not at each call. A list or dict default is shared across all calls—mutations accumulate. Use `None` as the default and create a new container inside the function.
When do I use *args vs **kwargs?
`*args` collects extra **positional** arguments into a tuple. `**kwargs` collects extra **keyword** arguments into a dict. Use *args for variable-length positional inputs; use **kwargs for option-style named parameters.
What does the `/` in a function signature mean?
Parameters before `/` are positional-only (PEP 570, Python 3.8+). Callers can't use keyword syntax for them. This lets you change parameter names without breaking callers.
What does `*` (bare asterisk) mean in a signature?
Everything after a bare `*` is keyword-only—callers must use `name=value` syntax. Use this to prevent positional mistakes: `def connect(*, host, port)` forces `connect(host="...", port=...)`.
What's the difference between *args after * vs bare *?
Both make subsequent parameters keyword-only. `*args` also captures excess positional arguments; bare `*` just marks the boundary without capturing anything.
TypeError: got multiple values for argument 'x' — how do I fix it?
You passed a value positionally AND as a keyword argument. Example: `f(1, x=2)` when `x` is the first parameter. Either remove the keyword or use positional-only (`/`) to prevent keyword access.
TypeError: missing 1 required keyword-only argument: 'x' — what does this mean?
A parameter after `*` or `*args` has no default and you didn't pass it by name. Keyword-only parameters without defaults are required—you must use `name=value` syntax.
TypeError: takes N positional arguments but M were given — how do I fix it?
You passed more positional arguments than the function accepts. Either add `*args` to capture extras, or check your call site for too many arguments. Remember: `self` counts as the first positional argument in methods.
When should I use lambda vs def?
Use `lambda` for short, single-expression functions you're passing immediately (e.g., `key=lambda x: x.name`). Use `def` for anything with statements, multiple lines, or that needs a name for debugging/reuse.
Why does my function return None?
Python functions return `None` implicitly if they reach the end without a `return` statement. If you want to return a value, you need an explicit `return value`.
How do local vs global variables work in functions?
Variables assigned inside a function are local by default. To modify a global variable, declare it with `global x` first. For nested functions, use `nonlocal x` to modify a variable from an enclosing scope.
Python Function Arguments: defaults, *args, keyword-only, **kwargs Syntax Quick Reference
def example(pos_only, /, pos_or_kw, *args, kw_only, **kwargs):
# pos_only: must be positional
# pos_or_kw: can be either
# *args: captures extra positionals
# kw_only: must use keyword
# **kwargs: captures extra keywords
passdef connect(*, host, port, timeout=30):
# All parameters are keyword-only
# Callers MUST use: connect(host="...", port=...)
passdef greet(name, /):
# Caller cannot use: greet(name="Alice")
# Only: greet("Alice")
return f"Hello, {name}!"def append_to(item, target=None):
if target is None:
target = [] # Fresh list each call
target.append(item)
return targetdef bad_append(item, target=[]): # BUG!
target.append(item) # Same list every call
return target
bad_append(1) # [1]
bad_append(2) # [1, 2] — not [2]!def log(message, *args, **kwargs):
print(message.format(*args), **kwargs)
log("User {} logged in", "alice", end="\n\n")def point(x, y, z):
return x + y + z
coords = [1, 2, 3]
point(*coords) # Unpacks list as positional args
opts = {"x": 1, "y": 2, "z": 3}
point(**opts) # Unpacks dict as keyword args# Lambda: single expression, no name
square = lambda x: x ** 2
# def: statements, name, docstring
def square(x):
"""Return x squared."""
return x ** 2# BUG: all lambdas capture same i reference
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2]
# FIX: capture by default argument
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs] # [0, 1, 2]def greet(name: str, times: int = 1) -> str:
return f"Hello, {name}! " * timesPython Function Arguments: defaults, *args, keyword-only, **kwargs Sample Exercises
Write the call that passes `1` positionally into `get_item`
get_item(1)Write the call using a keyword argument so `item` receives `""`
add_item(item="")Write the call passing `""` positionally and `active=True` as a keyword
add_item("", active=True)+ 55 more exercises