Previous | Tutorial index | Next

Tutorial 4: Function Arguments in Depth – Positional, Keyword, and Variable-Length Arguments

Learning Objectives

Overview

Functions become far more powerful when you understand how to pass arguments to them. Python offers a flexible and expressive argument‑handling system that allows you to write functions that can be called in many different ways. In this tutorial, we will explore positional arguments (the default), keyword arguments (by name), default parameter values, and variable‑length arguments (*args and **kwargs). We’ll also cover the correct order of parameters, the concept of positional‑only and keyword‑only arguments, and how arguments are passed (the “pass by assignment” model). By the end, you’ll be able to design functions with clear, intuitive calling conventions.

1. Positional Arguments

1.1 The Basics

Positional arguments are the simplest form: the arguments are matched to parameters in the order they are passed.

def subtract(a, b): return a - b print(subtract(10, 3)) # 7 (a=10, b=3) print(subtract(3, 10)) # -7 (a=3, b=10)

The order matters.

1.2 Required Positional Arguments

If a parameter has no default value, it is required. Calling the function without enough arguments raises a TypeError.

def greet(name): print(f"Hello, {name}!") greet() # TypeError: missing 1 required positional argument: 'name'

2. Keyword Arguments

2.1 Calling with Keywords

You can specify which argument goes to which parameter by using the parameter name. This makes the call more explicit and allows you to change the order.

def introduce(name, age, city): print(f"{name} is {age} years old and lives in {city}.") introduce(city="Paris", age=25, name="Alice") # Works fine

2.2 Mixing Positional and Keyword

You can mix them, but positional arguments must come before keyword arguments in the call.

introduce("Bob", city="London", age=30) # valid: "Bob" -> name, then keyword for age, city # introduce(city="London", "Bob", age=30) # SyntaxError: positional argument follows keyword argument

3. Default Parameter Values

You can assign a default value to a parameter. If the caller does not provide an argument for that parameter, the default is used.

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

Parameters with defaults are optional. They must come after any required parameters (without defaults) in the definition.

def set_timeout(seconds, message="Timeout!"): # valid: required first, default second ... def set_timeout(message="Timeout!", seconds): # SyntaxError: non-default argument follows default argument

3.1 Mutable Defaults – A Common Trap

Default values are evaluated once when the function is defined, not each time it is called. This can cause surprising behavior when the default is a mutable object (like a list or dictionary).

Example of the trap:

def append_to_list(value, my_list=[]): my_list.append(value) return my_list print(append_to_list(1)) # [1] print(append_to_list(2)) # [1, 2] <-- unexpected!

The same list object is used across calls. To fix, use None as a sentinel.

def append_to_list(value, my_list=None): if my_list is None: my_list = [] my_list.append(value) return my_list

Rule of thumb: Never use mutable objects as default values unless you explicitly want to share state (which is rare).

4. How Arguments Are Passed – Pass by Assignment

Python uses a model called “pass by assignment” (also known as “pass by object reference” or “call by sharing”).

def modify_list(lst): lst.append(4) # modifies the original list lst = [1, 2, 3] # rebinds local variable; does not affect original nums = [10, 20, 30] modify_list(nums) print(nums) # [10, 20, 30, 4] (append worked; reassignment did not)

This is similar to how Java works with objects. Immutable objects (like integers, strings, tuples) cannot be changed in place, so you always get new objects.

5. Variable‑Length Arguments – *args and **kwargs

Sometimes you want a function to accept an arbitrary number of arguments. This is where *args and **kwargs come in.

5.1 *args – Variable‑Length Positional Arguments

When you prefix a parameter with *, it collects all extra positional arguments into a tuple (named args by convention, but you can use any name).

def sum_all(*args): total = 0 for num in args: total += num return total print(sum_all(1, 2, 3)) # 6 print(sum_all(10, 20, 30, 40)) # 100 print(sum_all()) # 0 (empty tuple)

You can combine *args with normal parameters, but *args must come after all positional parameters.

def multiply(multiplier, *numbers): return [multiplier * n for n in numbers] print(multiply(2, 1, 2, 3)) # [2, 4, 6]

5.2 **kwargs – Variable‑Length Keyword Arguments

Similarly, **kwargs collects extra keyword arguments into a dictionary.

