Skip to main content

Python *args and **kwargs Practice Problems & Exercises

Practice: *args and **kwargs

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Flexible SumEasy
*argskeyword-only

Write a function that accepts any number of positional numeric arguments and a keyword-only start parameter (default 0). Return the sum of all positional arguments plus start.

Python
def flexible_sum(*args, start=0):
    return start + sum(args)

print(flexible_sum(1, 2, 3))
print(flexible_sum(1, 2, 3, start=10))
print(flexible_sum(start=100))
print(flexible_sum())
Solution
def flexible_sum(*args, start=0):
return start + sum(args)

Key insight: Because *args appears before start, the start parameter is automatically keyword-only. Callers cannot accidentally pass a start value positionally — they must write start=10. This is exactly what you want for an optional modifier that should not be confused with the main data.

def flexible_sum(*args, start=0):
    """Return the sum of all positional args plus start.

    >>> flexible_sum(1, 2, 3)
    6
    >>> flexible_sum(1, 2, 3, start=10)
    16
    >>> flexible_sum(start=100)
    100
    """
    # TODO: implement
    pass
Expected Output
6
16
100
0
Hints

Hint 1: args is a tuple — you can pass it directly to the built-in sum() function.

Hint 2: sum() accepts an optional start argument too, but here just add start to sum(args).

#2Tag BuilderEasy
**kwargsstring-formatting

Write a function that takes a tag name and any number of keyword arguments representing HTML attributes. Return the opening tag as a string. If no attributes are given, return just the tag in angle brackets.

Python
def build_tag(tag, **attrs):
    attr_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
    if attr_str:
        return f"<{tag} {attr_str}>"
    return f"<{tag}>"

print(build_tag("a", href="/home", id="nav"))
print(build_tag("br"))
print(build_tag("input", type="text", placeholder="name"))
Solution
def build_tag(tag, **attrs):
attr_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
if attr_str:
return f"<{tag} {attr_str}>"
return f"<{tag}>"

Key insight: **attrs collects all keyword arguments the caller passes (except tag) into a regular dict. You iterate it like any dict. The function signature is clean — callers write build_tag("a", href="/home") instead of passing a dict manually.

def build_tag(tag, **attrs):
    """Build an HTML opening tag from keyword arguments.

    >>> build_tag("a", href="/home", id="nav")
    '<a href="/home" id="nav">'
    >>> build_tag("br")
    '<br>'
    """
    # TODO: implement
    pass
Expected Output
<a href="/home" id="nav">
<br>
<input type="text" placeholder="name">
Hints

Hint 1: Iterate over attrs.items() to build key="value" pairs.

Hint 2: Join the attribute pairs with spaces, then place them after the tag name.

Hint 3: Handle the case where attrs is empty — just return the tag with angle brackets.

#3Unpack and CallEasy
* unpacking** unpacking

You are given a function create_profile(name, age, city="Unknown") and two variables: a list of positional values and a dict of keyword values. Call the function using * and ** unpacking to produce the correct output.

Python
def create_profile(name, age, city="Unknown"):
    return f"{name}, {age}, from {city}"

args_list = ["Alice", 30]
kwargs_dict = {"city": "Paris"}

result = create_profile(*args_list, **kwargs_dict)
print(result)
Solution
result = create_profile(*args_list, **kwargs_dict)

Key insight: *args_list spreads ["Alice", 30] into two positional arguments, and **kwargs_dict spreads {"city": "Paris"} into the keyword argument city="Paris". The call becomes equivalent to create_profile("Alice", 30, city="Paris"). This pattern is common in config-driven code where arguments come from files or dicts rather than being hardcoded.

def create_profile(name, age, city="Unknown"):
    return f"{name}, {age}, from {city}"

# Given these data structures:
args_list = ["Alice", 30]
kwargs_dict = {"city": "Paris"}

# TODO: Call create_profile using * and ** unpacking
# so that it prints: Alice, 30, from Paris
result = None  # fix this line
print(result)
Expected Output
Alice, 30, from Paris
Hints

Hint 1: Use *args_list to unpack the list as positional arguments.

Hint 2: Use **kwargs_dict to unpack the dict as keyword arguments.

Hint 3: You can combine both in a single call: func(*list, **dict).

#4Min and Max FinderEasy
*argstuple-return

Write a function that accepts any number of positional arguments and returns a tuple of (minimum, maximum). If no arguments are provided, raise a ValueError.

