Python & AI Tutorials Logo
Python Programming

20. Function Parameters and Arguments

In Chapter 19, we learned how to define and call functions with basic parameters. Now we'll explore Python's flexible parameter and argument system in depth. Understanding these mechanisms allows you to write functions that are both powerful and easy to use.

20.1) Positional and Keyword Arguments

When you call a function, you can pass arguments in two fundamental ways: by position or by name (keyword).

20.1.1) Positional Arguments

Positional arguments are matched to parameters based on their order. The first argument goes to the first parameter, the second to the second parameter, and so on.

python
def calculate_discount(price, discount_percent):
    """Calculate the final price after applying a discount."""
    discount_amount = price * (discount_percent / 100)
    final_price = price - discount_amount
    return final_price
 
# Passing arguments by position
result = calculate_discount(100, 20)
print(result)

Output:

80.0

In this example, 100 is assigned to price and 20 to discount_percent based purely on their positions in the function call.

The order matters critically with positional arguments:

python
# Example: We want to calculate a $100 item with 20% discount
 
# Correct order: price first, then discount
print(calculate_discount(100, 20))
 
# Wrong order: discount first, then price
print(calculate_discount(20, 100))

Output:

80.0
-16.0

When you swap the arguments, Python doesn't know you made a mistake—it simply assigns them in order. This produces a mathematically valid but logically incorrect result (a negative price!).

20.1.2) Keyword Arguments

Keyword arguments explicitly specify which parameter receives which value by using the parameter name followed by an equals sign and the value. This makes your code more readable and protects against ordering mistakes.

python
def create_user_profile(username, email, age):
    """Create a user profile with the given information."""
    profile = f"User: {username}\nEmail: {email}\nAge: {age}"
    return profile
 
# Using keyword arguments
profile = create_user_profile(username="alice_smith", email="alice@example.com", age=28)
print(profile)

Output:

User: alice_smith
Email: alice@example.com
Age: 28

With keyword arguments, the order doesn't matter:

python
# Same result, different order
profile1 = create_user_profile(username="bob", email="bob@example.com", age=35)
profile2 = create_user_profile(age=35, username="bob", email="bob@example.com")
profile3 = create_user_profile(email="bob@example.com", age=35, username="bob")
 
# All three produce identical results
print(profile1 == profile2 == profile3)

Output:

True

This flexibility is particularly valuable when a function has many parameters, making it easy to see which value corresponds to which parameter.

20.1.3) Mixing Positional and Keyword Arguments

You can combine both styles in a single function call, but there's an important rule: positional arguments must come before keyword arguments.

python
def format_address(street, city, state, zip_code):
    """Format a mailing address."""
    return f"{street}\n{city}, {state} {zip_code}"
 
# Valid: positional arguments first, then keyword arguments
address = format_address("123 Main St", "Springfield", state="IL", zip_code="62701")
print(address)

Output:

123 Main St
Springfield, IL 62701

Here, "123 Main St" and "Springfield" are positional (assigned to street and city), while state and zip_code are specified by name.

Attempting to place positional arguments after keyword arguments causes an error:

python
# Invalid: positional argument after keyword argument
# address = format_address(street="123 Main St", "Springfield", state="IL", zip_code="62701")
# SyntaxError: positional argument follows keyword argument

Python enforces this rule because once you start using keyword arguments, it becomes ambiguous which positional parameter a subsequent unnamed argument should fill.

20.1.4) When to Use Each Style

Use positional arguments when:

  • The function has few parameters (typically 1-3)
  • The parameter order is obvious and intuitive
  • The function is commonly used and the order is well-known
python
# Obvious and concise
print(len("hello"))
result = max(10, 20, 5)

Use keyword arguments when:

  • The function has many parameters
  • Parameter meanings aren't immediately obvious
  • You want to skip some parameters that have defaults (covered next)
  • You want to make your code self-documenting
