Skip to content

Function Arguments Interview Questions & Answers

16 questions Updated 2026-06-18 Share:

Python interview questions on args and kwargs, positional vs keyword arguments, defaults, keyword-only and positional-only parameters, unpacking at the call site, and parameter ordering rules.

Read the in-depth guidePython Function Arguments Explained — *args, **kwargs, Defaults, and Keyword-Only(opens in new tab)
16 of 16

In a function signature, *args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dict. They let a function accept a variable number of arguments. The names are convention — only the */** matter.

def log(level, *args, **kwargs):
    print(level, args, kwargs)

log("INFO", 1, 2, user="ada", id=7)
# INFO (1, 2) {'user': 'ada', 'id': 7}

args is always a tuple and kwargs always a dict. They're essential for writing wrappers/decorators that forward arbitrary arguments through to another callable.

Positional arguments are matched to parameters by their order. Keyword arguments are matched by name (param=value), so order doesn't matter among them. At the call site you can mix the two, but every positional argument must come before any keyword argument.

def greet(name, greeting): ...

greet("Ada", "Hello")              # both positional
greet(name="Ada", greeting="Hi")   # both keyword (order free)
greet("Ada", greeting="Hi")        # mix: positional first
greet(name="Ada", "Hi")            # SyntaxError — kw before positional

Keyword arguments make calls self-documenting and let you skip over earlier parameters that have defaults. Use them for clarity on boolean flags and long argument lists.

A parameter with name=value is optional — if the caller omits it, the default is used. Defaults are evaluated once, when the def runs, so using a mutable default ([], {}) is a classic trap: the same object persists across calls.

def connect(host, port=5432, timeout=30):
    ...
connect("db")                 # uses port=5432, timeout=30
connect("db", timeout=5)      # override one by keyword

def bad(item, bucket=[]):     # DON'T — shared list
    bucket.append(item); return bucket

The safe pattern for a mutable default is bucket=None plus if bucket is None: bucket = [] inside the body. Parameters with defaults must come after those without.

Any parameter listed after a bare * (or after *args) is keyword-only — it can never be passed positionally and must be named at the call site. This forces clearer calls and prevents accidental positional mistakes.

def make_request(url, *, timeout=30, verify=True):
    ...

make_request("http://x", timeout=5)     # OK
make_request("http://x", 5)             # TypeError — timeout is kw-only

Keyword-only parameters are great for optional flags whose meaning isn't obvious from position (especially booleans). The lone * is just a separator; it doesn't collect anything.

Parameters listed before a / in the signature are positional-only (Python 3.8+) — they cannot be passed by keyword. This is useful for APIs where the parameter name is an implementation detail you don't want callers to depend on.

def divide(a, b, /):
    return a / b

divide(10, 2)          # OK
divide(a=10, b=2)      # TypeError — a, b are positional-only

It also frees those names for use in **kwargs. Many built-ins (like len, pow) are positional-only. Combined with *, a signature can have positional-only, normal, and keyword-only sections.

A full signature follows a fixed order: positional-only /, then normal, then *args, then keyword-only, then **kwargs. Within each group, parameters without defaults precede those with defaults.

def f(pos_only, /, normal, *args, kw_only, **kwargs):
    ...

# call-site unpacking mirrors this:
def g(a, b, c): ...
nums = [1, 2, 3]
g(*nums)                 # spread list into positionals
g(**{"a": 1, "b": 2, "c": 3})   # spread dict into keywords

Getting the order wrong is a SyntaxError. The *// markers partition the signature; remember the sequence "positional-only → normal → varargs → keyword-only → varkwargs."

The default is evaluated once at definition time and shared across all calls. A [] or {} default therefore persists and accumulates between calls. Use None as the sentinel and create a fresh object inside.

def add(item, bucket=[]):        # BUG: one shared list
    bucket.append(item); return bucket
add(1); add(2)                   # [1, 2] !

def add(item, bucket=None):      # correct
    if bucket is None: bucket = []
    bucket.append(item); return bucket

