Python Function Arguments and Scope

Write flexible functions with *args and **kwargs, unpack arguments, understand local and global scope, and avoid the mutable default trap.

Lesson 27 of 107Intermediate18 min read

In the functions lesson every function had a fixed list of parameters. But some of Python's own functions are more flexible. print() accepts one value or twenty. max(3, 9, 4) works with any number of arguments. dict(name="Ada", age=36) accepts whatever keywords you give it. How do they do it?

The answer is *args and **kwargs, two special parameters that collect "everything else" the caller passes in. The same star symbols also work the other way round, unpacking a list or dictionary into separate arguments.

In this lesson you'll write flexible functions with *args and **kwargs, unpack values into calls, force callers to name certain arguments, return several values at once, and understand scope: which variables a function can see and change. You'll also learn about a classic Python trap that catches experienced programmers too.

*args: any number of positional arguments

A parameter that starts with * collects all extra positional arguments into a tuple:

Python
def total(*prices):
    return sum(prices)

print(total(2, 3))           # 5
print(total(1.5, 2, 4, 10))  # 17.5
print(total())               # 0

The name args is only a convention; *prices is clearer here. You can combine it with normal parameters, which come first: def order(customer, *items):.

**kwargs: any number of keyword arguments

A parameter that starts with ** collects extra keyword arguments into a dictionary:

Python
def profile(name, **details):
    print(name)
    for key, value in details.items():
        print(f"  {key}: {value}")

profile("Ada", city="London", role="Engineer")

The full order of parameters is: normal ones, *args, keyword-only ones, then **kwargs. **kwargs is useful for options, settings and functions that pass arguments on to another function.

Unpacking and keyword-only arguments

Stars also work when calling a function. * spreads a list or tuple into positional arguments, and ** spreads a dictionary into keyword arguments:

Python
point = [3, 4]
settings = {"sep": " | ", "end": "!\n"}
print(*point, **settings)    # 3 | 4!

A bare * in a definition makes every parameter after it keyword-only. def book(room, *, nights, breakfast=False): must be called as book(12, nights=2), so nobody can mix up two numbers by accident.

Positional-only parameters

Python 3.8 added the opposite of keyword-only parameters. Parameters listed before a / in the definition are positional-only: callers can't pass them by name.

Python
def power(base, exponent, /):
    return base ** exponent

power(2, 10)            # 1024
power(base=2, exponent=10)   # TypeError

Many built-in functions work like this, for example len(obj), which is why len(obj=items) fails. Library authors use / so they can rename a parameter later without breaking anyone's code. In your own scripts you will mostly read it rather than write it.

Scope: local and global

A variable created inside a function is local: it exists only while the function runs, and the rest of the program can't see it. A variable created at the top level of a file is global, and functions can read it.

To change a global variable inside a function you'd need the global keyword, but it's usually better to pass values in and return results. Code where functions secretly change global variables is hard to follow and test.

A function can return several values at once by separating them with commas. Python packs them into a tuple, and the caller can unpack them: low, high = min_max(scores).

How Python looks up a name

When a function uses a name, Python searches four places in order, often remembered as LEGB:

  1. Local: names created inside the current function.
  2. Enclosing: names in any function that contains this one.
  3. Global: names at the top level of the module (file).
  4. Built-in: Python's own names, such as len and print.

The first match wins. This is why creating a variable called list or sum is a bad idea: your global name hides the built-in, and later calls such as sum(values) fail with a confusing error.

Assigning to a name inside a function makes it local for the whole function, which explains the UnboundLocalError in the mistake below. To assign to a variable from an enclosing function, declare it with nonlocal; to assign to a module-level variable, use global. Both should be rare in everyday code.

Python Function Arguments Syntax

Parameter order

def func(a, b=2, *args, key, option=None, **kwargs):
    ...

func(1, 2, 3, 4, key="k", extra=True)
func(*[1, 2], **{"key": "k"})

def min_max(values):
    return min(values), max(values)

low, high = min_max([4, 9, 1])

Python Function Arguments Summary

  • *args collects extra positional arguments into a tuple.
  • **kwargs collects extra keyword arguments into a dictionary.
  • * and ** in a call unpack a list or dictionary into arguments.
  • Parameters after a bare * are keyword-only.
  • Variables inside a function are local; prefer returning values to using global. Never use a list or dict as a default value.

Python Function Arguments Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A flexible average

Python
def average(*numbers, decimals=1):
    if not numbers:
        return 0
    return round(sum(numbers) / len(numbers), decimals)

print(average(4, 8, 9))
print(average(7, 8, decimals=2))
scores = [62, 75, 81, 90]
print(average(*scores))
Output
7.0
7.5
77.0
Line-by-line explanation
  1. Line 1decimals comes after *numbers, so it can only be given by name.
  2. Lines 2 to 3An empty tuple is falsy, so calling average() with no numbers returns 0 instead of crashing.
  3. Line 9*scores unpacks the list into four separate arguments.