python
# Clear and explicit
user = create_user_profile(username="charlie", email="charlie@example.com", age=42)

20.2) Default Parameter Values

Functions can specify default values for parameters. When a caller doesn't provide an argument for a parameter with a default, Python uses the default value instead.

20.2.1) Defining Parameters with Defaults

Default values are specified in the function definition using the assignment operator:

python
def greet_user(name, greeting="Hello"):
    """Greet a user with a customizable greeting."""
    return f"{greeting}, {name}!"
 
# Using the default greeting
print(greet_user("Alice"))
 
# Providing a custom greeting
print(greet_user("Bob", "Good morning"))
print(greet_user("Carol", greeting="Hi"))

Output:

Hello, Alice!
Good morning, Bob!
Hi, Carol!

The parameter greeting has a default value of "Hello". When you call greet_user("Alice"), Python uses this default. When you provide a second argument, it overrides the default.

20.2.2) Parameters with Defaults Must Come After Required Parameters

Python requires that parameters with default values appear after all parameters without defaults. This rule prevents ambiguity about which arguments correspond to which parameters.

python
# Correct: required parameters first, then defaults
def create_product(name, price, category="General", in_stock=True):
    """Create a product record."""
    return {
        "name": name,
        "price": price,
        "category": category,
        "in_stock": in_stock
    }
 
product = create_product("Laptop", 999.99)
print(product)

Output:

{'name': 'Laptop', 'price': 999.99, 'category': 'General', 'in_stock': True}

Attempting to place a required parameter after one with a default causes a syntax error:

python
# Invalid: required parameter after default parameter
# def invalid_function(name="Unknown", age):
#     return f"{name} is {age} years old"
# SyntaxError: non-default argument follows default argument

This makes sense: if name has a default but age doesn't, how would Python know whether invalid_function(25) means name=25 with age missing, or age=25 with name using its default? The rule eliminates this ambiguity.

20.2.3) Practical Uses of Default Parameters

Default parameters are excellent for functions where certain arguments rarely change:

python
def calculate_shipping(weight, distance, express=False):
    """Calculate shipping cost based on weight and distance."""
    base_rate = 0.50 * weight + 0.10 * distance
    
    if express:
        base_rate *= 2  # Express shipping costs double
    
    return round(base_rate, 2)
 
# Most shipments are standard
standard_cost = calculate_shipping(5, 100)
print(f"Standard: ${standard_cost}")
 
# Occasionally someone needs express
express_cost = calculate_shipping(5, 100, express=True)
print(f"Express: ${express_cost}")

Output:

Standard: $12.5
Express: $25.0

This design makes the common case (standard shipping) simple to call, while still supporting the less common case (express shipping) when needed.

20.2.4) Multiple Defaults and Selective Overriding

When a function has multiple parameters with defaults, you can override any combination of them using keyword arguments:

python
def format_currency(amount, currency="USD", show_symbol=True, decimal_places=2):
    """Format a number as currency."""
    symbols = {"USD": "$", "EUR": "€", "GBP": "£", "JPY": "¥"}
    
    formatted = f"{amount:.{decimal_places}f}"
    
    if show_symbol and currency in symbols:
        formatted = f"{symbols[currency]}{formatted}"
    
    return formatted
 
# Using all defaults
print(format_currency(42.5))
 
# Override just the currency
print(format_currency(42.5, currency="EUR"))
 
# Override multiple defaults
print(format_currency(42.5, currency="JPY", decimal_places=0))

Output:

$42.50
€42.50
¥42

This flexibility allows callers to customize exactly what they need while keeping the function call concise.

20.3) Variable-Length Argument Lists with *args

Sometimes you want a function to accept any number of arguments without knowing in advance how many there will be. Python provides *args for this purpose.

20.3.1) Understanding *args

The *args syntax in a parameter list collects all extra positional arguments into a tuple. The name args is a convention (short for "arguments"), but you can use any valid parameter name after the asterisk.