def print_profile(**kwargs): for key, value in kwargs.items(): print(f"{key}: {value}") print_profile(name="Alice", age=30, city="NYC") # name: Alice # age: 30 # city: NYC

5.3 Using Both *args and **kwargs

It’s common to see def func(*args, **kwargs): to accept any combination. This is useful for decorators or wrapper functions.

def logger(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}") return func(*args, **kwargs) return wrapper

6. Parameter Order in Function Definitions

There is a strict order for parameters in a function definition:

  1. Positional‑only parameters (optional, introduced in Python 3.8 – we’ll cover them shortly).
  2. Positional (or positional‑or‑keyword) parameters – the normal ones.
  3. * – a special marker to separate positional‑only from keyword‑only (if you want to force keyword‑only after it).
  4. Keyword‑only parameters – parameters that must be supplied by keyword.
  5. **kwargs – must come last.

In practice, the typical order without positional‑only is:

def func(positional_required, positional_optional=default, *args, keyword_only, **kwargs): pass

6.1 Keyword‑Only Arguments (Python 3+)

If you want to force certain parameters to be passed only by keyword, place them after a *.

def greet(name, *, greeting="Hello", punctuation="!"): return f"{greeting}, {name}{punctuation}" # Both are valid: greet("Alice") # Hello, Alice! (uses defaults) greet("Bob", greeting="Hi") # Hi, Bob! # greet("Bob", "Hi") # TypeError: greet() takes 1 positional argument but 2 were given

The * itself does not collect arguments; it marks the end of positional parameters.

6.2 Positional‑Only Arguments (Python 3.8+)

You can make parameters positional‑only by placing a / before them. Parameters before / cannot be passed by keyword.

def divmod(a, b, /): return a // b, a % b divmod(10, 3) # (3, 1) # divmod(a=10, b=3) # TypeError: divmod() got some positional-only arguments passed as keyword arguments

This is used in some built‑in functions (like len()) and is mostly for library design.

7. Unpacking Arguments on Calling

You can also use * and ** when calling a function to unpack a sequence or dictionary into arguments.

def add(a, b, c): return a + b + c numbers = [1, 2, 3] print(add(*numbers)) # 6 info = {"a": 5, "b": 10, "c": 15} print(add(**info)) # 30

This is very handy when you already have the data in a collection.

8. Putting It All Together – A Complex Example

def show_info(name, age, *hobbies, city="Unknown", **extras): print(f"Name: {name}, Age: {age}") if hobbies: print(f"Hobbies: {', '.join(hobbies)}") print(f"City: {city}") if extras: print("Extra info:") for key, value in extras.items(): print(f" {key}: {value}") show_info("Alice", 30, "reading", "swimming", city="Paris", job="Engineer", pet="cat")

Output:

Name: Alice, Age: 30 Hobbies: reading, swimming City: Paris Extra info: job: Engineer pet: cat

📝 Quiz – Check Your Understanding

  1. What is a positional argument?

    Answer(B) An argument passed by position in the call.
  2. Given def f(a, b=2, c=3): return a+b+c, what does f(1, c=10) return?

    Answer(B) `13` – `a=1, b=2, c=10` → 1+2+10=13.
  3. What is the purpose of *args in a function definition?

    Answer(B) It collects arbitrary positional arguments into a tuple.
  4. True or False: Mutable default values (like []) are safe and recommended because they allow caching.

    AnswerFalse – they are unsafe due to shared state.
  5. What is the correct order of parameters in a function definition?

    Answer(B) Positional, `*args`, keyword-only, `**kwargs`
  6. What does this code print?

    def func(x, y, *args): return x + y + sum(args) print(func(1, 2, 3, 4))
    Answer(A) `10` – 1+2+3+4 = 10.
  7. How can you force a parameter to be passed only by keyword?

    Answer(A) Use `*` before that parameter.
  8. What is the difference between *args in definition and *list in a call?

    Answer(B) `*args` collects arguments; `*list` unpacks a list into arguments.
  9. What does def my_func(**kwargs): allow you to do?

    Answer(B) Accept any number of keyword arguments.
  10. Given def test(a, b, /, c, *, d):, which call is invalid?

    Answer(C) – because `a` and `b` are positional‑only, cannot be passed by keyword.

💻 Exercises – Practice Makes Perfect