Example 2

Building HTML attributes with **kwargs

Python
def tag(name, text, **attributes):
    attrs = "".join(f' {key}="{value}"' for key, value in attributes.items())
    return f"<{name}{attrs}>{text}</{name}>"

print(tag("p", "Hello"))
print(tag("a", "Home", href="index.html", title="Go home"))
Output
<p>Hello</p>
<a href="index.html" title="Go home">Home</a>
Line-by-line explanation
  1. Line 1Any keyword arguments end up in the attributes dictionary.
  2. Line 2Each key and value becomes an HTML attribute.
  3. Line 6href and title weren't listed as parameters, but **attributes accepts them.
Example 3

Local, global and returning several values

Python
tax_rate = 0.2

def price_with_tax(price):
    tax = price * tax_rate
    return price + tax, tax

total, tax = price_with_tax(50)
print(total, tax)
print(tax_rate)
Output
60.0 10.0
0.2
Line-by-line explanation
  1. Line 4Reading the global tax_rate inside the function is fine.
  2. Line 5Returning two values gives back a tuple.
  3. Line 7Unpack the tuple into two variables. This tax is a new global variable; the function's own tax variable disappeared when it finished.

Common Python Function Arguments Mistakes

The mutable default argument

Don't
def add_item(item, basket=[]):
    basket.append(item)
    return basket

print(add_item("apple"))   # ['apple']
print(add_item("bread"))   # ['apple', 'bread']  surprise!
Do
def add_item(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket

Changing a global without saying so

Don't
count = 0
def increase():
    count += 1        # UnboundLocalError
Do
def increase(count):
    return count + 1

count = increase(count)

Putting *args in the wrong place

Don't
def order(*items, customer):
    ...
order("tea", "cake", "Sam")   # TypeError: missing customer
Do
def order(customer, *items):
    ...
order("Sam", "tea", "cake")

Python Function Arguments in Real Projects

Flexible arguments in libraries

Many popular libraries use these features. Web frameworks pass extra options through **kwargs, plotting functions accept dozens of optional keyword settings, and decorators rely on *args, **kwargs to wrap any function regardless of its parameters. Keyword-only arguments appear throughout Python's standard library, such as sorted(items, key=..., reverse=True).

Python Function Arguments FAQ

What do * and ** mean in a Python function definition?

In a definition, *args collects extra positional arguments into a tuple and **kwargs collects extra keyword arguments into a dictionary. In a function call, the same symbols do the reverse: * spreads a list or tuple into separate arguments and ** spreads a dictionary into keyword arguments.

Do I have to call them args and kwargs?

No. Only the stars matter; args and kwargs are conventions. A descriptive name such as *prices or **options is often clearer. Stick with the conventional names when writing general wrappers, such as decorators, where readers expect them.

When should I use the global keyword?

Rarely. global lets a function reassign a module-level variable, but it hides where changes come from and makes testing harder. Passing values in as arguments and returning new values is almost always clearer. Reading a global constant, such as TAX_RATE, needs no keyword at all.

What is the difference between global and nonlocal?

global refers to a variable at the top level of the module. nonlocal refers to a variable in the nearest enclosing function, and only works inside nested functions. Both are needed only when you assign to the variable; reading it works without either keyword.

Why does Python say an argument was given more than once?

This TypeError happens when you pass the same parameter both by position and by name, for example greet("Ada", name="Bob") for def greet(name). Python fills name from the first argument, then finds a second value for it. Pass each parameter only once.

Python Function Arguments Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the definition so the function accepts any number of names.

def greet_all(names):
    for name in names:
        print("Hi", name)
Predict the outputeasy

What does this print?

def count(*things):
    return len(things)

print(count("a", "b", "c"))
Predict the outputmedium

What does this print?

def show(**info):
    print(sorted(info))

show(b=2, a=1)
Multiple choicemedium

Given def book(room, *, nights):, which call works?

Predict the outputhard

What does this print?

def f(x, items=[]):
    items.append(x)
    return len(items)

f(1)
print(f(2))

Python Function Arguments Quiz

5 questions to check what you learned.

  1. What type is args inside def f(*args):?

    • list
    • dict
    • set
    • tuple
  2. What type is kwargs inside def f(**kwargs):?

    • list
    • tuple
    • str
    • dict
  3. What does print(*[1, 2, 3]) print?

    • [1, 2, 3]
    • 1 2 3
    • 123
    • (1, 2, 3)
  4. What happens to a local variable when its function finishes?

    • It disappears
    • It becomes global
    • It's saved for the next call
    • It's printed
  5. What's the safe default for a parameter that should start as an empty list?

    • basket=[]
    • basket=list
    • basket=None, then create a list inside the function
    • basket=()