You're writing a logging function that should accept any number of messages. Or a config function that takes arbitrary key-value options. *args and **kwargs let you accept variable arguments without knowing them in advance.

Accept any positional arguments

Use *args to collect extra positional arguments.

args.py
def main():
    print("=== *args: Variable Positional Arguments ===\n")
    
    # Any number of arguments
    print("sum_all with different counts:")
    print(f"sum_all(1, 2) = {sum_all(1, 2)}")
    print(f"sum_all(1, 2, 3) = {sum_all(1, 2, 3)}")
    print(f"sum_all(1, 2, 3, 4, 5) = {sum_all(1, 2, 3, 4, 5)}")
    print(f"sum_all() = {sum_all()}")  # Zero args OK!
    
    print("\n=== What is *args? ===")
    show_args(10, 20, 30)
    
    print("\n=== Required + *args ===")
    greet("Hello", "Alice", "Bob", "Charlie")
    greet("Hi", "Dave")
    
    print("\n=== Type Hints with *args ===")
    result = concat_strings("Hello", " ", "World", "!")
    print(f"concat_strings result: {result}")

def sum_all(*numbers):
    """Sum any number of values."""
    total = 0
    for n in numbers:
        total += n
    return total

def show_args(*args):
    """Show what args contains."""
    print(f"args = {args}")
    print(f"type(args) = {type(args).__name__}")
    print(f"len(args) = {len(args)}")

def greet(greeting, *names):
    """Greet multiple people."""
    for name in names:
        print(f"{greeting}, {name}!")

def concat_strings(*strings: str) -> str:
    """Concatenate any number of strings (typed)."""
    return "".join(strings)

if __name__ == "__main__":
    main()

*args collects extra positional arguments into a tuple.

*args Collects variable positional arguments: `def f(*args):` - args is a tuple.

Accept any keyword arguments

Use **kwargs to collect extra keyword arguments.

kwargs.py
def main():
    print("=== **kwargs: Variable Keyword Arguments ===\n")
    
    # Any keyword arguments
    print("User profiles:")
    print_info(name="Alice", age=30, city="NYC")
    print()
    print_info(name="Bob", occupation="Developer")
    print()
    print_info(username="charlie", email="c@test.com", premium=True)
    
    print("\n=== What is **kwargs? ===")
    show_kwargs(x=10, y=20, z=30)
    
    print("\n=== Building Objects ===")
    config = make_config(debug=True, timeout=30, retries=3)
    print(f"Config: {config}")
    
    print("\n=== With Validation ===")
    try:
        validated = create_user(name="Alice", email="alice@test.com")
        print(f"Created: {validated}")
    except ValueError as e:
        print(f"Error: {e}")

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

def show_kwargs(**kwargs):
    """Show what kwargs contains."""
    print(f"kwargs = {kwargs}")
    print(f"type(kwargs) = {type(kwargs).__name__}")
    print(f"keys = {list(kwargs.keys())}")

def make_config(**settings):
    """Create config dict from keyword args."""
    # Add defaults
    config = {
        'debug': False,
        'timeout': 60,
        'retries': 5
    }
    # Override with provided settings
    config.update(settings)
    return config

def create_user(**fields):
    """Create user with validation."""
    required = ['name', 'email']
    for field in required:
        if field not in fields:
            raise ValueError(f"Missing required field: {field}")
    return fields

if __name__ == "__main__":
    main()

**kwargs collects extra keyword arguments into a dictionary.

**kwargs Collects variable keyword arguments: `def f(**kwargs):` - kwargs is a dict.

Combine *args and **kwargs

Accept both kinds of variable arguments.

combined.py
def main():
    print("=== Combining *args and **kwargs ===\n")
    
    # Both together
    log("INFO", "User logged in", user="Alice", ip="192.168.1.1")
    log("ERROR", "Connection failed", "timeout", retries=3)
    log("DEBUG", "Processing")  # Just message
    
    print("\n=== Full Signature Order ===")
    demonstrate_order("required", "extra1", "extra2", 
                     kw_only="value", opt1="a", opt2="b")
    
    print("\n=== Catch-All Function ===")
    catch_all(1, 2, 3, name="test", flag=True)

def log(level, message, *tags, **metadata):
    """Log with optional tags and metadata."""
    print(f"[{level}] {message}")
    if tags:
        print(f"  Tags: {', '.join(tags)}")
    if metadata:
        for key, value in metadata.items():
            print(f"  {key}: {value}")
    print()