Python
def min_max(*args):
    if not args:
        raise ValueError("min_max requires at least one argument")
    return (min(args), max(args))

print(min_max(3, 1, 4, 1, 5, 9))
print(min_max(42))
try:
    min_max()
except ValueError as e:
    print(f"Caught: {e}")
Solution
def min_max(*args):
if not args:
raise ValueError("min_max requires at least one argument")
return (min(args), max(args))

Key insight: *args gives you a tuple, which works directly with min() and max(). The empty-args guard is important — without it, min() and max() would raise their own less descriptive ValueError. Writing your own error message makes the API self-documenting.

def min_max(*args):
    """Return a (min, max) tuple from any number of arguments.

    Raise ValueError if no arguments are given.

    >>> min_max(3, 1, 4, 1, 5, 9)
    (1, 9)
    >>> min_max(42)
    (42, 42)
    """
    # TODO: implement
    pass
Expected Output
(1, 9)
(42, 42)
Caught: min_max requires at least one argument
Hints

Hint 1: Check if args is empty (falsy) and raise ValueError if so.

Hint 2: Use the built-in min() and max() functions on the args tuple.


Medium

#5Config MergerMedium
** unpackingdict-mergingprecedence

Write a function that accepts any number of dicts via *configs and merges them left-to-right, where later dicts override keys from earlier ones. Return the merged dict.

Python
def merge_configs(*configs):
    merged = {}
    for config in configs:
        merged.update(config)
    return merged

base = {"host": "localhost", "port": 8080, "debug": False}
dev = {"debug": True, "port": 3000}
local = {"port": 9999}

print(merge_configs(base, dev, local))
print(merge_configs(base))
print(merge_configs())
Solution
def merge_configs(*configs):
merged = {}
for config in configs:
merged.update(config)
return merged

Alternative using unpacking (Python 3.9+):

def merge_configs(*configs):
from functools import reduce
return reduce(lambda a, b: a | b, configs, {})

Key insight: This is the production pattern for layered configuration — base defaults, environment overrides, local overrides. The left-to-right order with later-wins precedence matches how tools like Docker Compose and Kubernetes merge config layers. Using *configs makes the function accept any number of layers.

def merge_configs(*configs):
    """Merge multiple config dicts left-to-right.
    Later dicts override earlier ones.

    >>> base = {"host": "localhost", "port": 8080, "debug": False}
    >>> dev = {"debug": True, "port": 3000}
    >>> local = {"port": 9999}
    >>> merge_configs(base, dev, local)
    {'host': 'localhost', 'port': 9999, 'debug': True}
    """
    # TODO: implement
    pass
Expected Output
{'host': 'localhost', 'port': 9999, 'debug': True}
{'host': 'localhost', 'port': 8080, 'debug': False}
{}
Hints

Hint 1: Start with an empty dict and update it with each config in order.

Hint 2: dict.update() or {**merged, **next} both work — later keys overwrite earlier ones.

Hint 3: Handle the case where no configs are passed (return empty dict).

#6Argument ForwarderMedium
forwarding*args**kwargs

Write a function that takes a callable as its first argument, followed by any positional and keyword arguments. It should print a log line showing the function name and arguments, then forward all arguments to the function and return its result.

Python
def log_and_call(func, *args, **kwargs):
    print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
    return func(*args, **kwargs)

def add(a, b, c=0):
    return a + b + c

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

print(log_and_call(add, 1, 2, c=10))
print(log_and_call(greet, name="Alice", greeting="Hi"))
Solution
def log_and_call(func, *args, **kwargs):
print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
return func(*args, **kwargs)

Key insight: This is the forwarding pattern — the foundation of every Python decorator. The wrapper captures *args, **kwargs (packing), then passes them to the real function with func(*args, **kwargs) (unpacking). The wrapper never needs to know what parameters func expects. This makes the pattern completely generic and reusable across any function signature.

def log_and_call(func, *args, **kwargs):
    """Print the function name and its arguments,
    then call the function and return its result.

    >>> def add(a, b, c=0):
    ...     return a + b + c
    >>> log_and_call(add, 1, 2, c=10)
    Calling add with args=(1, 2) kwargs={'c': 10}
    13
    """
    # TODO: implement
    pass
Expected Output
Calling add with args=(1, 2) kwargs={'c': 10}
13
Calling greet with args=() kwargs={'name': 'Alice', 'greeting': 'Hi'}
Hi, Alice!
Hints

