Back to Blog
Python

Python Function Arguments: Syntax, Defaults, and *args/**kwargs

python function arguments: Learn how Python function arguments work: positional, keyword, default, *args, and **kwargs, with practical examples and common pitfalls.

pythonfunction-argumentskwargsargsdefault-argumentskeyword-arguments
Illustration of Python function arguments with parameters and values

Python function arguments are the values you pass into a function when calling it. The way you define and call functions determines how those values are bound to parameters, and Python offers several mechanisms to make this flexible. Understanding these mechanisms is essential for writing clear, maintainable code, especially when designing APIs or handling dynamic inputs.

The Basics of Python Function Arguments

At its core, a function definition lists parameters, and a function call provides arguments. Python binds arguments to parameters based on their position or an explicit keyword. For example:

def greet(name, greeting): return f"{greeting}, {name}!" greet("Alice", "Hello") # positional greet(name="Bob", greeting="Hi") # keyword

In the positional call, "Alice" maps to name and "Hello" to greeting. In the keyword call, the order does not matter because each argument is explicitly assigned. This basic distinction is the foundation for all other argument features.

Positional vs Keyword Arguments

Positional arguments are matched by order, while keyword arguments are matched by name. You can mix them, but positional arguments must come before keyword arguments in a call:

def describe_person(name, age, city): return f"{name} is {age} years old and lives in {city}" describe_person("Alice", 30, city="New York") # valid # describe_person(name="Alice", 30, "New York") # SyntaxError

This rule exists to avoid ambiguity: once you start using keywords, Python cannot reliably infer the position of subsequent positional arguments. In practice, using keyword arguments improves readability when a function has many parameters, especially if some have defaults.

Default Arguments and the Mutable Default Trap

Default arguments let you define a parameter that takes a fallback value if the caller omits it:

def connect(host, port=8080): return f"Connecting to {host}:{port}"

Defaults are evaluated once at function definition time, not on every call. This becomes a problem when the default is a mutable object like a list or dictionary:

def add_item(item, items=[]): items.append(item) return items print(add_item(1)) # [1] print(add_item(2)) # [1, 2] # unexpected!

The same list object is shared across calls. To avoid this, use None as the default and create a new mutable inside the function:

def add_item(item, items=None): if items is None: items = [] items.append(item) return items

This pattern is a well-known Python gotcha. Always treat mutable defaults as a bug unless you intentionally want shared state.

Using *args for Variable-Length Positional Arguments

The *args syntax collects any number of extra positional arguments into a tuple. This is useful when a function should accept an arbitrary number of inputs, such as a sum function:

def total(*args): return sum(args) total(1, 2, 3) # 6 total(1, 2, 3, 4, 5) # 15

Inside the function, args is a tuple. You can still have normal parameters before *args, but they must be provided explicitly:

def log(level, *messages): for msg in messages: print(f"[{level}] {msg}")

This pattern is common in logging, event handlers, and math utilities. The name args is a convention, not a keyword; you can use any name after the asterisk.

Using **kwargs for Variable-Length Keyword Arguments

The **kwargs syntax collects extra keyword arguments into a dictionary. It is often used to pass configuration options or to forward arguments to another function:

def make_request(url, **kwargs): if "timeout" in kwargs: print(f"Timeout set to {kwargs['timeout']}") return f"Request to {url} with {kwargs}" make_request("https://api.example.com", timeout=5, retries=2)

Here, kwargs is a dictionary mapping parameter names to values. This is powerful for building flexible APIs, but it also hides the exact signature from callers, which can reduce clarity. Use **kwargs when the set of options is truly dynamic, such as in a plugin system or when wrapping a third-party library.

Keyword-Only and Positional-Only Arguments

Python 3 allows you to enforce how arguments are passed. The / and * markers in a function definition control this:

def compare(a, b, /, *, key=None): pass

Parameters before / are positional-only; parameters after * are keyword-only. This is useful for API design where you want to prevent callers from using keywords for certain parameters, or force them to use keywords for others. For example, a function that takes two numbers and an optional key:

def sort_items(iterable, /, *, reverse=False): pass

Here, iterable must be passed positionally, while reverse must be passed as a keyword. This prevents ambiguity and makes the intent clearer. The syntax may look odd at first, but it is a valuable tool for library authors.

Common Pitfalls and How to Avoid Them

Beyond mutable defaults, several other argument-related issues trip up developers:

  • Using mutable objects as defaults – already covered; always use None and create inside.
  • Ordering errors in mixed calls – remember positional before keyword.
  • Unpacking mistakes – when calling a function with *list or **dict, the number of elements must match the parameters.
  • Shadowing built-in names – avoid naming a parameter list or dict.
  • Overusing *args and **kwargs – they reduce readability; use them only when necessary.

For example, unpacking a list into positional arguments is convenient but can fail if the list length is wrong:

def add(a, b, c): return a + b + c values = [1, 2, 3] add(*values) # works values = [1, 2] add(*values) # TypeError: missing 1 required positional argument

Be deliberate about when to use these features. A function with a fixed, small number of parameters is easier to understand and test than one that accepts arbitrary arguments.

Performance and Maintainability Considerations

From a performance standpoint, *args and **kwargs introduce a small overhead because they create a tuple and a dictionary, respectively. For most applications this is negligible, but in hot loops or high-frequency calls, it can add up. If you have a function that is called millions of times, prefer explicit parameters over *args when the argument count is known.

Maintainability is more significant. Explicit signatures act as documentation and enable static analysis tools to catch errors. When you use **kwargs, you lose the ability to see what parameters are accepted without reading the function body. This makes refactoring harder and increases the risk of typos in parameter names. Reserve **kwargs for genuinely dynamic scenarios, such as passing options to a third-party API or building a decorator that must forward arbitrary arguments.

Another maintainability concern is the interaction between default arguments and introspection. Tools like inspect.signature() can reveal a function's parameters, but they treat *args and **kwargs as opaque. If you rely on such tools for automated documentation or validation, explicit parameters are more friendly.

In summary, Python function arguments offer a rich set of features. Use positional and keyword arguments for clarity, default arguments for optional values, and *args/**kwargs only when the number of inputs is truly variable. Always be wary of mutable defaults and prefer explicit signatures for code that others will read and maintain.

python function arguments: Practical Usage and Code Examples | RYUSLOG DEV