def demonstrate_order(required, *args, kw_only, **kwargs):
    """Show the required parameter order."""
    print(f"required: {required}")
    print(f"*args: {args}")
    print(f"kw_only: {kw_only}")
    print(f"**kwargs: {kwargs}")

def catch_all(*args, **kwargs):
    """Accept literally anything."""
    print(f"Positional ({len(args)}): {args}")
    print(f"Keyword ({len(kwargs)}): {kwargs}")

if __name__ == "__main__":
    main()

Order matters: regular params, *args, keyword-only params, **kwargs.

Unpack into function calls

Use * and ** to expand sequences and dicts into arguments.

unpacking.py
def main():
    print("=== Unpacking: * and ** in Function Calls ===\n")
    
    # Unpack list/tuple into positional args
    numbers = 
    print(f"numbers = {numbers}")
    print(f"add(numbers) would fail - it's one arg")
    print(f"add(*numbers) = {add(*numbers)}")  # Unpacks!
    
    print("\n=== Unpack Dict into Keyword Args ===")
    config = {'host': 'localhost', 'port': 8080, 'debug': True}
    print(f"config = {config}")
    connect(**config)  # Same as connect(host='...', port=..., debug=...)
    
    print("\n=== Combined Unpacking ===")
    args = [5, 3]
    kwargs = {'operation': 'multiply'}
    calculate(*args, **kwargs)
    
    print("\n=== Range Example ===")
    # start, stop, step
    bounds = 
    print(f"bounds = {bounds}")
    print(f"list(range(*bounds)) = {list(range(*bounds))}")
    
    print("\n=== Merge Dicts with ** ===")
    defaults = {'color': 'blue', 'size': 'medium'}
    overrides = {'size': 'large', 'style': 'bold'}
    merged = {**defaults, **overrides}
    print(f"defaults: {defaults}")
    print(f"overrides: {overrides}")
    print(f"merged: {merged}")

def add(a, b, c):
    """Add three numbers."""
    return a + b + c

def connect(host, port, debug=False):
    """Simulate connection."""
    print(f"Connecting to {host}:{port} (debug={debug})")

def calculate(x, y, operation='add'):
    """Perform calculation."""
    if operation == 'add':
        print(f"{x} + {y} = {x + y}")
    elif operation == 'multiply':
        print(f"{x} * {y} = {x * y}")

if __name__ == "__main__":
    main()
def main():
    print("=== Unpacking: * and ** in Function Calls ===\n")
    
    # Unpack list/tuple into positional args
    numbers = 
    print(f"numbers = {numbers}")
    print(f"add(numbers) would fail - it's one arg")
    print(f"add(*numbers) = {add(*numbers)}")  # Unpacks!
    
    print("\n=== Unpack Dict into Keyword Args ===")
    config = {'host': 'localhost', 'port': 8080, 'debug': True}
    print(f"config = {config}")
    connect(**config)  # Same as connect(host='...', port=..., debug=...)
    
    print("\n=== Combined Unpacking ===")
    args = [5, 3]
    kwargs = {'operation': 'multiply'}
    calculate(*args, **kwargs)
    
    print("\n=== Range Example ===")
    # start, stop, step
    bounds = 
    print(f"bounds = {bounds}")
    print(f"list(range(*bounds)) = {list(range(*bounds))}")
    
    print("\n=== Merge Dicts with ** ===")
    defaults = {'color': 'blue', 'size': 'medium'}
    overrides = {'size': 'large', 'style': 'bold'}
    merged = {**defaults, **overrides}
    print(f"defaults: {defaults}")
    print(f"overrides: {overrides}")
    print(f"merged: {merged}")

def add(a, b, c):
    """Add three numbers."""
    return a + b + c

def connect(host, port, debug=False):
    """Simulate connection."""
    print(f"Connecting to {host}:{port} (debug={debug})")

def calculate(x, y, operation='add'):
    """Perform calculation."""
    if operation == 'add':
        print(f"{x} + {y} = {x + y}")
    elif operation == 'multiply':
        print(f"{x} * {y} = {x * y}")

if __name__ == "__main__":
    main()