Hint 1: Use func.__name__ to get the function name for the log message.

Hint 2: Print the log line, then call func(*args, **kwargs) to forward everything.

Hint 3: Return the result of the forwarded call.

#7Keyword FilterMedium
**kwargsdict-filtering

Write a function that accepts a list of allowed key names and any keyword arguments. Return a new dict containing only the keyword arguments whose keys appear in the allowed list. This pattern is useful when sanitizing kwargs before forwarding them to another function.

Python
def filter_kwargs(allowed_keys, **kwargs):
    allowed = set(allowed_keys)
    return {k: v for k, v in kwargs.items() if k in allowed}

print(filter_kwargs(["name", "age"], name="Alice", age=30, role="admin"))
print(filter_kwargs(["x"], a=1, b=2))
print(filter_kwargs(["x", "y", "z"], x=1, y=2, z=3))
Solution
def filter_kwargs(allowed_keys, **kwargs):
allowed = set(allowed_keys)
return {k: v for k, v in kwargs.items() if k in allowed}

Key insight: This pattern appears in production code when you need to forward a subset of kwargs to a downstream function that does not accept all of them. For example, a UI framework might accept **kwargs and split them into layout-related kwargs and style-related kwargs, forwarding each subset to the appropriate renderer.

def filter_kwargs(allowed_keys, **kwargs):
    """Return a new dict containing only keys in allowed_keys.

    >>> filter_kwargs(["name", "age"], name="Alice", age=30, role="admin")
    {'name': 'Alice', 'age': 30}
    """
    # TODO: implement
    pass
Expected Output
{'name': 'Alice', 'age': 30}
{}
{'x': 1, 'y': 2, 'z': 3}
Hints

Hint 1: Use a dict comprehension to filter kwargs by checking if each key is in allowed_keys.

Hint 2: Converting allowed_keys to a set makes the membership check O(1).

#8Defaults with OverrideMedium
** unpackingdict-mergingfunction-design

Write a function that takes host, port, and any keyword overrides. Start with a defaults dict (timeout=30, retries=3, ssl=True, pool_size=5), merge the overrides on top, and include host and port in the returned dict.

Python
def create_connection(host, port, **overrides):
    defaults = {"timeout": 30, "retries": 3, "ssl": True, "pool_size": 5}
    config = {"host": host, "port": port, **defaults, **overrides}
    return config

print(create_connection("db.local", 5432, timeout=10, ssl=False))
print(create_connection("api.prod", 443))
print(create_connection("cache", 6379, timeout=5, retries=10, ssl=False, pool_size=20))
Solution
def create_connection(host, port, **overrides):
defaults = {"timeout": 30, "retries": 3, "ssl": True, "pool_size": 5}
config = {"host": host, "port": port, **defaults, **overrides}
return config

Key insight: The {**defaults, **overrides} merge pattern gives overrides precedence because later keys win in dict literals. This is the standard "defaults with override" pattern used in database drivers, HTTP clients, and configuration systems. The explicit host and port parameters enforce that those are always required, while **overrides provides flexibility for optional settings.

def create_connection(host, port, **overrides):
    """Create a connection config with sensible defaults.
    Any key in overrides replaces the default.

    Defaults:
      timeout=30, retries=3, ssl=True, pool_size=5

    >>> create_connection("db.local", 5432, timeout=10, ssl=False)
    {'host': 'db.local', 'port': 5432, 'timeout': 10, 'retries': 3, 'ssl': False, 'pool_size': 5}
    """
    # TODO: implement
    pass
Expected Output
{'host': 'db.local', 'port': 5432, 'timeout': 10, 'retries': 3, 'ssl': False, 'pool_size': 5}
{'host': 'api.prod', 'port': 443, 'timeout': 30, 'retries': 3, 'ssl': True, 'pool_size': 5}
{'host': 'cache', 'port': 6379, 'timeout': 5, 'retries': 10, 'ssl': False, 'pool_size': 20}
Hints

Hint 1: Define a defaults dict inside the function with all default key-value pairs.

Hint 2: Use {**defaults, **overrides} to merge — overrides win because they come second.

Hint 3: Add host and port to the result dict as well.


Hard

#9Universal Decorator with LoggingHard
decoratorforwardingfunctools.wraps

