Python Basics — Default & Keyword Arguments

2 minute read

Default & Keyword Arguments

Default values let you make parameters optional. Keyword arguments let you pass values by name in any order.

Default Parameter Values

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

print(greet("Alice"))             # Hello, Alice!
print(greet("Bob", "Good morning"))  # Good morning, Bob!

Default parameters must come after non-default parameters:

# ✅ Correct
def power(base, exponent=2):
    return base ** exponent

# ❌ SyntaxError
# def power(base=10, exponent):
#     return base ** exponent

Mutable Default Argument Trap

This is one of Python’s most infamous gotchas:

# ❌ DANGEROUS — mutable default is shared between calls
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['a', 'b']  ← unexpected!
print(add_item("c"))  # ['a', 'b', 'c']  ← keeps accumulating

The default list is created once when the function is defined, not on each call.

Fix: Use None as Default

# ✅ SAFE pattern
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['b']  ← independent calls

Keyword Arguments

Pass arguments by name (in any order):

def create_user(name, age, city):
    return {"name": name, "age": age, "city": city}

# Positional
user1 = create_user("Alice", 30, "NYC")

# Keyword (any order)
user2 = create_user(city="London", name="Bob", age=25)

# Mixed (positional first, then keyword)
user3 = create_user("Charlie", city="Paris", age=35)

Positional-Only Parameters (/)

Python 3.8+ lets you force parameters to be positional-only:

def divide(a, b, /):
    return a / b

print(divide(10, 3))   # ✅ 3.333...
# divide(a=10, b=3)    # ❌ TypeError

Keyword-Only Parameters (*)

Parameters after * must be passed as keywords:

def connect(host, port, *, timeout=30, retries=3):
    print(f"Connecting to {host}:{port}")
    print(f"Timeout: {timeout}s, Retries: {retries}")

connect("localhost", 8080)                    # ✅
connect("localhost", 8080, timeout=60)        # ✅
# connect("localhost", 8080, 60)             # ❌ TypeError

Argument Order Rules

The full parameter order:

def example(pos_only, /, normal, *, kw_only):
    pass

# pos_only  → must be positional
# normal    → positional or keyword
# kw_only   → must be keyword
def full_example(a, b, /, c, d, *, e, f):
    print(a, b, c, d, e, f)

full_example(1, 2, 3, d=4, e=5, f=6)  # ✅
full_example(1, 2, c=3, d=4, e=5, f=6)  # ✅

Practical Examples

Configuration Functions

def setup_logging(level="INFO", *, filename=None, format="%(message)s"):
    print(f"Level: {level}")
    if filename:
        print(f"Logging to: {filename}")
    print(f"Format: {format}")

setup_logging()
setup_logging("DEBUG", filename="app.log")

API Wrapper

def fetch_data(url, *, method="GET", headers=None, timeout=30):
    if headers is None:
        headers = {}
    print(f"{method} {url} (timeout={timeout}s)")
    return {"status": 200}

fetch_data("https://api.example.com/users")
fetch_data("https://api.example.com/users", method="POST", timeout=60)

Summary

  • Default values make parameters optional: def f(x, y=10):
  • Never use mutable defaults (lists, dicts, sets) — use None instead
  • Keyword arguments: f(name="Alice", age=30)
  • Positional-only (/): def f(x, /):
  • Keyword-only (*): def f(*, x, y):
  • Order: positional-only → normal → keyword-only

Next up: *args & **kwargs →

Updated: