Python *args and **kwargs Practice Problems & Exercises
Practice: *args and **kwargs
← Back to lessonEasy
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.
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
passExpected Output
6
16
100
0Hints
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).
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.
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
passExpected 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.
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.
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 ParisHints
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).
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.
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
passExpected Output
(1, 9)
(42, 42)
Caught: min_max requires at least one argumentHints
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
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.
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
passExpected 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).
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.
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
passExpected 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.
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.
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
passExpected 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).
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.
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
passExpected 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
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).
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
passExpected 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.
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.
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 capturesnamein a closure. The decorator stores the function but returns it unchanged so it can still be called directly.call()uses*args, **kwargsfor 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 viaself.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
passExpected 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.
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.
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()returnsdecorator, which returnswrapper. The outer function captures config (max_attempts,exceptions), the middle capturesfunc, 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 exceptionscatches 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
tenacityandbackoff.
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
passExpected 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.
