Technology  /  Python

๐Ÿ Python 78 guides ยท updated 2026

From first variable to OOP, generators, and real projects โ€” the language that runs everything from data pipelines to AI agents, taught the practical way.

Default and Keyword Arguments in Python: Flexible APIs and the Mutable Default Trap

Well-designed Python functions balance flexibility (callers can customise behaviour) with convenience (callers do not have to specify everything every time). Default arguments and keyword arguments are the two main tools for achieving this balance. They also carry one of the most notorious bugs in Python โ€” the mutable default argument โ€” that trips up even experienced developers.

Default Arguments

A default argument provides a fallback value when the caller does not supply one.

def connect(host, port=5432, timeout=30, ssl=True):
print(f"Connecting to {host}:{port} (timeout={timeout}s, ssl={ssl})")
# Most callers only need to specify the host
connect("db.example.com")
# Connecting to db.example.com:5432 (timeout=30s, ssl=True)
# Override what you need
connect("db.example.com", port=3306, ssl=False)
# Connecting to db.example.com:3306 (timeout=30s, ssl=False)

Default values are evaluated once when the function is defined, not each time the function is called. For immutable values like integers, strings, tuples, and None, this is fine. For mutable values, it causes problems.

The Mutable Default Argument Bug

This is one of the most frequently encountered Python gotchas:

def add_item(item, collection=[]): # Bug: list created once
collection.append(item)
return collection
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] โ€” wrong! should be ['banana']
print(add_item("cherry")) # ['apple', 'banana', 'cherry'] โ€” accumulating

The same list object is used across all calls because it was created when the function was defined. Every call that does not pass collection modifies the same list.

The standard fix is to use None as the default and create the mutable object inside the function:

def add_item(item, collection=None):
if collection is None:
collection = [] # new list on each call
collection.append(item)
return collection
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['banana'] โ€” correct
print(add_item("cherry")) # ['cherry'] โ€” correct
# Can still pass an existing collection
my_list = ["existing"]
print(add_item("new", my_list)) # ['existing', 'new']

This applies to any mutable default: lists, dicts, sets, and custom mutable objects. Use None for all of them.

Keyword Arguments: Passing by Name

Any parameter can be passed by name (as a keyword argument), regardless of its position in the function signature.

def create_user(username, email, role="reader", active=True):
print(f"Creating user: {username}, {email}, role={role}, active={active}")
# Positional
create_user("alice", "alice@example.com", "admin", True)
# Keyword โ€” order doesn't matter
create_user(email="bob@example.com", username="bob", active=False)
# Mix: positional first, then keyword
create_user("carol", "carol@example.com", active=False)

Keyword arguments make call sites self-documenting. When a function has several boolean or similar parameters, seeing active=False, notify=True is clearer than seeing False, True and having to count positions.

Keyword-Only Parameters

Parameters that appear after *args (or after a bare *) can only be passed by keyword โ€” callers cannot use positional syntax for them.

def send_message(recipient, message, *, priority="normal", encrypt=False):
# priority and encrypt are keyword-only
print(f"To: {recipient}")
print(f"Priority: {priority}, Encrypted: {encrypt}")
print(f"Message: {message}")
send_message("alice", "Hello!", priority="high", encrypt=True)
# This raises TypeError โ€” priority can't be positional after *
# send_message("alice", "Hello!", "high")

Keyword-only parameters are useful when a function has arguments that change semantics and you want callers to name them explicitly. They prevent mistakes like accidentally passing a priority where a boolean was expected.

A bare * (with no name) can force keyword-only without collecting extra positional arguments:

def resize(image, *, width, height, keep_aspect=True):
# width and height are required keyword-only โ€” no default
pass
resize(img, width=800, height=600)
# resize(img, 800, 600) # TypeError

Positional-Only Parameters (Python 3.8+)

Python 3.8 added the / separator to mark parameters as positional-only โ€” callers cannot pass them by keyword name.

def distance(x1, y1, x2, y2, /):
# All four are positional-only
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print(distance(0, 0, 3, 4)) # 5.0
# distance(x1=0, y1=0, x2=3, y2=4) # TypeError

Positional-only parameters are useful in library APIs where the parameter names are implementation details you do not want to commit to.

The Full Parameter Order

A function signature can use all these features, in a specific order:

def full_example(pos_only, /, standard, *args, kw_only, **kwargs):
pass

Order:

  1. Positional-only parameters (before /)
  2. Standard parameters
  3. *args
  4. Keyword-only parameters (after * or *args)
  5. **kwargs

In practice, most functions use a small subset of these. The common patterns are:

def simple(a, b, c=10): # positional + default
def flexible(a, *args, **kwargs): # collect extras
def strict(a, *, b, c=10): # keyword-only

Default Values and Mutability: A Summary

Default typeSafe?Example
NoneYesdef f(x=None)
IntegerYesdef f(n=0)
StringYesdef f(s="")
TupleYesdef f(t=())
ListNodef f(lst=[]) โ€” use None instead
DictNodef f(d={}) โ€” use None instead
Custom objectDependsRisky if mutable

Common Mistakes

Putting keyword-only parameters before *args. Parameters before *args are positional. Only parameters after *args (or after a bare *) are keyword-only.

Providing defaults for keyword-only parameters that must be required. If callers must provide a value, do not give it a default:

def process(data, *, output_format): # required keyword-only โ€” no default
pass
process([1, 2, 3], output_format="csv")
# process([1, 2, 3]) # TypeError: missing keyword argument 'output_format'

Ordering non-default arguments after default arguments. This is a SyntaxError:

def bad(a=10, b): # SyntaxError โ€” b has no default but comes after one
pass

Non-default parameters must come before parameters with defaults.