Write a log_calls decorator that prints the function name with formatted arguments on call, prints the return value, and preserves the original function's __name__ and __doc__. Format the call as CALL func_name(arg1, arg2, key=value) and the return as RETURN repr(result).

Python
import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"CALL {func.__name__}({signature})")
        result = func(*args, **kwargs)
        print(f"RETURN {result!r}")
        return result
    return wrapper

@log_calls
def add(a, b, c=0):
    """Add numbers."""
    return a + b + c

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

print(add(1, 2, c=3))
print(greet("Alice", greeting="Hi"))
print(add.__name__)
print(add.__doc__)
Solution
import functools

def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"CALL {func.__name__}({signature})")
result = func(*args, **kwargs)
print(f"RETURN {result!r}")
return result
return wrapper

Key points:

  • wrapper(*args, **kwargs) captures all arguments regardless of the wrapped function's signature.
  • func(*args, **kwargs) forwards them perfectly to the original function.
  • @functools.wraps(func) copies __name__, __doc__, __module__, and __annotations__ from the original to the wrapper — without it, introspection tools and help() would show "wrapper" instead of the real function name.
  • The repr() formatting ensures strings show their quotes and special characters are escaped in the log output.
import functools

def log_calls(func):
    """Decorator that logs every call with args/kwargs
    and the return value. Preserves __name__ and __doc__.

    >>> @log_calls
    ... def add(a, b, c=0):
    ...     return a + b + c
    >>> add(1, 2, c=3)
    CALL add(1, 2, c=3)
    RETURN 6
    6
    """
    # TODO: implement
    pass
Expected Output
CALL add(1, 2, c=3)
RETURN 6
6
CALL greet('Alice', greeting='Hi')
RETURN 'Hi, Alice!'
Hi, Alice!
add
Add numbers.
Hints

Hint 1: Use @functools.wraps(func) on your inner wrapper to preserve metadata.

Hint 2: Format args as comma-separated repr values and kwargs as key=repr(value) pairs.

Hint 3: Join args formatting and kwargs formatting with a comma to build the full call string.

#10Function Registry with Config DispatchHard
classdecorator-factoryforwarding** merging

Build a Dispatcher class that registers functions by name and supports calling them with argument forwarding and config merging. The call_with_config method should merge a base config dict with per-call keyword overrides (overrides win), then forward everything to the registered function.

Python
class Dispatcher:
    def __init__(self):
        self._registry = {}

    def register(self, name):
        def decorator(func):
            self._registry[name] = func
            return func
        return decorator

    def call(self, name, *args, **kwargs):
        if name not in self._registry:
            raise KeyError(f"No function registered as '{name}'")
        return self._registry[name](*args, **kwargs)

    def call_with_config(self, name, base_config, *args, **overrides):
        merged = {**base_config, **overrides}
        return self.call(name, *args, **merged)

    def list_commands(self):
        return list(self._registry.keys())

d = Dispatcher()

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

@d.register("compute")
def compute(a, b, operation="add"):
    ops = {"add": a + b, "mul": a * b, "sub": a - b}
    return ops[operation]

print(d.call("greet", "Alice"))
print(d.call("greet", "Bob", greeting="Hi"))

base = {"operation": "add"}
print(d.call_with_config("compute", base, 5, 3))
print(d.call_with_config("compute", base, 5, 3, operation="sub"))

print(d.list_commands())
Solution
class Dispatcher:
def __init__(self):
self._registry = {}

def register(self, name):
def decorator(func):
self._registry[name] = func
return func
return decorator

def call(self, name, *args, **kwargs):
if name not in self._registry:
raise KeyError(f"No function registered as '{name}'")
return self._registry[name](*args, **kwargs)

def call_with_config(self, name, base_config, *args, **overrides):
merged = {**base_config, **overrides}
return self.call(name, *args, **merged)

def list_commands(self):
return list(self._registry.keys())

Key design points:

  • register() is a decorator factory — it returns a decorator that captures name in a closure. The decorator stores the function but returns it unchanged so it can still be called directly.
  • call() uses *args, **kwargs for perfect forwarding — it does not need to know the registered function's signature.
  • call_with_config() merges {**base_config, **overrides} so per-call values take precedence, then forwards via self.call() which does the actual dispatch.
  • This is the same pattern used by Flask's @app.route(), Click's @cli.command(), and Celery's @app.task().
