Arguments: positional, keyword and default
Python gives you several ways to pass values into a function. Most of it is convenience, but one feature — the mutable default — is a genuine trap that catches people who have been writing Python for years.
Positional arguments
The default: values match parameters by position.
def describe(name, age, city):
print(f"{name}, {age}, from {city}")
describe("Priya", 28, "Pune")
Simple, and it depends entirely on getting the order right. With three parameters of different types a mistake usually crashes. With three strings, it quietly prints nonsense.
Keyword arguments
Name them at the call site and order stops mattering:
describe(age=28, city="Pune", name="Priya")
More typing, and much clearer at the point somebody reads it. Compare:
create_user("Priya", "priya@example.com", True, False, True)
with:
create_user(
name="Priya",
email="priya@example.com",
is_active=True,
is_admin=False,
send_welcome=True,
)
The first requires opening the definition to understand. Whenever an argument
is a bare True, False or a number whose meaning is not obvious, name it.
You can mix, but positional arguments must come first:
describe("Priya", city="Pune", age=28) # fine
describe(name="Priya", 28, "Pune") # SyntaxError
Default values
Give a parameter a default and callers may omit it:
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("Priya")
greet("Priya", "Good morning")
greet("Priya", greeting="Namaste")
Hello, Priya!
Good morning, Priya!
Namaste, Priya!
Parameters with defaults must come after those without:
def greet(greeting="Hello", name): # SyntaxError
Otherwise Python could not tell which value a single argument was meant for.
Defaults are excellent for widening a function without breaking existing callers — add a parameter with a sensible default and every existing call keeps working.
The mutable default trap
Here is the one to remember.
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("apple"))
print(add_item("banana"))
You would expect ['apple'] then ['banana']. You get:
['apple']
['apple', 'banana']
The default value is created once, when the function is defined — not each time it is called. There is one list, shared by every call that does not supply its own. It accumulates for the lifetime of the program.
This is the aliasing lesson again: one list, several uses, each seeing the others' changes.
The fix is always the same shape:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
None is immutable and safe as a default. A fresh list is created on each call
that needs one.
Rule: never use a list, dictionary or set as a default value. Use None and
create it inside. Strings, numbers, True, False and tuples are immutable and
perfectly safe.
This is also exactly why is None rather than if not basket — an empty list
passed deliberately is falsy, and if not basket would replace it with a
different empty list, quietly losing the connection to the caller's one.
Arguments are passed by reference
A closely related point:
def add_tax(prices):
for i in range(len(prices)):
prices[i] = prices[i] * 1.18
my_prices = [100, 200]
add_tax(my_prices)
print(my_prices)
[118.0, 236.0]
The function changed the caller's list. It did not receive a copy — it received another label on the same list.
For immutable values, nothing of the sort happens:
def increment(n):
n += 1
count = 5
increment(count)
print(count) # 5 — unchanged
n += 1 rebound the local name to a new number. The caller's count was never
touched, because numbers cannot be modified in place.
So: a function can modify a list or dictionary you pass it, and cannot modify a number or string. Which behaviour you want should be a decision, not a surprise. When in doubt, return a new value rather than modifying the argument:
def with_tax(prices):
return [p * 1.18 for p in prices]
Callers of that cannot be surprised.
Forcing keyword arguments
A bare * in the parameter list means everything after it must be named:
def create_user(name, *, is_admin=False, send_email=True):
...
create_user("Priya", is_admin=True) # fine
create_user("Priya", True) # TypeError
Worth using when a function takes several flags. It stops
create_user("Priya", True, False) at the door rather than letting it become a
bug.
There is a matching / that forces the arguments before it to be positional.
You will see it in library code; you will rarely need it.
Unpacking into a call
* and ** spread a collection into arguments:
def describe(name, age, city):
print(f"{name}, {age}, from {city}")
values = ["Priya", 28, "Pune"]
describe(*values)
details = {"name": "Priya", "age": 28, "city": "Pune"}
describe(**details)
Both print the same line. * spreads a sequence into positional arguments;
** spreads a dictionary into keyword arguments, matching keys to parameter
names.
Useful when data arrives as a dictionary — from JSON, say — and lines up with a
function's parameters. Be careful: a key that does not match a parameter gives
TypeError: got an unexpected keyword argument.
Practice
- Write
describe(name, age, city)and call it positionally, with keywords, and with a mix. Then call it in the wrong order positionally and notice it does not crash. - Add a default
country="India"and call it both with and without. - Try putting a defaulted parameter before a non-defaulted one. Read the error.
- Run the
add_itemtrap exactly as written. Call it three times. Explain in one sentence why the list grows, then fix it withNone. - Write
add_tax(prices)that modifies the caller's list, andwith_tax(prices)that returns a new one. Show the difference by printing the original after each. - Write
increment(n)that tries to change a number, and show it cannot. Explain why this differs from question 5. - Write
create_user(name, *, is_admin=False)and prove the*stops a positional second argument. - Build a dictionary of arguments and call a function with
**. Then add an extra key and read the error.
Next: return values in more depth, including returning several things at once.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship