WasiLearn Code
Python · Functions

Default and named arguments

A function that always needs every argument spelled out gets tiring to call. Defaults let you name the common case once, and keyword arguments let the call site say what it means.

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}"

print(greet("Ayesha"))
print(greet("Bilal", "Good morning"))
output
Hello, Ayesha
Good morning, Bilal

One function, two ways to call it. The default is used when the argument is left out, and overridden when it is supplied.

Defaults must come last

def order(item, quantity=1, size="regular"):
    return f"{quantity} {size} {item}"

print(order("chai"))
print(order("chai", 3))
print(order("chai", 3, "large"))
output
1 regular chai
3 regular chai
3 large chai

Every parameter with a default has to come after every parameter without one. Otherwise a call could not tell which argument it was supplying:

def broken(quantity=1, item):
    return item
SyntaxError: parameter without a default follows parameter with a default

Python refuses at the point the file is read, before anything runs.

Keyword arguments

You can name arguments at the call site, in any order.

def order(item, quantity=1, size="regular"):
    return f"{quantity} {size} {item}"

print(order("chai", size="large"))
print(order(size="large", item="chai"))
print(order("chai", quantity=2, size="small"))
output
1 large chai
1 large chai
2 small chai

The first line is the real win: it skips quantity and sets size without having to supply the one in between.

Naming also makes calls readable. Compare:

create_user("Ayesha", True, False, True)
create_user("Ayesha", is_admin=True, is_banned=False, send_email=True)

The first is a puzzle at every call site. The second explains itself, and misordering it is impossible.

The one rule: positional arguments come first. order(item="chai", 2) is a syntax error, because Python cannot tell where the 2 belongs.

The mutable default trap

This is the classic Python gotcha, and it is worth meeting deliberately rather than in production.

You want a function that adds an item to a list, defaulting to a new empty list. The obvious spelling is wrong.

def add_item(item, basket=[]):
    basket.append(item)
    return basket

print(add_item("chai"))
print(add_item("paratha"))
print(add_item("samosa"))
output
['chai']
['chai', 'paratha']
['chai', 'paratha', 'samosa']

Every call was supposed to start with an empty basket. Instead each one inherited everything from the calls before it, including calls from completely unrelated parts of the program.

The reason is precise: the default value is evaluated once, when the def line runs, not each time the function is called. That [] is created a single time and becomes a permanent attachment to the function. Every call that omits basket gets the same list object and appends to it.

You can see the attachment directly:

def add_item(item, basket=[]):
    basket.append(item)
    return basket

add_item("chai")
print(add_item.__defaults__)
output
(['chai'],)

The default is no longer empty. The function is carrying state it was never meant to have.

The fix is to default to None and build the real value inside:

def add_item(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket

print(add_item("chai"))
print(add_item("paratha"))
print(add_item("samosa", ["lassi"]))
output
['chai']
['paratha']
['lassi', 'samosa']

Each call now gets a fresh list, and passing one in still works.

The rule: never use a list, dictionary or set as a default. Use None and check for it. Immutable defaults — numbers, strings, True, False, None, tuples — are completely safe, because nothing can modify them in place.

The same "evaluated once" rule catches other things. A default of datetime.now() is frozen at the moment the module loaded, not the moment the function was called, which produces timestamps that are all identical and all wrong.

Accepting any number of arguments

*args collects extra positional arguments into a tuple. **kwargs collects extra named ones into a dictionary.

def total(*prices):
    return sum(prices)

print(total(250, 120))
print(total(250, 120, 80))
print(total())
output
370
450
0
def describe(**details):
    for key, value in details.items():
        print(f"{key}: {value}")

describe(name="Ayesha", city="Karachi")
output
name: Ayesha
city: Karachi

The stars are the syntax; args and kwargs are only conventional names. All four can appear together, and the order is fixed:

def report(title, *items, separator=", ", **options):
    line = separator.join(items)
    return f"{title}: {line} {options}"

print(report("Order", "chai", "paratha", separator=" + ", urgent=True))
output
Order: chai + paratha {'urgent': True}

Anything after *args can only be given by keyword — which is a useful trick in itself.

Forcing keyword-only arguments

A bare * in the parameter list means "everything after this must be named".

def transfer(amount, *, to_account, confirm=False):
    return f"{amount} to {to_account}, confirmed={confirm}"

print(transfer(500, to_account="ACC-9", confirm=True))
output
500 to ACC-9, confirmed=True

Calling transfer(500, "ACC-9", True) now fails. For a function where a misordered argument would be expensive — money, deletion, permissions — that refusal is worth the extra typing at every call site.

Unpacking at the call site

The same stars work in reverse, spreading a collection into arguments.

def order(item, quantity=1, size="regular"):
    return f"{quantity} {size} {item}"

args = ["chai", 2]
opts = {"item": "lassi", "size": "large"}

print(order(*args))
print(order(**opts))
output
2 regular chai
1 large lassi

* spreads a list into positional arguments, ** spreads a dictionary into named ones. This is how a wrapper function passes along whatever it was given without knowing the details.

What to carry away

  • Defaults go last, and are evaluated once at definition time.
  • Never default to a list, dict or set — use None.
  • Name arguments at the call site whenever the meaning is not obvious.
  • * in the parameter list forces the rest to be keyword-only.

Next: scope, which covers where the variables inside a function actually live.

Prefer learning with a teacher?

WasiLearn Academy runs small-group classes covering this material, online and in Karachi.

See the classes