Rule of thumb: never use a mutable literal as a default — default to None and build the object in the body.

At a call, *iterable spreads items into positional arguments and **mapping spreads into keyword arguments. It's the inverse of *args/ **kwargs in a definition. You can mix them and even use * multiple times (3.5+).

def f(a, b, c): ...
args = (1, 2); f(*args, 3)            # f(1, 2, 3)
kw = {"b": 2, "c": 3}; f(1, **kw)     # f(1, b=2, c=3)
f(*[1], *[2], **{"c": 3})             # multiple unpacks

Rule of thumb: */** at the call site unpack collections into arguments; in the signature they collect arguments.

Neither exactly — it's "pass by object reference" (call by sharing). The function gets a reference to the same object. Mutating it (e.g. list.append) is visible to the caller; rebinding the parameter (x = ...) only changes the local name, not the caller's variable.

def f(lst, x):
    lst.append(1)        # caller sees this (mutation)
    x = 99               # caller does NOT see this (rebinding)

data, n = [], 0
f(data, n)               # data == [1], n == 0

Rule of thumb: mutations to the object propagate; reassigning the parameter name does not.

Yes — since Python 3.7, **kwargs is an ordinary dict and preserves the order the keyword arguments were passed. This lets you forward or process kwargs predictably (e.g. building HTML attributes in source order).

def tag(**attrs):
    return " ".join(f'{k}="{v}"' for k, v in attrs.items())

tag(id="x", cls="y")     # 'id="x" cls="y"'  -> order preserved

Rule of thumb: you can rely on kwargs insertion order on modern Python.

A lone * marks the start of keyword-only parameters — everything after it must be passed by name. It's used to force clarity at call sites, especially for boolean flags or options.

def connect(host, *, timeout=30, retries=3):
    ...
connect("db", timeout=5)        # OK
connect("db", 5)                # TypeError: too many positional args

Rule of thumb: put * before options you want callers to name explicitly, avoiding ambiguous positional flags.

Parameters before / are positional-only — they can't be passed by keyword (3.8+). It mirrors many C built-ins (len, abs), lets you rename params freely without breaking callers, and avoids name clashes with **kwargs.

def divide(a, b, /):
    return a / b
divide(10, 2)            # OK
divide(a=10, b=2)        # TypeError: positional-only

Rule of thumb: use / for parameters whose names are implementation details or that must accept arbitrary keyword keys via **kwargs.

A parameter is the variable in the function definition; an argument is the actual value passed at the call site. Parameters define the interface; arguments fill it in.

def greet(name):         # `name` is a parameter
    ...
greet("Ada")             # "Ada" is an argument

Rule of thumb: parameters live in the def, arguments live in the call.

Accept *args, **kwargs and pass them straight through with *args, **kwargs. This is the standard pattern for wrappers, decorators, and super().__init__ chains that shouldn't care about the exact signature.

def wrapper(*args, **kwargs):
    log("calling")
    return target(*args, **kwargs)     # transparent forwarding

Rule of thumb: *args, **kwargs in and out is how you write signature-agnostic wrappers.

Use keywords for booleans, numbers, and any value whose meaning isn't obvious at the call site. f(True, False) is cryptic; f(verbose=True, cache=False) is self-documenting and resilient to parameter reordering.

open("f.txt", "w", buffering=1)        # named buffering reads clearly
split(text, maxsplit=1)                # vs split(text, 1)

Rule of thumb: pass literals (especially bare True/False/numbers) by keyword for readability.

Once, when the def executes (definition time) — not on each call. So a default referencing a variable captures its value at definition, and a default like datetime.now() is frozen to one moment. Use None + compute inside for per-call defaults.

import time
def stamp(t=time.time()):    # frozen at def time
    return t
stamp(); time.sleep(1); stamp()    # same value both times

def stamp(t=None):                  # fresh each call
    return time.time() if t is None else t

Rule of thumb: if a default must be recomputed per call, default to None and build it in the body.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel