Functions & Scope
Default Arguments
Optional Parameters
Your greet() function usually says "Hello" but sometimes needs "Hi" or "Hey".
Default arguments let callers omit parameters when the default is fine, while
still allowing customization when needed.
Basic default values
Give parameters default values.
def main():
print("=== Default Arguments ===\n")
# Without default - must provide both
greet_formal("Alice", "Good morning")
# With default - greeting is optional
greet("Bob") # Uses default "Hello"
greet("Charlie", "Hey") # Overrides default
greet("Diana", "Good evening")
print("\n=== Common Pattern ===")
# Default makes common case simple
# Special cases still possible
greet("Everyone") # 90% of calls use default
greet("VIP", "Welcome, dear") # 10% need custom
def greet_formal(name, greeting):
"""Both arguments required."""
print(f"{greeting}, {name}!")
def greet(name, greeting="Hello"):
"""greeting has default value."""
print(f"{greeting}, {name}!")
def greet_polite(name, greeting="Hello", punctuation="!"):
"""Multiple defaults."""
print(f"{greeting}, {name}{punctuation}")
if __name__ == "__main__":
main()
Parameters with defaults can be omitted when calling the function.
Keyword arguments skip positional defaults
Use keyword syntax to specify later parameters.
def main():
print("=== Keyword Arguments ===\n")
# Positional: order matters
print("Positional order:")
describe_pet("Hamster", "Harry")
describe_pet("Dog", "Buddy")
# Keyword: name=value, order doesn't matter
print("\nKeyword arguments:")
describe_pet(pet_name="Whiskers", animal_type="Cat")
describe_pet(animal_type="Fish", pet_name="Nemo") # Reversed!
# Mix positional and keyword
print("\nMixed:")
describe_pet("Rabbit", pet_name="Thumper")
# Skip middle defaults with keywords!
print("\nSkipping defaults:")
format_text("hello world", uppercase=True) # Skip width
format_text("hello world", width=20) # Skip uppercase
format_text("hello world", uppercase=True, width=15) # All specified
def describe_pet(animal_type, pet_name):
"""Describe a pet."""
print(f" I have a {animal_type} named {pet_name}.")
def format_text(text, width=0, uppercase=False):
"""Format text with optional width and case."""
result = text.upper() if uppercase else text
if width > 0:
result = result.center(width)
print(f" '{result}'")
if __name__ == "__main__":
main()
func(c=30) skips a and b if they have defaults.
Multiple default parameters
Functions can have several parameters with defaults.
def main():
print("=== Multiple Default Arguments ===\n")
# Using all defaults
print("All defaults:")
make_coffee()
# Override some
print("\nCustom orders:")
make_coffee(size="large")
make_coffee(milk=True)
make_coffee(sugar=2)
make_coffee(size="small", sugar=3)
# Override all
print("\nFully customized:")
custom_size =
sugar_count =
make_coffee(custom_size, True, sugar_count) # positional
make_coffee(size=custom_size, milk=True, sugar=sugar_count) # keyword
# Configuration pattern
print("\n=== Configuration Pattern ===")
print_report()
print_report(title="Sales Report", columns=3)
print_report(show_header=True, title="Status")
def make_coffee(size="medium", milk=False, sugar=0):
"""Make coffee with customizable options."""
order = f" {size.capitalize()} coffee"
if milk:
order += " with milk"
if sugar > 0:
order += f", {sugar} sugar(s)"
print(order)
def print_report(title="Report", columns=2, show_header=False):
"""Print a configurable report."""
if show_header:
print(f" === {title} ===")
else:
print(f" {title}")
print(f" ({columns} columns)")
if __name__ == "__main__":
main()
def main():
print("=== Multiple Default Arguments ===\n")
# Using all defaults
print("All defaults:")
make_coffee()
# Override some
print("\nCustom orders:")
make_coffee(size="large")
make_coffee(milk=True)
make_coffee(sugar=2)
make_coffee(size="small", sugar=3)
# Override all
print("\nFully customized:")
custom_size =
sugar_count =
make_coffee(custom_size, True, sugar_count) # positional
make_coffee(size=custom_size, milk=True, sugar=sugar_count) # keyword
# Configuration pattern
print("\n=== Configuration Pattern ===")
print_report()
print_report(title="Sales Report", columns=3)
print_report(show_header=True, title="Status")
def make_coffee(size="medium", milk=False, sugar=0):
"""Make coffee with customizable options."""
order = f" {size.capitalize()} coffee"
if milk:
order += " with milk"
if sugar > 0:
order += f", {sugar} sugar(s)"
print(order)
def print_report(title="Report", columns=2, show_header=False):
"""Print a configurable report."""
if show_header:
print(f" === {title} ===")
else:
print(f" {title}")
print(f" ({columns} columns)")
if __name__ == "__main__":
main()
def main():
print("=== Multiple Default Arguments ===\n")
# Using all defaults
print("All defaults:")
make_coffee()
# Override some
print("\nCustom orders:")
make_coffee(size="large")
make_coffee(milk=True)
make_coffee(sugar=2)
make_coffee(size="small", sugar=3)
# Override all
print("\nFully customized:")
custom_size =
sugar_count =
make_coffee(custom_size, True, sugar_count) # positional
make_coffee(size=custom_size, milk=True, sugar=sugar_count) # keyword
# Configuration pattern
print("\n=== Configuration Pattern ===")
print_report()
print_report(title="Sales Report", columns=3)
print_report(show_header=True, title="Status")
def make_coffee(size="medium", milk=False, sugar=0):
"""Make coffee with customizable options."""
order = f" {size.capitalize()} coffee"
if milk:
order += " with milk"
if sugar > 0:
order += f", {sugar} sugar(s)"
print(order)
def print_report(title="Report", columns=2, show_header=False):
"""Print a configurable report."""
if show_header:
print(f" === {title} ===")
else:
print(f" {title}")
print(f" ({columns} columns)")
if __name__ == "__main__":
main()
def main():
print("=== Multiple Default Arguments ===\n")
# Using all defaults
print("All defaults:")
make_coffee()
# Override some
print("\nCustom orders:")
make_coffee(size="large")
make_coffee(milk=True)
make_coffee(sugar=2)
make_coffee(size="small", sugar=3)
# Override all
print("\nFully customized:")
custom_size =
sugar_count =
make_coffee(custom_size, True, sugar_count) # positional
make_coffee(size=custom_size, milk=True, sugar=sugar_count) # keyword
# Configuration pattern
print("\n=== Configuration Pattern ===")
print_report()
print_report(title="Sales Report", columns=3)
print_report(show_header=True, title="Status")
def make_coffee(size="medium", milk=False, sugar=0):
"""Make coffee with customizable options."""
order = f" {size.capitalize()} coffee"
if milk:
order += " with milk"
if sugar > 0:
order += f", {sugar} sugar(s)"
print(order)
def print_report(title="Report", columns=2, show_header=False):
"""Print a configurable report."""
if show_header:
print(f" === {title} ===")
else:
print(f" {title}")
print(f" ({columns} columns)")
if __name__ == "__main__":
main()
def main():
print("=== Multiple Default Arguments ===\n")
# Using all defaults
print("All defaults:")
make_coffee()
# Override some
print("\nCustom orders:")
make_coffee(size="large")
make_coffee(milk=True)
make_coffee(sugar=2)
make_coffee(size="small", sugar=3)
# Override all
print("\nFully customized:")
custom_size =
sugar_count =
make_coffee(custom_size, True, sugar_count) # positional
make_coffee(size=custom_size, milk=True, sugar=sugar_count) # keyword
# Configuration pattern
print("\n=== Configuration Pattern ===")
print_report()
print_report(title="Sales Report", columns=3)
print_report(show_header=True, title="Status")
def make_coffee(size="medium", milk=False, sugar=0):
"""Make coffee with customizable options."""
order = f" {size.capitalize()} coffee"
if milk:
order += " with milk"
if sugar > 0:
order += f", {sugar} sugar(s)"
print(order)
def print_report(title="Report", columns=2, show_header=False):
"""Print a configurable report."""
if show_header:
print(f" === {title} ===")
else:
print(f" {title}")
print(f" ({columns} columns)")
if __name__ == "__main__":
main()
def main():
print("=== Multiple Default Arguments ===\n")
# Using all defaults
print("All defaults:")
make_coffee()
# Override some
print("\nCustom orders:")
make_coffee(size="large")
make_coffee(milk=True)
make_coffee(sugar=2)
make_coffee(size="small", sugar=3)
# Override all
print("\nFully customized:")
custom_size =
sugar_count =
make_coffee(custom_size, True, sugar_count) # positional
make_coffee(size=custom_size, milk=True, sugar=sugar_count) # keyword
# Configuration pattern
print("\n=== Configuration Pattern ===")
print_report()
print_report(title="Sales Report", columns=3)
print_report(show_header=True, title="Status")
def make_coffee(size="medium", milk=False, sugar=0):
"""Make coffee with customizable options."""
order = f" {size.capitalize()} coffee"
if milk:
order += " with milk"
if sugar > 0:
order += f", {sugar} sugar(s)"
print(order)
def print_report(title="Report", columns=2, show_header=False):
"""Print a configurable report."""
if show_header:
print(f" === {title} ===")
else:
print(f" {title}")
print(f" ({columns} columns)")
if __name__ == "__main__":
main()
All defaults must come after non-default parameters.
The mutable default trap
Never use mutable objects as defaults!
def main():
print("=== ⚠️ Mutable Default Trap ===\n")
# Watch carefully!
print("Calling add_item 3 times with defaults:")
result1 = add_item_broken("apple")
print(f"Call 1: {result1}")
result2 = add_item_broken("banana")
print(f"Call 2: {result2}") # Surprise!
result3 = add_item_broken("cherry")
print(f"Call 3: {result3}") # Even worse!
print("\n=== What Happened? ===")
print("Default list is created ONCE at function definition.")
print("Every call shares the SAME list object!")
print("Each append adds to that shared list.")
print("\n=== With Explicit Lists ===")
# These work because we provide our own lists
my_list = []
add_item_broken("dog", my_list)
print(f"My list: {my_list}")
other_list = []
add_item_broken("cat", other_list)
print(f"Other list: {other_list}")
print("\n=== Same Problem with Dict ===")
print(add_settings_broken("theme", "dark"))
print(add_settings_broken("volume", "100")) # Both settings!
# ❌ BROKEN: Mutable default
def add_item_broken(item, items=[]):
items.append(item)
return items
# ❌ BROKEN: Dict default
def add_settings_broken(key, value, settings={}):
settings[key] = value
return settings
if __name__ == "__main__":
main()
Defaults are evaluated once at definition time. Mutable defaults persist!
Use None to avoid the trap
The standard pattern to avoid mutable default issues.
def main():
print("=== None Pattern: Safe Defaults ===\n")
# Correct way with lists
print("Using None pattern:")
result1 = add_item("apple")
print(f"Call 1: {result1}")
result2 = add_item("banana")
print(f"Call 2: {result2}") # Fresh list!
result3 = add_item("cherry")
print(f"Call 3: {result3}") # Still fresh!
print("\n=== Providing Your Own List ===")
my_list = ["existing"]
result = add_item("new", my_list)
print(f"With my list: {result}")
print("\n=== Pattern Comparison ===")
print("❌ def func(items=[]): - Broken!")
print("✅ def func(items=None): - Safe!")
# Dictionary pattern
print("\n=== None Pattern with Dict ===")
config1 = build_config(debug=True)
print(f"Config 1: {config1}")
config2 = build_config(verbose=True)
print(f"Config 2: {config2}") # Independent!
# ✅ CORRECT: None default
def add_item(item, items=None):
if items is None:
items = [] # Fresh list each time!
items.append(item)
return items
# ✅ CORRECT: Dict with None
def build_config(base_config=None, **settings):
if base_config is None:
base_config = {} # Fresh dict each time!
base_config.update(settings)
return base_config
if __name__ == "__main__":
main()
Use None as default, then create the mutable object inside the function.
Exercise: sentinel.py
Explore sentinel objects for distinguishing None from 'not provided'