Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Python
  3. Python Function Arguments Practice: defaults, *args, keyword-only, **kwargs
Python58 exercises

Python Function Arguments Practice: defaults, *args, keyword-only, **kwargs

Practice Python function arguments: parameter order (*args before keyword-only before **kwargs), the mutable default trap, positional-only with /, and common TypeErrors with fixes.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Write the function header for `add_item` with one parameter named `item`

On this page
  1. 1Parameter order
  2. 2The mutable default trap
  3. 3Keyword-only arguments: forcing clarity
  4. 4Positional-only parameters (Python 3.8+)
  5. 5Common TypeErrors and fixes
  6. TypeError: got multiple values for argument 'x'
  7. TypeError: missing 1 required keyword-only argument
  8. TypeError: takes N positional arguments but M were given
  9. 6Lambda expressions: when to use them
  10. The closure trap
  11. 7Variable scope: local, global, nonlocal
  12. 8Unpacking arguments: and *
  13. 9References
Parameter orderThe mutable default trapKeyword-only arguments: forcing clarityPositional-only parameters (Python 3.8+)Common TypeErrors and fixesLambda expressions: when to use themVariable scope: local, global, nonlocalUnpacking arguments: and *References

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.

Related Python Topics
Python Decorators: wraps, factories, stacking order & lru_cachePython Comprehensions: List, Dict, Set & Generator ExpressionsPython Exception Handling: try/except/else/finally, raise from, custom exceptions

Strict left-to-right: positional-only (before /) then regular then *args then keyword-only (after *) then **kwargs. Python's parser enforces this order.

Python function parameters follow this strict order:

Parameter order: pos_only, then / separator, then pos_or_kw, then *args, then kw_only, then **kwargs

def f(positional_only, /, positional_or_keyword, *args, keyword_only, **kwargs)
PositionWhat it means
Before /Positional-only (can't use keyword syntax)
After /, before *Positional or keyword (the default)
After * or *argsKeyword-only (must use name=value)
**kwargsCaptures 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

Ready to practice?

Start practicing Python Function Arguments: defaults, *args, keyword-only, **kwargs with spaced repetition

Defaults are evaluated once at definition time, not per call. A default [] or {} is shared across all calls. Use None as a sentinel and create fresh containers inside the function.

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.


A bare * in the signature forces all following parameters to use name=value syntax. Prevents positional mixups like confusing width and height.

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=True is clearer than just True)
  • 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 write len(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:

  1. Remove the duplicate: greet("Alice")
  2. Make name positional-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:

  1. Pass fewer arguments
  2. Add *args to capture extras: def add(*args): return sum(args)
  3. In methods, remember self counts 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).

LEGB scope resolution: Local to Enclosing to Global to Built-in, then NameError

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

  • Python Tutorial: Defining Functions
  • Python Tutorial: More on Defining Functions
  • Python FAQ: Why are default values shared between objects?
  • PEP 570 – Positional-Only Parameters
  • PEP 3102 – Keyword-Only Arguments
  • Python Reference: Lambda expressions

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

Prompt

Write a function that accepts any number of positional arguments and returns their sum.

What a strong answer looks like

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

Parameter order: positional-only → positional-or-keyword → *args → keyword-only → **kwargsDefault parameter values (and the mutable default trap)Positional-only parameters with / (PEP 570)Keyword-only parameters with * or *args*args and **kwargs for flexible APIsReturn statements and implicit NoneLambda expressions for inline functionsVariable scope (local, global, nonlocal)Unpacking arguments with * and **Type hints in function signatures

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

Parameter order cheat sheet
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
    pass
Keyword-only with bare *
def connect(*, host, port, timeout=30):
    # All parameters are keyword-only
    # Callers MUST use: connect(host="...", port=...)
    pass
Positional-only with /
def greet(name, /):
    # Caller cannot use: greet(name="Alice")
    # Only: greet("Alice")
    return f"Hello, {name}!"
Safe default (mutable trap fix)
def append_to(item, target=None):
    if target is None:
        target = []  # Fresh list each call
    target.append(item)
    return target
The mutable default trap
def 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]!
*args and **kwargs
def log(message, *args, **kwargs):
    print(message.format(*args), **kwargs)

log("User {} logged in", "alice", end="\n\n")
Unpacking in function calls
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 vs def
# Lambda: single expression, no name
square = lambda x: x ** 2

# def: statements, name, docstring
def square(x):
    """Return x squared."""
    return x ** 2
Lambda closure trap
# 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]
Type hints
def greet(name: str, times: int = 1) -> str:
    return f"Hello, {name}! " * times

Python Function Arguments: defaults, *args, keyword-only, **kwargs Sample Exercises

Example 1Difficulty: 1/5

Write the call that passes `1` positionally into `get_item`

get_item(1)
Example 2Difficulty: 1/5

Write the call using a keyword argument so `item` receives `""`

add_item(item="")
Example 3Difficulty: 2/5

Write the call passing `""` positionally and `active=True` as a keyword

add_item("", active=True)

+ 55 more exercises

Related Design Patterns

Strategy Pattern

Also in Other Languages

JavaScript Functions Practice

Start practicing Python Function Arguments: defaults, *args, keyword-only, **kwargs

Free daily exercises with spaced repetition. No credit card required.

← Back to Python Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.