Python *args, **kwargs, Default Values, and Keyword Arguments
Think about ordering coffee. You always need to pick a size — that's required. But extras like oat milk, an extra shot, or caramel drizzle? Those are optional. You only mention them when you want them.
Python function arguments work the same way. Some are required, some have sensible defaults, and some let you pass in as many extras as you want. Mastering these patterns is what separates a beginner from someone who writes truly flexible code.
In this tutorial, you'll learn every way Python lets you pass data into functions — from basic positional arguments all the way to the powerful *args and **kwargs patterns used in real-world libraries.
Positional vs Keyword Arguments
When you call a function, Python matches arguments to parameters in two ways. Positional arguments are matched by their position — first argument goes to the first parameter. Keyword arguments are matched by name.
With keyword arguments, you write parameter_name=value. This makes the call self-documenting — anyone reading the code can see exactly what each value means, even without checking the function definition.
You can mix both styles in the same call, but positional arguments must come before keyword arguments.
How Default Values Make Functions Flexible
Default values let you define parameters that are optional. If the caller provides a value, it's used. If not, the default kicks in.
The size parameter has no default — it's required. The milk and sugar parameters have defaults, so they're optional. This is exactly how many real Python libraries work: a few required parameters and many optional ones with sensible defaults.
The Mutable Default Trap
This is one of Python's most famous gotchas. When you use a mutable object (like a list) as a default value, that object is created once when the function is defined — not each time the function is called.
That means every call shares the same list. Changes from one call carry over to the next. Let's see it in action:
Surprised? Each call is supposed to start with a fresh list, but the list keeps growing. This happens because [] is created once at function definition time and reused on every call.
The fix is a well-known Python pattern: use None as the default, and create a new list inside the function.
What Does *args Do?
Sometimes you don't know in advance how many arguments a function will receive. Maybe you want a function that adds up any amount of numbers — 2, 5, or 100.
The *args syntax collects all extra positional arguments into a tuple. You can then loop over them, index them, or do anything you'd do with a tuple.
The name args is just a convention. You could write *numbers, *items, or *values. The magic is in the * — it tells Python to pack all remaining positional arguments into a tuple.
You can combine *args with regular parameters. Regular parameters grab their values first, then *args collects everything left over.
What Does **kwargs Do?
While *args collects extra positional arguments, **kwargs collects extra keyword arguments into a dictionary. The keys are the argument names and the values are the argument values.
Like args, the name kwargs is just a convention. You could write **info, **options, or **settings. The ** is what matters — it tells Python to pack extra keyword arguments into a dictionary.
This is incredibly useful for functions that need to be flexible. You can pass any named data without changing the function definition.
Combining All Argument Types
You can use regular parameters, defaults, *args, and **kwargs in the same function. But the order matters. Python requires this specific sequence:
*args**kwargsdef process(*args, **kwargs):
# What does this expect?
passdef process(data, format='json',
verbose=False):
# Easy to understand
passPractice Exercises
What does this code print? Type the exact output.
def power(base, exponent=2):
print(base ** exponent)
power(5)
power(2, 10)
power(exponent=3, base=4)Write a function called greet that takes a name parameter and an optional greeting parameter with a default value of 'Hello'. It should print <greeting>, <name>!. For example, greet('Alice') prints Hello, Alice! and greet('Bob', 'Hey') prints Hey, Bob!.
Write a function called sum_all that uses *args to accept any number of numeric arguments and returns their sum. If no arguments are given, return 0.
This function is supposed to create a new list each time, but it has the mutable default bug. Fix it so each call starts with a fresh list.
Expected output:
[1]
[2]
[3]Write a function called build_profile that takes a required name parameter and uses **kwargs to accept any additional info. It should print the name on the first line, then each key-value pair on subsequent lines in the format key: value (two leading spaces). Print keys in the order they are passed.
Example: build_profile('Alice', age=25, city='NYC') should print:
Alice
age: 25
city: NYCWrite a function called calc that takes an operation string ('add', 'multiply', or 'max') and then any number of numeric arguments using *args. It should return the result:
'add': sum of all numbers (0 if none)'multiply': product of all numbers (1 if none)'max': largest number (0 if none)Examples: calc('add', 1, 2, 3) returns 6. calc('multiply', 2, 3, 4) returns 24.
Refactor the function below to use **kwargs instead of individual parameters. The function should accept name as a required parameter and everything else through **kwargs. Print the name, then print each detail on its own line in key: value format. The output must match exactly.
Original:
def show_student(name, grade, school, gpa):
print(name)
print('grade: ' + str(grade))
print('school: ' + school)
print('gpa: ' + str(gpa))Expected output for show_student('Alice', grade=10, school='Lincoln', gpa=3.8):
Alice
grade: 10
school: Lincoln
gpa: 3.8