python
def calculate_total(*numbers):
    """Calculate the sum of any number of values."""
    total = 0
    for num in numbers:
        total += num
    return total
 
# Works with any number of arguments
print(calculate_total(10))
print(calculate_total(10, 20))
print(calculate_total(10, 20, 30, 40))
print(calculate_total())

Output:

10
30
100
0

Inside the function, numbers is a tuple containing all the positional arguments passed to the function. When no arguments are provided, it's an empty tuple.

20.3.2) Combining Regular Parameters with *args

You can have regular parameters before *args. The regular parameters consume the first few arguments, and *args collects the rest:

python
def create_team(team_name, *members):
    """Create a team with a name and any number of members."""
    member_list = ", ".join(members)
    return f"Team {team_name}: {member_list}"
 
# First argument goes to team_name, rest go to members
print(create_team("Alpha", "Alice", "Bob"))
print(create_team("Beta", "Carol"))
print(create_team("Gamma", "Dave", "Eve", "Frank", "Grace"))

Output:

Team Alpha: Alice, Bob
Team Beta: Carol
Team Gamma: Dave, Eve, Frank, Grace

The first argument ("Alpha", "Beta", or "Gamma") is assigned to team_name, and all remaining arguments are collected into the members tuple.

20.4) Keyword-Only and **kwargs Parameters

Python provides two additional mechanisms for handling arguments: keyword-only parameters and **kwargs for collecting arbitrary keyword arguments.

20.4.1) Keyword-Only Parameters

Keyword-only parameters must be specified using keyword arguments—they cannot be passed positionally. You create them by placing them after a * or after *args in the parameter list.

python
def create_account(username, *, email, age):
    """Create an account. Email and age must be specified by name."""
    return {
        "username": username,
        "email": email,
        "age": age
    }
 
# Correct: email and age specified by keyword
account = create_account("alice", email="alice@example.com", age=28)
print(account)
 
# Invalid: trying to pass email and age positionally
# account = create_account("bob", "bob@example.com", 30)
# TypeError: create_account() takes 1 positional argument but 3 were given

Output:

{'username': 'alice', 'email': 'alice@example.com', 'age': 28}

The * in the parameter list acts as a separator. Everything after it must be passed as a keyword argument. This is useful when you want to force callers to be explicit about certain parameters, making the code more readable and less error-prone.

You can also combine regular parameters, *args, and keyword-only parameters:

python
def log_event(event_type, *details, severity="INFO", timestamp=None):
    """Log an event with optional details and metadata."""
    # We'll learn about the datetime module in detail in Chapter 39,
    # but for now, just know that these lines get the current time
    # and format it as a timestamp string
    from datetime import datetime
    
    if timestamp is None:
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    details_str = " | ".join(details)
    return f"[{timestamp}] {severity}: {event_type} - {details_str}"
 
# event_type is positional, details are collected by *details,
# severity and timestamp are keyword-only
print(log_event("Login", "User: alice", "IP: 192.168.1.1"))
print(log_event("Error", "Database connection failed", severity="ERROR"))

Output (timestamp will vary based on when you run the code):

[2025-12-18 19:29:16] INFO: Login - User: alice | IP: 192.168.1.1
[2025-12-18 19:29:16] ERROR: Error - Database connection failed

20.4.2) Understanding **kwargs

The **kwargs syntax collects all extra keyword arguments into a dictionary. Like args, the name kwargs is conventional (short for "keyword arguments"), but you can use any valid name after the double asterisk.

python
def create_product(**attributes):
    """Create a product with any number of attributes."""
    product = {}
    for key, value in attributes.items():
        product[key] = value
    return product
 
# Pass any keyword arguments you want
laptop = create_product(name="Laptop", price=999.99, brand="TechCorp", in_stock=True)
print(laptop)
 
phone = create_product(name="Phone", price=699.99, color="Black")
print(phone)

Output:

{'name': 'Laptop', 'price': 999.99, 'brand': 'TechCorp', 'in_stock': True}
{'name': 'Phone', 'price': 699.99, 'color': 'Black'}

Inside the function, attributes is a dictionary where keys are the parameter names and values are the arguments passed.

20.4.3) Combining Regular Parameters, *args, and **kwargs

You can use all these mechanisms together, but they must appear in a specific order:

  1. Regular positional parameters
  2. *args (if present)
  3. Keyword-only parameters (if present)
  4. **kwargs (if present)
python
def complex_function(required, *args, keyword_only, **kwargs):
    """Demonstrate all parameter types together."""
    print(f"Required: {required}")
    print(f"Args: {args}")
    print(f"Keyword-only: {keyword_only}")
    print(f"Kwargs: {kwargs}")
 
complex_function(
    "value1",           # required
    "value2", "value3", # args
    keyword_only="kw",  # keyword_only
    extra1="e1",        # kwargs
    extra2="e2"         # kwargs
)

Output:

Required: value1
Args: ('value2', 'value3')
Keyword-only: kw
Kwargs: {'extra1': 'e1', 'extra2': 'e2'}

This flexibility is powerful but should be used judiciously. Most functions don't need all these mechanisms.

20.4.4) Practical Use Case: Configuration Functions

A common use for **kwargs is creating functions that accept configuration options:

python
def connect_to_database(host, port, **options):
    """Connect to a database with flexible configuration options."""
    connection_string = f"Connecting to {host}:{port}"
    
    # Process any additional options
    if options.get("ssl"):
        connection_string += " with SSL"
    
    if options.get("timeout"):
        connection_string += f" (timeout: {options['timeout']}s)"
    
    if options.get("pool_size"):
        connection_string += f" (pool size: {options['pool_size']})"
    
    return connection_string
 
# Basic connection
print(connect_to_database("localhost", 5432))
 
# With SSL
print(connect_to_database("db.example.com", 5432, ssl=True))
 
# With multiple options
print(connect_to_database("db.example.com", 5432, ssl=True, timeout=30, pool_size=10))

Output:

Connecting to localhost:5432
Connecting to db.example.com:5432 with SSL
Connecting to db.example.com:5432 with SSL (timeout: 30s) (pool size: 10)

This pattern allows the function to accept any number of optional configuration parameters without defining them all explicitly in the parameter list.

Positional

Extra Positional

Keyword-Only

Extra Keyword

Function Call

Parameter Type?

Regular Parameters

*args tuple

Keyword-Only Parameters

**kwargs dict

Assigned by Position

Collected into Tuple

Must Use Name

Collected into Dict

20.5) Argument Unpacking When Calling Functions

Just as *args and **kwargs collect arguments when defining functions, you can use * and ** to unpack collections when calling functions.

20.5.1) Unpacking Sequences with *

The * operator unpacks a sequence (list, tuple, etc.) into separate positional arguments:

python
def calculate_rectangle_area(width, height):
    """Calculate the area of a rectangle."""
    return width * height
 
# Instead of passing arguments individually
dimensions = [5, 10]
area = calculate_rectangle_area(dimensions[0], dimensions[1])
print(area)
 
# Unpack the list directly
area = calculate_rectangle_area(*dimensions)
print(area)

Output:

50
50

When you write *dimensions, Python unpacks the list [5, 10] into two separate arguments, as if you had written calculate_rectangle_area(5, 10).

This works with any iterable:

python
def format_name(first, middle, last):
    """Format a full name."""
    return f"{first} {middle} {last}"
 
# Unpacking a tuple
name_tuple = ("John", "Q", "Public")
print(format_name(*name_tuple))
 
# Unpacking a list
name_list = ["Jane", "M", "Doe"]
print(format_name(*name_list))
 
# Even unpacking a string (each character becomes an argument)
# This only works if the function expects the right number of arguments
def show_first_three(a, b, c):
    return f"{a}, {b}, {c}"
 
print(show_first_three(*"ABC"))