class Dispatcher:
    """Registry that maps string names to callables.

    Supports:
    - @dispatcher.register("name") decorator
    - dispatcher.call("name", *args, **kwargs)
    - dispatcher.call_with_config("name", base_dict, *args, **overrides)
    - dispatcher.list_commands() -> list of registered names
    """

    def __init__(self):
        # TODO: initialize registry
        pass

    def register(self, name):
        # TODO: decorator factory
        pass

    def call(self, name, *args, **kwargs):
        # TODO: forward to registered function
        pass

    def call_with_config(self, name, base_config, *args, **overrides):
        # TODO: merge base_config with overrides, then call
        pass

    def list_commands(self):
        # TODO: return list of names
        pass
Expected Output
Hello, Alice!
Hi, Bob!
8
2
['greet', 'compute']
Hints

Hint 1: register() should return a decorator that stores func in a dict under the given name.

Hint 2: call() should look up the name and forward *args, **kwargs with the forwarding pattern.

Hint 3: call_with_config() should merge {**base_config, **overrides} then pass the merged dict as kwargs.

#11Retry Decorator FactoryHard
decorator-factoryforwarding*args/**kwargsexception-handling

Build a retry decorator factory that accepts max_attempts and a tuple of exceptions to catch. The decorated function should be retried up to max_attempts times. On each failure, print Attempt N failed: error_message. If all attempts fail, re-raise the last exception. The wrapper must forward all arguments and preserve function metadata.

Python
import functools
import random

def retry(max_attempts=3, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    print(f"Attempt {attempt} failed: {e}")
                    if attempt == max_attempts:
                        raise
        return wrapper
    return decorator

@retry(max_attempts=3, exceptions=(ValueError,))
def parse_data(data, strict=False):
    """Parse incoming data."""
    if random.random() < 0.6:
        raise ValueError("unlucky")
    return f"got lucky on attempt {random.getstate}"

# Demo with seeded randomness for reproducible output
random.seed(42)

@retry(max_attempts=3, exceptions=(ValueError,))
def flaky_parse(data):
    """Parse incoming data."""
    if random.random() < 0.7:
        raise ValueError("unlucky")
    return f"got lucky on attempt"

try:
    result = flaky_parse("test")
    print(f"Result: {result}")
except ValueError:
    pass

# Always-failing function to show exhaustion
@retry(max_attempts=3, exceptions=(RuntimeError,))
def always_fails():
    raise RuntimeError("always fails")

try:
    always_fails()
except RuntimeError as e:
    print(f"All 3 attempts failed: {e}")

print(parse_data.__name__)
print(parse_data.__doc__)
Solution
import functools

def retry(max_attempts=3, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
print(f"Attempt {attempt} failed: {e}")
if attempt == max_attempts:
raise
return wrapper
return decorator

Key points:

  • This is a three-layer nesting pattern: retry() returns decorator, which returns wrapper. The outer function captures config (max_attempts, exceptions), the middle captures func, and the inner captures *args, **kwargs.
  • wrapper(*args, **kwargs) + func(*args, **kwargs) is perfect forwarding — the retry logic works on any function regardless of its signature.
  • except exceptions catches only the specified exception types. Unrelated exceptions propagate immediately without retry.
  • @functools.wraps(func) ensures the decorated function retains its original name and docstring.
  • This is the exact pattern used by production retry libraries like tenacity and backoff.
import functools

def retry(max_attempts=3, exceptions=(Exception,)):
    """Decorator factory: retry the wrapped function up to
    max_attempts times if it raises one of the given exceptions.

    >>> @retry(max_attempts=3, exceptions=(ValueError,))
    ... def risky():
    ...     # might raise ValueError
    ...     pass
    """
    # TODO: implement the decorator factory
    # It should return a decorator, which returns a wrapper
    # that uses *args/**kwargs forwarding
    pass
Expected Output
Attempt 1 failed: unlucky
Attempt 2 failed: unlucky
Result: got lucky on attempt 3
Attempt 1 failed: always fails
Attempt 2 failed: always fails
Attempt 3 failed: always fails
All 3 attempts failed: always fails
parse_data
Parse incoming data.
Hints

Hint 1: retry() returns a decorator. The decorator takes func and returns a wrapper.

Hint 2: The wrapper uses *args, **kwargs to forward arguments to func on each attempt.

Hint 3: Catch only the specified exception types. Re-raise on the final attempt.

Hint 4: Use @functools.wraps(func) on the wrapper to preserve metadata.

© 2026 EngineersOfAI. All rights reserved.