Python Function Arguments Cheatsheet

Python Function Arguments Cheatsheet

Functions are the building blocks of any Python program, and understanding how to work with their arguments is essential. Whether you're a beginner or an experienced developer, this cheatsheet will help you master Python's function arguments once and for all.

Positional and Keyword Arguments

The most basic way to pass arguments to a function is through positional arguments. These are matched based on their position in the function call.

def greet(name, greeting):
    print(f"{greeting}, {name}!")

greet("Alice", "Hello")  # Output: Hello, Alice!

You can also use keyword arguments, where you explicitly name each argument. This makes your code more readable and allows you to change the order of arguments.

greet(greeting="Hi", name="Bob")  # Output: Hi, Bob!

Mixing positional and keyword arguments is possible, but positional arguments must come first.

greet("Charlie", greeting="Hey")  # Output: Hey, Charlie!
Argument Type Example Description
Positional func(1, 2) Arguments passed in order
Keyword func(a=1, b=2) Arguments passed by name
Mixed func(1, b=2) Positional first, then keyword

Default Arguments

Default arguments allow you to specify values that will be used if the caller doesn't provide them. This is incredibly useful for making functions more flexible.

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

print(power(3))      # Output: 9
print(power(3, 3))   # Output: 27

Important caveat: Default argument values are evaluated only once when the function is defined, not each time the function is called. This can lead to unexpected behavior with mutable objects.

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item(1))  # Output: [1]
print(add_item(2))  # Output: [1, 2] - Probably not what you wanted!

To avoid this, use None as the default and create a new object inside the function:

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

args and *kwargs

These special syntaxes allow you to handle variable numbers of arguments in your functions.

*args collects extra positional arguments as a tuple:

def sum_numbers(*args):
    return sum(args)

print(sum_numbers(1, 2, 3, 4))  # Output: 10

**kwargs collects extra keyword arguments as a dictionary:

def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=30, city="Boston")

You can combine all argument types in a single function definition:

def complex_function(a, b=2, *args, **kwargs):
    print(f"a: {a}, b: {b}")
    print(f"args: {args}")
    print(f"kwargs: {kwargs}")

complex_function(1, 3, 4, 5, name="Alice", age=25)

Common use cases for args and *kwargs: - Writing wrapper functions - Creating decorators - Implementing function overloading - Building flexible APIs

Argument Unpacking

You can also use * and ** when calling functions to unpack sequences and dictionaries into arguments.

def point(x, y, z):
    return f"Point at ({x}, {y}, {z})"

coordinates = [1, 2, 3]
print(point(*coordinates))  # Output: Point at (1, 2, 3)

params = {'x': 4, 'y': 5, 'z': 6}
print(point(**params))      # Output: Point at (4, 5, 6)

This is particularly useful when you have data in collections that you want to pass to functions that expect individual arguments.

Type Hints and Annotations

Python supports type hints, which make your code more readable and help catch errors early. While they don't enforce types at runtime, tools like mypy can use them for static type checking.

from typing import List, Optional

def process_items(items: List[str], max_items: Optional[int] = None) -> int:
    if max_items is not None:
        items = items[:max_items]
    return len(items)

Type hints improve code documentation and make it easier for other developers (and your future self) to understand what types of arguments a function expects.

Type Hint Example Description
Basic def func(x: int) -> str: Simple type annotation
Optional def func(x: Optional[int]) Argument can be None
List def func(items: List[str]) List of strings
Multiple def func(x: Union[int, float]) Multiple allowed types

Common Patterns and Best Practices

When designing functions, consider these best practices for argument handling:

Choose meaningful argument names that clearly indicate what the argument represents. Avoid single-letter names unless they're very obvious (like x and y for coordinates).

Keep functions focused on a single purpose. If you find yourself needing many arguments, consider whether your function might be doing too much.

Use keyword arguments for optional parameters, especially when there are several of them. This makes function calls more readable.

Consider using dataclasses or custom classes when you need to pass many related parameters:

from dataclasses import dataclass

@dataclass
class UserConfig:
    username: str
    email: str
    is_admin: bool = False
    is_active: bool = True

def create_user(config: UserConfig):
    # Process user creation
    pass

This approach groups related data together and makes your function signatures cleaner.

Error Handling with Arguments

Proper validation of function arguments can prevent many bugs. You can use assertions or custom checks:

def divide(numerator, denominator):
    if denominator == 0:
        raise ValueError("Denominator cannot be zero")
    return numerator / denominator

For more complex validation, consider using libraries like Pydantic or writing custom validation functions.

Remember that clear error messages are just as important as the validation itself. They help other developers understand what went wrong and how to fix it.

Practical Examples

Let's look at some real-world examples that combine multiple argument techniques:

def send_email(to, subject, body, *, cc=None, bcc=None, attachments=None):
    """Send an email with various options"""
    # Implementation would go here
    print(f"Sending email to {to} with subject '{subject}'")

# Usage
send_email("user@example.com", "Hello", "This is the body", 
           cc=["manager@example.com"], attachments=["file.pdf"])

Another common pattern is creating configuration functions:

def configure_logging(level="INFO", format=None, file=None, **extra_options):
    """Configure logging with flexible options"""
    config = {
        "level": level,
        "format": format or "%(asctime)s - %(levelname)s - %(message)s",
        "file": file,
        **extra_options
    }
    # Apply logging configuration
    return config

These examples show how different argument types can work together to create flexible, readable functions.

Advanced Techniques

For more complex scenarios, you might encounter some advanced argument patterns:

Function overloading using argument types:

from typing import overload

@overload
def process(data: str) -> str: ...

@overload
def process(data: list) -> list: ...

def process(data):
    if isinstance(data, str):
        return data.upper()
    elif isinstance(data, list):
        return [item.upper() for item in data]

Using inspect module to examine function arguments programmatically:

import inspect

def example(a, b=2, *args, **kwargs):
    pass

sig = inspect.signature(example)
print(sig.parameters)

These advanced techniques are powerful but should be used judiciously, as they can make code more complex.

Summary Table

Here's a quick reference table for all the argument types we've covered:

Argument Type Syntax Purpose
Positional def func(a, b) Required arguments in order
Keyword def func(a=1, b=2) Optional with defaults
Variable positional def func(*args) Capture extra positional args
Variable keyword def func(**kwargs) Capture extra keyword args
Keyword-only def func(a, *, b) b must be passed as keyword
Positional-only def func(a, b, /) a and b must be positional

Remember that mastering function arguments will make you a more effective Python programmer. Practice using these different techniques, and soon they'll become second nature in your coding workflow.