def main():
    print("=== Unpacking: * and ** in Function Calls ===\n")
    
    # Unpack list/tuple into positional args
    numbers = 
    print(f"numbers = {numbers}")
    print(f"add(numbers) would fail - it's one arg")
    print(f"add(*numbers) = {add(*numbers)}")  # Unpacks!
    
    print("\n=== Unpack Dict into Keyword Args ===")
    config = {'host': 'localhost', 'port': 8080, 'debug': True}
    print(f"config = {config}")
    connect(**config)  # Same as connect(host='...', port=..., debug=...)
    
    print("\n=== Combined Unpacking ===")
    args = [5, 3]
    kwargs = {'operation': 'multiply'}
    calculate(*args, **kwargs)
    
    print("\n=== Range Example ===")
    # start, stop, step
    bounds = 
    print(f"bounds = {bounds}")
    print(f"list(range(*bounds)) = {list(range(*bounds))}")
    
    print("\n=== Merge Dicts with ** ===")
    defaults = {'color': 'blue', 'size': 'medium'}
    overrides = {'size': 'large', 'style': 'bold'}
    merged = {**defaults, **overrides}
    print(f"defaults: {defaults}")
    print(f"overrides: {overrides}")
    print(f"merged: {merged}")

def add(a, b, c):
    """Add three numbers."""
    return a + b + c

def connect(host, port, debug=False):
    """Simulate connection."""
    print(f"Connecting to {host}:{port} (debug={debug})")

def calculate(x, y, operation='add'):
    """Perform calculation."""
    if operation == 'add':
        print(f"{x} + {y} = {x + y}")
    elif operation == 'multiply':
        print(f"{x} * {y} = {x * y}")

if __name__ == "__main__":
    main()
def main():
    print("=== Unpacking: * and ** in Function Calls ===\n")
    
    # Unpack list/tuple into positional args
    numbers = 
    print(f"numbers = {numbers}")
    print(f"add(numbers) would fail - it's one arg")
    print(f"add(*numbers) = {add(*numbers)}")  # Unpacks!
    
    print("\n=== Unpack Dict into Keyword Args ===")
    config = {'host': 'localhost', 'port': 8080, 'debug': True}
    print(f"config = {config}")
    connect(**config)  # Same as connect(host='...', port=..., debug=...)
    
    print("\n=== Combined Unpacking ===")
    args = [5, 3]
    kwargs = {'operation': 'multiply'}
    calculate(*args, **kwargs)
    
    print("\n=== Range Example ===")
    # start, stop, step
    bounds = 
    print(f"bounds = {bounds}")
    print(f"list(range(*bounds)) = {list(range(*bounds))}")
    
    print("\n=== Merge Dicts with ** ===")
    defaults = {'color': 'blue', 'size': 'medium'}
    overrides = {'size': 'large', 'style': 'bold'}
    merged = {**defaults, **overrides}
    print(f"defaults: {defaults}")
    print(f"overrides: {overrides}")
    print(f"merged: {merged}")

def add(a, b, c):
    """Add three numbers."""
    return a + b + c

def connect(host, port, debug=False):
    """Simulate connection."""
    print(f"Connecting to {host}:{port} (debug={debug})")

def calculate(x, y, operation='add'):
    """Perform calculation."""
    if operation == 'add':
        print(f"{x} + {y} = {x + y}")
    elif operation == 'multiply':
        print(f"{x} * {y} = {x * y}")

if __name__ == "__main__":
    main()
def main():
    print("=== Unpacking: * and ** in Function Calls ===\n")
    
    # Unpack list/tuple into positional args
    numbers = 
    print(f"numbers = {numbers}")
    print(f"add(numbers) would fail - it's one arg")
    print(f"add(*numbers) = {add(*numbers)}")  # Unpacks!
    
    print("\n=== Unpack Dict into Keyword Args ===")
    config = {'host': 'localhost', 'port': 8080, 'debug': True}
    print(f"config = {config}")
    connect(**config)  # Same as connect(host='...', port=..., debug=...)
    
    print("\n=== Combined Unpacking ===")
    args = [5, 3]
    kwargs = {'operation': 'multiply'}
    calculate(*args, **kwargs)
    
    print("\n=== Range Example ===")
    # start, stop, step
    bounds = 
    print(f"bounds = {bounds}")
    print(f"list(range(*bounds)) = {list(range(*bounds))}")
    
    print("\n=== Merge Dicts with ** ===")
    defaults = {'color': 'blue', 'size': 'medium'}
    overrides = {'size': 'large', 'style': 'bold'}
    merged = {**defaults, **overrides}
    print(f"defaults: {defaults}")
    print(f"overrides: {overrides}")
    print(f"merged: {merged}")

def add(a, b, c):
    """Add three numbers."""
    return a + b + c

def connect(host, port, debug=False):
    """Simulate connection."""
    print(f"Connecting to {host}:{port} (debug={debug})")

def calculate(x, y, operation='add'):
    """Perform calculation."""
    if operation == 'add':
        print(f"{x} + {y} = {x + y}")
    elif operation == 'multiply':
        print(f"{x} * {y} = {x * y}")