Exercise 1: Default Argument
Write a function greet_user(name, title="Mr.") that returns a string like "Mr. John" or "Ms. Jane".

Sample Solution ```python def greet_user(name, title="Mr."): return f"{title} {name}"

print(greet_user("John")) # Mr. John print(greet_user("Jane", "Ms.")) # Ms. Jane

</details> **Exercise 2: Sum with Variable Arguments** Write a function `product(*numbers)` that returns the product of all numbers; if none, return `1`. <details><summary>Sample Solution</summary> ```python def product(*numbers): result = 1 for n in numbers: result *= n return result print(product(2, 3, 4)) # 24 print(product()) # 1

Exercise 3: Keyword‑Only Arguments
Write a function create_student(name, age, *, grade, school) that requires grade and school as keyword arguments. Return a dictionary.

Sample Solution ```python def create_student(name, age, *, grade, school): return {"name": name, "age": age, "grade": grade, "school": school}

student = create_student("Bob", 20, grade="A", school="MIT") print(student)

</details> **Exercise 4: Unpacking** Write a function `introduce_person(name, age, city)` and call it using a tuple unpacked with `*` and a dictionary unpacked with `**`. <details><summary>Sample Solution</summary> ```python def introduce_person(name, age, city): print(f"{name} is {age} years old and lives in {city}.") # using tuple person = ("Alice", 25, "London") introduce_person(*person) # using dict info = {"name": "Bob", "age": 30, "city": "Paris"} introduce_person(**info)

Exercise 5: Flexible Logging
Write a function log(level, message, **extras) that prints the level and message, then each extra key‑value pair.

Sample Solution ```python def log(level, message, **extras): print(f"[{level}] {message}") for key, value in extras.items(): print(f" {key}: {value}")

log("INFO", "User logged in", user_id=101, session="abc123")

</details> --- ### 🏠 Homework – Deeper Thinking #### Short Answer Questions **1. Arg Parser Helper** Write a function `make_url(base_url, *paths, **params)` that builds a URL. <details><summary>Sample Answer</summary> ```python def make_url(base_url, *paths, **params): if paths: base_url = base_url.rstrip('/') + '/' + '/'.join(paths) if params: base_url += '?' + '&'.join(f"{k}={v}" for k, v in params.items()) return base_url print(make_url("https://api.com", "v1", "users", id=5, sort="asc")) # https://api.com/v1/users?id=5&sort=asc

2. Function with Positional‑Only and Keyword‑Only
Write calculate(a, b, /, operation, *, round_result=False) that performs the operation and optionally rounds.

Sample Answer ```python def calculate(a, b, /, operation, *, round_result=False): if operation == "add": result = a + b elif operation == "subtract": result = a - b elif operation == "multiply": result = a * b elif operation == "divide": if b == 0: return None result = a / b else: raise ValueError("Invalid operation") return round(result, 2) if round_result else result ```

3. Decorator with *args and **kwargs
Write a decorator timer that measures and prints execution time.

Sample Answer ```python import time def timer(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) print(f"{func.__name__} took {time.time()-start:.4f}s") return result return wrapper ```

Essay Questions

4. Configuration Builder
Write a function create_config(**settings) that returns a dictionary with default settings overridden by keyword arguments.

Sample Answer ```python def create_config(**settings): defaults = {'debug': False, 'log_level': 'INFO', 'max_retries': 3} config = defaults.copy() config.update(settings) return config

print(create_config(debug=True)) print(create_config(log_level='ERROR', max_retries=5))

</details> **5. Arbitrary Argument Forwarding** Write a function `wrapper(func, *args, **kwargs)` that prints the function name and arguments, then calls and returns the result. <details><summary>Sample Answer</summary> ```python def wrapper(func, *args, **kwargs): print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}") return func(*args, **kwargs) def add(a, b, c=0): return a + b + c print(wrapper(add, 1, 2, c=3)) # prints and returns 6

Homework Hints

Summary

In this tutorial, you have learned:

Mastering these concepts allows you to write functions that are both flexible and clear, with intuitive calling interfaces that work well in a variety of scenarios.

Next Steps: In Tutorial 5, we will explore Lambda Functions and Functional Programming Tools like map, filter, and reduce.

Happy argument handling!

Previous | Tutorial index | Next