Output:

John Q Public
Jane M Doe
A, B, C

20.5.2) Unpacking Dictionaries with **

The ** operator unpacks a dictionary into keyword arguments:

python
def create_user(username, email, age):
    """Create a user profile."""
    return f"User: {username}, Email: {email}, Age: {age}"
 
# Dictionary with keys matching parameter names
user_data = {
    "username": "alice",
    "email": "alice@example.com",
    "age": 28
}
 
# Unpack the dictionary
profile = create_user(**user_data)
print(profile)

Output:

User: alice, Email: alice@example.com, Age: 28

When you write **user_data, Python unpacks the dictionary into keyword arguments, equivalent to:

python
create_user(username="alice", email="alice@example.com", age=28)

The dictionary keys must match the function's parameter names, or you'll get an error:

python
# Invalid: dictionary key doesn't match parameter name
invalid_data = {"name": "bob", "email": "bob@example.com", "age": 30}
# profile = create_user(**invalid_data)
# TypeError: create_user() got an unexpected keyword argument 'name'

20.5.3) Combining Unpacking with Regular Arguments

You can mix unpacked arguments with regular arguments:

python
def calculate_total(base_price, tax_rate, discount):
    """Calculate total price after tax and discount."""
    subtotal = base_price * (1 + tax_rate)
    total = subtotal * (1 - discount)
    return round(total, 2)
 
# Some arguments regular, some unpacked
pricing = [0.08, 0.10]  # tax_rate and discount
total = calculate_total(100, *pricing)
print(total)

Output:

97.2

You can also unpack multiple collections in a single call:

python
def create_full_address(street, city, state, zip_code, country):
    """Create a complete address."""
    return f"{street}, {city}, {state} {zip_code}, {country}"
 
street_address = ["123 Main St", "Springfield"]
location_details = ["IL", "62701", "USA"]
 
address = create_full_address(*street_address, *location_details)
print(address)

Output:

123 Main St, Springfield, IL 62701, USA

20.5.4) Practical Example: Flexible Function Calls

Unpacking is particularly useful when working with data from external sources:

python
def send_email(recipient, subject, body, cc=None, bcc=None):
    """Send an email with optional CC and BCC."""
    message = f"To: {recipient}\nSubject: {subject}\n\n{body}"
    
    if cc:
        message += f"\nCC: {cc}"
    if bcc:
        message += f"\nBCC: {bcc}"
    
    return message
 
# Email data from a configuration file or database
email_config = {
    "recipient": "user@example.com",
    "subject": "Welcome",
    "body": "Thank you for signing up!",
    "cc": "manager@example.com"
}
 
# Unpack the configuration directly
result = send_email(**email_config)
print(result)

Output:

To: user@example.com
Subject: Welcome
 
Thank you for signing up!
CC: manager@example.com

This pattern makes it easy to pass around function arguments as data structures, which is common when building APIs or processing configuration files.

* operator

** operator

Collection

Unpacking Type

Sequence Unpacking

Dictionary Unpacking

List/Tuple → Positional Args

Dict → Keyword Args

Function Call

20.6) The Trap of Mutable Default Arguments (Why List Defaults Persist)

One of Python's most notorious pitfalls involves using mutable objects (like lists or dictionaries) as default parameter values. Understanding this issue is crucial for writing correct functions.

20.6.1) The Problem: Shared Mutable Defaults

Consider this seemingly innocent function:

python
def add_student(name, grades=[]):
    """Add a student with their grades."""
    grades.append(name)
    return grades
 
# First call
students1 = add_student("Alice")
print(students1)
 
# Second call - expecting a fresh list
students2 = add_student("Bob")
print(students2)
 
# Third call
students3 = add_student("Carol")
print(students3)

Output:

['Alice']
['Alice', 'Bob']
['Alice', 'Bob', 'Carol']

This behavior surprises many programmers. Each call to add_student() without providing a grades argument uses the same list object, not a new one. The list persists across function calls, accumulating values.