if __name__ == "__main__":
    main()
def main():
    print("=== Unpacking: * and ** in Function Calls ===\n")
    
    # Unpack list/tuple into positional args
    numbers = 
    print(f"numbers = {numbers}")
    print(f"add(numbers) would fail - it's one arg")
    print(f"add(*numbers) = {add(*numbers)}")  # Unpacks!
    
    print("\n=== Unpack Dict into Keyword Args ===")
    config = {'host': 'localhost', 'port': 8080, 'debug': True}
    print(f"config = {config}")
    connect(**config)  # Same as connect(host='...', port=..., debug=...)
    
    print("\n=== Combined Unpacking ===")
    args = [5, 3]
    kwargs = {'operation': 'multiply'}
    calculate(*args, **kwargs)
    
    print("\n=== Range Example ===")
    # start, stop, step
    bounds = 
    print(f"bounds = {bounds}")
    print(f"list(range(*bounds)) = {list(range(*bounds))}")
    
    print("\n=== Merge Dicts with ** ===")
    defaults = {'color': 'blue', 'size': 'medium'}
    overrides = {'size': 'large', 'style': 'bold'}
    merged = {**defaults, **overrides}
    print(f"defaults: {defaults}")
    print(f"overrides: {overrides}")
    print(f"merged: {merged}")

def add(a, b, c):
    """Add three numbers."""
    return a + b + c

def connect(host, port, debug=False):
    """Simulate connection."""
    print(f"Connecting to {host}:{port} (debug={debug})")

def calculate(x, y, operation='add'):
    """Perform calculation."""
    if operation == 'add':
        print(f"{x} + {y} = {x + y}")
    elif operation == 'multiply':
        print(f"{x} * {y} = {x * y}")

if __name__ == "__main__":
    main()
def main():
    print("=== Unpacking: * and ** in Function Calls ===\n")
    
    # Unpack list/tuple into positional args
    numbers = 
    print(f"numbers = {numbers}")
    print(f"add(numbers) would fail - it's one arg")
    print(f"add(*numbers) = {add(*numbers)}")  # Unpacks!
    
    print("\n=== Unpack Dict into Keyword Args ===")
    config = {'host': 'localhost', 'port': 8080, 'debug': True}
    print(f"config = {config}")
    connect(**config)  # Same as connect(host='...', port=..., debug=...)
    
    print("\n=== Combined Unpacking ===")
    args = [5, 3]
    kwargs = {'operation': 'multiply'}
    calculate(*args, **kwargs)
    
    print("\n=== Range Example ===")
    # start, stop, step
    bounds = 
    print(f"bounds = {bounds}")
    print(f"list(range(*bounds)) = {list(range(*bounds))}")
    
    print("\n=== Merge Dicts with ** ===")
    defaults = {'color': 'blue', 'size': 'medium'}
    overrides = {'size': 'large', 'style': 'bold'}
    merged = {**defaults, **overrides}
    print(f"defaults: {defaults}")
    print(f"overrides: {overrides}")
    print(f"merged: {merged}")

def add(a, b, c):
    """Add three numbers."""
    return a + b + c

def connect(host, port, debug=False):
    """Simulate connection."""
    print(f"Connecting to {host}:{port} (debug={debug})")

def calculate(x, y, operation='add'):
    """Perform calculation."""
    if operation == 'add':
        print(f"{x} + {y} = {x + y}")
    elif operation == 'multiply':
        print(f"{x} * {y} = {x * y}")

if __name__ == "__main__":
    main()
def main():
    print("=== Unpacking: * and ** in Function Calls ===\n")
    
    # Unpack list/tuple into positional args
    numbers = 
    print(f"numbers = {numbers}")
    print(f"add(numbers) would fail - it's one arg")
    print(f"add(*numbers) = {add(*numbers)}")  # Unpacks!
    
    print("\n=== Unpack Dict into Keyword Args ===")
    config = {'host': 'localhost', 'port': 8080, 'debug': True}
    print(f"config = {config}")
    connect(**config)  # Same as connect(host='...', port=..., debug=...)
    
    print("\n=== Combined Unpacking ===")
    args = [5, 3]
    kwargs = {'operation': 'multiply'}
    calculate(*args, **kwargs)
    
    print("\n=== Range Example ===")
    # start, stop, step
    bounds = 
    print(f"bounds = {bounds}")
    print(f"list(range(*bounds)) = {list(range(*bounds))}")
    
    print("\n=== Merge Dicts with ** ===")
    defaults = {'color': 'blue', 'size': 'medium'}
    overrides = {'size': 'large', 'style': 'bold'}
    merged = {**defaults, **overrides}
    print(f"defaults: {defaults}")
    print(f"overrides: {overrides}")
    print(f"merged: {merged}")

