RizTech Academy logo
RizTech Academy
FunctionsLesson 2 of 725 min

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

  1. 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.
  2. Add a default country="India" and call it both with and without.
  3. Try putting a defaulted parameter before a non-defaulted one. Read the error.
  4. Run the add_item trap exactly as written. Call it three times. Explain in one sentence why the list grows, then fix it with None.
  5. Write add_tax(prices) that modifies the caller's list, and with_tax(prices) that returns a new one. Show the difference by printing the original after each.
  6. Write increment(n) that tries to change a number, and show it cannot. Explain why this differs from question 5.
  7. Write create_user(name, *, is_admin=False) and prove the * stops a positional second argument.
  8. 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