Default and Keyword Arguments: Advanced Functions
Default arguments and keyword arguments are two of the most powerful features for writing flexible, readable functions. Default arguments make parameters optional by providing fallback values; keyword arguments let you call functions with explicit parameter names, improving clarity and eliminating argument-order confusion. Together, they enable you to write functions that scale from simple to complex use cases without creating multiple versions.
Key Takeaways:
- Default arguments provide fallback values, making parameters optional:
def greet(name, title=""): - Mutable defaults (
[],{}) are a common pitfall; useNoneinstead and create objects inside the function - Keyword arguments improve readability:
create_user(username="alice", is_active=True)is clearer than positional - In definitions, defaults must come after non-defaults; in calls, positionals must come before keywords
What Are Default Arguments and How Do You Use Them?
A default argument is a parameter that has a fallback value if the caller doesn't provide one. In the function definition, you assign a value using the = operator:
def greet(name, title=""):
"""Greets a person with an optional title."""
if title:
print(f"Hello, {title} {name}!")
else:
print(f"Hello, {name}!")
greet("Alice") # Uses default: title=""
greet("Dr. Smith", "Dr.") # Overrides default
greet("Professor Plum", title="Prof.") # Keyword argument with default
Output:
Hello, Alice!
Hello, Dr. Smith!
Hello, Professor Plum!
How it works: When you call the function, Python checks if you provided a value for each parameter. If you did, it uses your value. If you didn't, it uses the default from the definition.
The Golden Rule: Defaults Come Last
In a function definition, all parameters with defaults must come after all parameters without defaults. This prevents ambiguity:
# CORRECT: required parameter first, then optional
def create_user(username, is_active=True, email_verified=False):
pass
# WRONG: would cause SyntaxError
# def create_user(is_active=True, username, email_verified=False):
# pass
If Python allowed defaults before required parameters, it would be impossible to know which arguments are positional and which are optional.
What Is the Mutable Default Argument Pitfall?
This is one of the most dangerous and confusing gotchas in Python. Default values are created once when the function is defined, not each time it's called. This is fine for immutable types (numbers, strings, tuples), but catastrophic for mutable types (lists, dictionaries, sets).
The Problem Demonstrated
def add_item(item, target_list=[]):
"""Dangerous: list is created once and reused."""
target_list.append(item)
print(f"Items: {target_list}")
add_item("apple") # Items: ['apple']
add_item("banana") # Items: ['apple', 'banana'] (NOT ['banana']!)
add_item("cherry") # Items: ['apple', 'banana', 'cherry']
Every call modifies the same list object because [] is evaluated once at function definition time, then reused for every call. The function doesn't create a fresh list each time—it mutates the original.
The Solution: Use None as the Default
The Pythonic pattern is to use None as the default, then create a fresh mutable object inside the function:
def add_item(item, target_list=None):
"""Safe: creates a new list only when needed."""
if target_list is None:
target_list = []
target_list.append(item)
print(f"Items: {target_list}")
add_item("apple") # Items: ['apple']
add_item("banana") # Items: ['banana']
add_item("cherry") # Items: ['cherry']
Now each call gets its own independent list because we create a fresh [] whenever None is passed.
Why This Happens
In Python, default values are evaluated once, at function definition time, not at call time. This is an optimization—default values are computed once and stored in the function object. For immutable values this is invisible; for mutable objects, you mutate the shared default.
Best Practice: Never use mutable defaults. Always use None and instantiate inside the function.
How Do Keyword Arguments Improve Function Calls?
A keyword argument is where you explicitly name the parameter when calling the function: parameter=value. This makes your intent crystal clear and frees you from memorizing parameter order.
Keyword vs. Positional Arguments
def create_db_connection(host, port, username, password, timeout=30):
print(f"Connecting to {host}:{port} as {username}...")
print(f"Timeout: {timeout} seconds")
# Positional: What does 3306 mean? Which string is which?
create_db_connection("localhost", 3306, "admin", "s3cr3t")
# Keyword: Self-documenting and clear
create_db_connection(
host="localhost",
port=3306,
username="admin",
password="s3cr3t"
)
# With keywords, order doesn't matter
create_db_connection(
password="s3cr3t",
username="admin",
host="localhost",
port=3306
)
Benefits of Keyword Arguments
- Readability: Code is self-documenting; you know exactly what each value represents.
- Flexibility: You don't memorize parameter order. Adding new parameters doesn't break old calls (if they use keywords).
- Scalability: Functions with 5+ parameters become unreadable with positional arguments; keywords solve this.
How Do You Combine Positional, Keyword, and Default Arguments?
You can mix all three, but there's a critical ordering rule: all positional arguments must come before any keyword arguments in the function call.
def create_db_connection(host, port, username, password, timeout=30, ssl=True):
pass
# CORRECT: positional first, then keyword
create_db_connection("localhost", 3306, password="secret", username="admin")
create_db_connection("localhost", 3306, "admin", "secret", timeout=60)
# WRONG: positional after keyword raises SyntaxError
# create_db_connection(host="localhost", 3306, "admin", "secret")
In the definition, defaults come last. In the call, positionals come first.
Practical Examples
Configuration function with defaults:
def configure_logger(level="INFO", filename="app.log", max_size=10_000_000):
"""Configure the application logger."""
print(f"Level: {level}, File: {filename}, Max: {max_size}")
configure_logger() # Uses all defaults
configure_logger(level="DEBUG") # Override one default
configure_logger(level="DEBUG", max_size=50_000_000) # Override multiple
API client with required and optional parameters:
def send_request(url, method="GET", headers=None, timeout=5, retries=3):
"""Send an HTTP request with sensible defaults."""
if headers is None:
headers = {}
print(f"{method} {url} (timeout={timeout}s, retries={retries})")
send_request("https://api.example.com/users") # All defaults
send_request("https://api.example.com/users", method="POST", headers={"Content-Type": "application/json"})
Key Takeaways
- Default Arguments: Provide fallback values with
parameter=valuesyntax, making parameters optional. - Parameter Order: In definitions, required parameters must come before default parameters.
- Mutable Defaults Trap: Never use
[],{}, or other mutable objects as defaults; useNoneand create them inside the function. - Keyword Arguments: Use
parameter=valuein function calls to improve readability and eliminate order dependency. - Call Order: Mix positional and keyword arguments freely, but positionals must come before keywords.
- Self-Documenting Code: Keyword arguments serve as inline documentation, making code easier to maintain.
Frequently Asked Questions
Why does the mutable default argument problem exist?
Python evaluates default arguments once at function definition time (not call time) as an optimization. For immutable values this is invisible. For mutable objects, all calls share the same object, leading to unexpected mutations. This is a fundamental language design choice; the workaround is to use None and create objects inside the function.
Can I use a list comprehension or function call as a default value?
Technically yes, but it's evaluated at definition time, not call time. For example, def foo(x=[1, 2, 3]): creates the list once. def foo(x=list(range(3))): also creates it once. If you want a fresh list per call, use None and create it inside the function. If you need a computed default like "today's date," use None and compute it inside.
What is the difference between None and an empty list as a default?
None is a sentinel value meaning "not provided." It allows you to detect when the caller didn't pass an argument, so you can create a fresh object. An empty list [] is a specific value; all callers who don't provide an argument get the same list. Always use None as the sentinel.
Can you use *args or **kwargs with default arguments?
Yes. *args (variable positional arguments) and **kwargs (variable keyword arguments) can coexist with default arguments. Order is important: required parameters first, then defaults, then *args, then keyword-only parameters, then **kwargs. For example: def func(req, opt=10, *args, kw_only, **kwargs):.
How do you make a parameter keyword-only?
In Python 3, place a * in the parameter list. Everything after * must be passed by keyword:
def func(a, b, *, c, d=10):
pass
func(1, 2, c=3) # OK
func(1, 2, 3, 4) # ERROR: c and d must be keywords
func(1, 2, c=3, d=4) # OK