def add(a, b, c):
    """Add three numbers."""
    return a + b + c

def connect(host, port, debug=False):
    """Simulate connection."""
    print(f"Connecting to {host}:{port} (debug={debug})")

def calculate(x, y, operation='add'):
    """Perform calculation."""
    if operation == 'add':
        print(f"{x} + {y} = {x + y}")
    elif operation == 'multiply':
        print(f"{x} * {y} = {x * y}")

if __name__ == "__main__":
    main()
def main():
    print("=== Unpacking: * and ** in Function Calls ===\n")
    
    # Unpack list/tuple into positional args
    numbers = 
    print(f"numbers = {numbers}")
    print(f"add(numbers) would fail - it's one arg")
    print(f"add(*numbers) = {add(*numbers)}")  # Unpacks!
    
    print("\n=== Unpack Dict into Keyword Args ===")
    config = {'host': 'localhost', 'port': 8080, 'debug': True}
    print(f"config = {config}")
    connect(**config)  # Same as connect(host='...', port=..., debug=...)
    
    print("\n=== Combined Unpacking ===")
    args = [5, 3]
    kwargs = {'operation': 'multiply'}
    calculate(*args, **kwargs)
    
    print("\n=== Range Example ===")
    # start, stop, step
    bounds = 
    print(f"bounds = {bounds}")
    print(f"list(range(*bounds)) = {list(range(*bounds))}")
    
    print("\n=== Merge Dicts with ** ===")
    defaults = {'color': 'blue', 'size': 'medium'}
    overrides = {'size': 'large', 'style': 'bold'}
    merged = {**defaults, **overrides}
    print(f"defaults: {defaults}")
    print(f"overrides: {overrides}")
    print(f"merged: {merged}")

def add(a, b, c):
    """Add three numbers."""
    return a + b + c

def connect(host, port, debug=False):
    """Simulate connection."""
    print(f"Connecting to {host}:{port} (debug={debug})")

def calculate(x, y, operation='add'):
    """Perform calculation."""
    if operation == 'add':
        print(f"{x} + {y} = {x + y}")
    elif operation == 'multiply':
        print(f"{x} * {y} = {x * y}")

if __name__ == "__main__":
    main()

func(*list) unpacks list as positional args. func(**dict) unpacks as keyword args.

unpacking Expand collections into arguments: `f(*[1,2])` is `f(1,2)`.

Forward arguments to other functions

Pass received arguments to another function unchanged.

forwarding.py
def main():
    print("=== Forwarding Arguments ===\n")
    
    # Wrapper forwards to inner function
    print("Wrapper forwarding:")
    wrapper_example(1, 2, 3, name="test", flag=True)
    
    print("\n=== Logging Wrapper ===")
    logged_add(10, 20)
    logged_multiply(5, 3, verbose=True)
    
    print("\n=== Super() Forwarding ===")
    fancy = FancyButton("Click Me", color="blue", size="large")

def wrapper_example(*args, **kwargs):
    """Wrapper that forwards everything."""
    print(f"  Wrapper received: args={args}, kwargs={kwargs}")
    print("  Forwarding to inner function...")
    inner_function(*args, **kwargs)

def inner_function(*args, **kwargs):
    print(f"  Inner got: args={args}, kwargs={kwargs}")

# Logging wrapper pattern
def logged_add(a, b):
    print(f"  [LOG] Calling add({a}, {b})")
    result = a + b
    print(f"  [LOG] Result: {result}")
    return result

def logged_multiply(a, b, verbose=False):
    if verbose:
        print(f"  [VERBOSE] multiply called with a={a}, b={b}")
    result = a * b
    print(f"  [LOG] multiply result: {result}")
    return result

# Class inheritance forwarding
class Button:
    def __init__(self, text, **options):
        self.text = text
        self.options = options
        print(f"  Button created: '{text}' with {options}")

class FancyButton(Button):
    def __init__(self, text, **options):
        # Add default options
        options.setdefault('border', True)
        # Forward to parent
        super().__init__(text, **options)
        print("  (FancyButton enhancements applied)")

if __name__ == "__main__":
    main()

Wrapper functions use *args, **kwargs to forward all arguments.

Exercise: decorators.py

Use *args/**kwargs in decorator functions