Python interview question
What are *args and **kwargs, and how are they used in test helpers?
Short answer
*args collects extra positional arguments into a tuple and **kwargs collects extra keyword arguments into a dict. The same operators unpack sequences and dicts into calls. They are used for wrappers and decorators that forward arguments, and for builders that accept field overrides.
Answer
*args and **kwargs let a function accept a variable number of arguments. *args gathers any extra positional arguments into a tuple; **kwargs gathers any extra keyword arguments into a dict. The names are only conventions; the * and ** are what matter. The same operators work in reverse at a call site to unpack a sequence or mapping into arguments.
Collecting
def describe(*args, **kwargs):
print(type(args), args)
print(type(kwargs), kwargs)
describe(1, 2, browser="firefox", headless=True)
# <class 'tuple'> (1, 2)
# <class 'dict'> {'browser': 'firefox', 'headless': True}
Unpacking
def login(username, password, remember=False): ...
creds = ("qa_user", "s3cret")
options = {"remember": True}
login(*creds, **options) # login("qa_user", "s3cret", remember=True)
merged = {**default_headers, **{"X-Trace": "abc"}} # dict merge, later keys win
Parameter order
The full order in a signature is: positional-only, regular, *args, keyword-only, **kwargs.
def request(method, url, /, *args, timeout: float = 10, **kwargs): ...
- Parameters before
/are positional-only (Python 3.8+). - Parameters after
*args(or a bare*) are keyword-only, which is useful for flags:create_user(name, *, admin=False)forces callers to writeadmin=True, making tests more readable.
Uses in test code
Builders with overrides
def build_user(**overrides) -> dict:
user = {
"email": "qa@example.com",
"role": "member",
"active": True,
}
unknown = overrides.keys() - user.keys()
if unknown:
raise TypeError(f"Unknown fields: {unknown}")
return {**user, **overrides}
admin = build_user(role="admin")
The guard against unknown keys prevents typos like rol="admin" from silently doing nothing.
Wrappers that forward everything
import functools, logging, time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
logging.info("%s took %.2fs", func.__name__, time.perf_counter() - start)
return wrapper
A decorator does not know the wrapped function’s signature, so it accepts and forwards *args, **kwargs. functools.wraps preserves the name and docstring, which matters for pytest reports.
Thin API client methods
class Api:
def __init__(self, session, base_url):
self.session, self.base_url = session, base_url
def get(self, path, **kwargs):
return self.session.get(f"{self.base_url}{path}", timeout=10, **kwargs)
api.get("/orders", params={"status": "paid"}, headers={"X-Debug": "1"})
Caveats
- Overusing
**kwargshides the real interface: editors cannot autocomplete and typos go undetected. Prefer explicit parameters for public helpers, and usetyping.Unpackwith aTypedDict(Python 3.12+) if you need typed kwargs. - Passing the same key both explicitly and via
**kwargsraisesTypeError: got multiple values for keyword argument.
What interviewers look for
- Tuple versus dict, positional versus keyword.
- Unpacking at call sites, including dict merging.
- A real use: decorators forwarding arguments, builders with overrides.
- Awareness of the readability cost and keyword-only parameters.
A strong answer covers
- *args collects positional arguments into a tuple
- **kwargs collects keyword arguments into a dict
- Unpacking in calls with * and **
- Forwarding arguments in wrappers and decorators
- Mentions parameter order or keyword-only and positional-only parameters