All 57 lessons
Lesson 18 of 57 · Functions
Python Function Arguments: Defaults, Keywords, *args and **kwargs
By Skillancy Editorial Team
Run the code in the free Python Compiler or on your own computer. Lessons that read files use the FreshBasket practice data: download the dataset (ZIP).
In the last lesson every function took a fixed list of inputs in a fixed order. Real functions need more flexibility. A report function might take a required data set, an optional city filter, and a number of formatting options. A logging helper might accept any number of messages. Python gives you a rich set of tools for this: default values, keyword arguments, and the special *args and **kwargs collectors.
Knowing these tools also makes you better at reading other people's code and library documentation. When you see pd.read_csv(path, sep=",", parse_dates=["order_date"]), you are looking at positional and keyword arguments working together. By the end of this lesson, signatures like that will be second nature, and you will know about one famous bug (the mutable default argument) that catches many experienced developers.
Worked examples
Example 1: Positional and keyword arguments
Question: Call the same function in two different styles.
def delivery_fee(distance_km, order_value):
if order_value >= 499:
return 0
return 15 + 5 * distance_km
print(delivery_fee(3, 250))
print(delivery_fee(order_value=250, distance_km=3))
print(delivery_fee(3, order_value=250))Output
30
30
30How it works: With positional arguments the order matters. With keyword arguments you name each parameter, so the order does not matter and the call documents itself. You can mix the styles, but positional ones must come first.
Reading the result: All three calls gave the same fee of ₹30. Keyword arguments make calls with many numbers much easier to read: delivery_fee(order_value=250, distance_km=3) leaves no doubt about which number is which.
Example 2: Default values
Question: Make the GST rate optional, defaulting to 5%.
def with_gst(price, rate=5):
return round(price * (1 + rate / 100), 2)
print(with_gst(200))
print(with_gst(200, 12))
print(with_gst(200, rate=18))Output
210.0
224.0
236.0How it works: A parameter with = value in the definition is optional. If the caller omits it, the default is used. Parameters with defaults must come after those without.
Reading the result: The default 5% rate gives 210.0, while 12% gives 224.0 and 18% gives 236.0. Choose defaults that are right for the most common case.
Example 3: A realistic function with several options
Question: Write a labelling function with optional prefix, suffix and width.
def label(name, value, prefix="", suffix="", width=18):
text = f"{name}:"
return f"{text:<{width}}{prefix}{value}{suffix}"
print(label("Orders", 6000))
print(label("Revenue", "35.3", prefix="₹", suffix=" L"))
print(label("Avg delivery", 28.4, suffix=" min", width=24))Output
Orders: 6000
Revenue: ₹35.3 L
Avg delivery: 28.4 minHow it works: Callers override only the options they care about, using keywords. This is how most library functions are designed: required inputs first, many optional settings after.
Reading the result: Each line used a different mix of options, for example Revenue got a rupee prefix and an L suffix. Because options are passed by keyword, adding a new option later does not break existing calls.
Example 4: Keyword-only and positional-only parameters
Question: How can a function force callers to name an argument?
def fee(distance_km, order_value, *, express=False):
base = 0 if order_value >= 499 else 15 + 5 * distance_km
return base + (30 if express else 0)
print(fee(2, 300))
print(fee(2, 300, express=True))
try:
fee(2, 300, True)
except TypeError as e:
print("TypeError:", e)Output
25
55
TypeError: fee() takes 2 positional arguments but 3 were givenHow it works: A bare * in the parameter list means everything after it must be passed by keyword. That stops unclear calls like fee(2, 300, True), where nobody can tell what True means. There is a matching / marker for positional-only parameters, used by some built-ins.
Reading the result: The standard fee is ₹25 and the express fee is ₹55. The unnamed True is rejected with a clear error.
Example 5: *args collects extra positional arguments
Question: Write an average function that accepts any number of values.
def average(*numbers):
print(type(numbers), numbers)
return sum(numbers) / len(numbers)
print(average(10, 20))
print(average(68, 45, 96, 62))Output
<class 'tuple'> (10, 20)
15.0
<class 'tuple'> (68, 45, 96, 62)
67.75How it works: *numbers gathers all the positional arguments into a tuple. The name args is only a convention; *numbers is clearer here. Inside the function you use it like any tuple.
Reading the result: The two-value call gave 15.0 and the four-value call gave 67.75. The same function handled both because of the star.
Example 6: kwargs collects extra keyword arguments
Question: Accept any number of named options and display them.
def make_filter(**conditions):
print(type(conditions), conditions)
return " AND ".join(f"{k} = {v!r}" for k, v in conditions.items())
print(make_filter(city="Pune", status="Delivered"))
print(make_filter(is_prime=True))Output
<class 'dict'> {'city': 'Pune', 'status': 'Delivered'}
city = 'Pune' AND status = 'Delivered'
<class 'dict'> {'is_prime': True}
is_prime = TrueHow it works: **conditions gathers all keyword arguments that were not matched by named parameters into a dictionary. This is useful when you want to forward options to another function or build something dynamic, such as a SQL-like condition.
Reading the result: The first call produced city = 'Pune' AND status = 'Delivered'. The full signature order is: normal parameters, *args, keyword-only parameters, then **kwargs.
Example 7: Unpacking into a call
Question: Call a function using values stored in a list and a dictionary.
def delivery_fee(distance_km, order_value):
if order_value >= 499:
return 0
return 15 + 5 * distance_km
args = (4, 350)
kwargs = {"distance_km": 4, "order_value": 650}
print(delivery_fee(*args))
print(delivery_fee(**kwargs))
nums = [3, 7, 5]
print(max(*nums), min(*nums))Output
35
0
7 3How it works: At the call site, *sequence spreads the items out as positional arguments and **mapping spreads the pairs out as keyword arguments. It is the mirror image of *args and **kwargs in a definition. Unpacking is helpful when arguments come from a file or from user settings.
Reading the result: The list produced a fee of ₹35 and the dictionary produced ₹0, because the second order qualified for free delivery.
Example 8: Forwarding arguments
Question: Write a wrapper that adds timing information to any function.
import time
def timed(func, *args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
return result, elapsed
result, secs = timed(sum, range(1_000_000))
print(result)
print(secs < 5)
result2, _ = timed(sorted, [3, 1, 2], reverse=True)
print(result2)Output
499999500000
True
[3, 2, 1]How it works: timed accepts a function and whatever arguments that function needs, then passes them along with *args, **kwargs. This forwarding pattern is the foundation of decorators, which you will meet in the advanced module.
Reading the result: The sum was 499999500000 and the sorted list came back as [3, 2, 1]. Functions are values, so you can pass them as arguments just like numbers.
Example 9: Type hints
Question: How do you tell readers what types a function expects?
def apply_discount(price: float, pct: float = 10.0) -> float:
"""Return price after a percentage discount."""
return round(price * (1 - pct / 100), 2)
print(apply_discount(250))
print(apply_discount.__annotations__)Output
225.0
{'price': <class 'float'>, 'pct': <class 'float'>, 'return': <class 'float'>}How it works: price: float and -> float are type hints. Python does not enforce them while running, but editors and checkers like mypy use them to catch mistakes, and they act as built-in documentation. They are optional, though most professional code uses them.
Reading the result: A 10% discount on ₹250 gives 225.0. The hints are stored in __annotations__, but nothing stops you from passing the wrong type at runtime.
Example 10: The mutable default trap
Question: Why is def add(item, bucket=[]) dangerous?
def add_bad(item, bucket=[]):
bucket.append(item)
return bucket
print(add_bad("milk"))
print(add_bad("eggs"))
def add_good(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(add_good("milk"))
print(add_good("eggs"))Output
['milk']
['milk', 'eggs']
['milk']
['eggs']How it works: Default values are created once, when the function is defined, not on every call. So the same default list is shared by all calls that omit bucket. The fix is to use None as the default and create a fresh list inside the function.
Reading the result: The bad version remembered milk and returned ['milk', 'eggs', 'bread'] on the third call, while the good version gave ['bread'] every time. Never use a list, dictionary or set as a default value.
Common mistakes
1. Non-default parameter after a default one.
def f(rate=5, price):
return priceOutput
File "example.py", line 1
def f(rate=5, price):
^^^^^
SyntaxError: parameter without a default follows parameter with a default2. Passing the same argument twice.
def delivery_fee(distance_km, order_value):
return 0
delivery_fee(3, distance_km=3)Output
Traceback (most recent call last):
File "example.py", line 3, in <module>
delivery_fee(3, distance_km=3)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
TypeError: delivery_fee() got multiple values for argument 'distance_km'3. Using a mutable default such as [] or {}.
4. Forgetting the star when forwarding. func(args) passes one tuple; func(*args) passes its contents.
*5. Using `args` when a list parameter would be clearer.** If callers already have a list, make the parameter a list.
Practice questions
Practice 1 (Easy): Greeting with a default
Write greet(name, greeting="Hello") that returns something like Hello, Aarav!. Call it with and without the second argument.
Show solution
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Aarav"))
print(greet("Diya", greeting="Namaste"))Output
Hello, Aarav!
Namaste, Diya!The default saves callers from typing the common case: Hello, Aarav!.
Practice 2 (Medium): A flexible total
Write total(*prices, tax=0) that adds all the prices and then adds a percentage tax. Call it as total(100, 50, 25) and total(100, 50, 25, tax=10).
Show solution
def total(*prices, tax=0):
return round(sum(prices) * (1 + tax / 100), 2)
print(total(100, 50, 25))
print(total(100, 50, 25, tax=10))Output
175.0
192.5Anything after *prices is keyword-only, so tax must be named. The two answers are 175.0 and 192.5.
Practice 3 (Hard): A safe filter builder
Write describe_order(order_id, **details) that returns a string like O1 | city=Pune | amount=450 with details sorted alphabetically by key. Then call it using a dictionary through **.
Show solution
def describe_order(order_id, **details):
parts = [f"{k}={v}" for k, v in sorted(details.items())]
return " | ".join([order_id] + parts)
info = {"city": "Pune", "amount": 450}
print(describe_order("O1", **info))
print(describe_order("O2"))Output
O1 | amount=450 | city=Pune
O2Sorting makes the output predictable regardless of the order the caller used. The first line is O1 | amount=450 | city=Pune, and with no details the result is just O2.
Key takeaways
- Positional arguments go by order; keyword arguments go by name and read better.
- Defaults make parameters optional; put them after the required ones.
- A bare
*forces the following parameters to be keyword-only. *argscollects extras into a tuple;**kwargscollects named extras into a dictionary.*seqand**mappingin a call spread values into arguments.- Type hints document intent but are not enforced.
- Never use a mutable default; use
Noneand create the object inside.
Frequently asked questions
What is the difference between * in a definition and * in a call?
In a definition, *args collects extras into a tuple. In a call, *seq spreads a sequence into separate arguments. They are opposite operations with the same symbol.
Do I have to call the variables args and kwargs?
No, only the stars matter. The names args and kwargs are a strong convention, but a descriptive name such as *prices is often better.
When should I use keyword-only parameters?
When a parameter is a flag or option whose meaning would be unclear as a bare value, like True or 0. Forcing the name makes calls readable.
Are type hints required?
No. They are optional documentation checked by external tools. They are very valuable in larger projects, though.
Why does Python evaluate defaults only once?
The default is part of the function object, created when def runs. It is a design choice that is efficient but surprises beginners with mutable values.
Can I change the arguments I receive?
You can rebind a local name freely, but changing a mutable object (like a list) passed in modifies the caller's object too. Copy it first if you need to keep the original untouched.
Keep learning Python
One email a week with Python and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.
Ready to practise? Try the Python interview questions