20.6.2) Why This Happens: Default Values Are Created Once

The key to understanding this behavior is knowing when default values are created. Python evaluates default parameter values once, when the function is defined, not each time the function is called.

python
def demonstrate_default_creation():
    """Show when defaults are created."""
    print("Function defined!")
 
def use_default(value=demonstrate_default_creation()):
    """Use a default that calls a function."""
    return value
 
# The message prints when the function is DEFINED, not called

Output:

Function defined!

When Python encounters the def use_default line, it evaluates the default parameter value=demonstrate_default_creation(). This calls demonstrate_default_creation(), which prints "Function defined!" immediately. Later calls to use_default() don't evaluate the default again, so nothing additional prints.

When Python encounters def add_student(name, grades=[]):, it creates an empty list object and stores it as the default value for grades. Every subsequent call that doesn't provide a grades argument uses that same list object.

Here's a clearer demonstration using object identity:

python
def show_list_identity(items=[]):
    """Show that the same list object is reused."""
    print(f"List ID: {id(items)}")
    items.append("item")
    return items
 
# Each call uses the same list object (same ID)
show_list_identity()
show_list_identity()
show_list_identity()

Output:

List ID: 140234567890123
List ID: 140234567890123
List ID: 140234567890123

The exact ID numbers will vary on your system, but notice that all three calls show the same ID value, proving they're using the same list object. The id() function returns a unique identifier for each object in memory—when the IDs match, it's the same object.

20.6.3) The Correct Pattern: Use None as Default

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

python
def add_student_correct(name, grades=None):
    """Add a student with their grades (correct version)."""
    if grades is None:
        grades = []  # Create a NEW list each time
    
    grades.append(name)
    return grades
 
# Now each call gets its own list
students1 = add_student_correct("Alice")
print(students1)
 
students2 = add_student_correct("Bob")
print(students2)
 
students3 = add_student_correct("Carol")
print(students3)

Output:

['Alice']
['Bob']
['Carol']

This pattern works because None is immutable and a new list is created inside the function body each time grades is None.

20.6.4) The Same Issue with Dictionaries

This problem affects all mutable types, not just lists:

python
# WRONG: Dictionary default
def create_config_wrong(key, value, config={}):
    """Create a configuration (BUGGY VERSION)."""
    config[key] = value
    return config
 
config1 = create_config_wrong("theme", "dark")
print(config1)
 
config2 = create_config_wrong("language", "en")
print(config2)
 
print("---")
 
# CORRECT: None as default
def create_config_correct(key, value, config=None):
    """Create a configuration (CORRECT VERSION)."""
    if config is None:
        config = {}
    
    config[key] = value
    return config
 
config1 = create_config_correct("theme", "dark")
print(config1)
 
config2 = create_config_correct("language", "en")
print(config2)

Output:

{'theme': 'dark'}
{'theme': 'dark', 'language': 'en'}
---
{'theme': 'dark'}
{'language': 'en'}

20.6.5) Summary: The Golden Rule

Never use mutable objects (lists, dictionaries, sets) as default parameter values. Always use None and create the mutable object inside the function:

python
# ❌ WRONG
def function(items=[]):
    pass
 
# ✅ CORRECT
def function(items=None):
    if items is None:
        items = []
    # Now use items safely

This pattern ensures each function call gets its own independent mutable object, preventing mysterious bugs where data leaks between calls.


In this chapter, we've explored Python's flexible parameter and argument system in depth. You've learned how to use positional and keyword arguments, provide default values, handle variable numbers of arguments with *args and **kwargs, unpack collections when calling functions, and avoid the mutable default argument trap.

These mechanisms give you powerful tools for designing function interfaces that are both flexible and easy to use. As you write more functions, you'll develop intuition for which parameter patterns best suit different situations. The key is to balance flexibility with clarity—make your functions easy to call correctly and hard to call incorrectly.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai