Python functions support default parameter values and can accept a flexible number of extra positional and keyword arguments.
def greet(name, greeting="Hello"): # default value used if not supplied
return f"{greeting}, {name}!"
greet("Rahim") # "Hello, Rahim!"
greet("Karim", "Hi") # "Hi, Karim!"
def total(*args): # *args collects extra positional args into a tuple
return sum(args)
total(1, 2, 3, 4) # 10
def describe(**kwargs): # **kwargs collects extra keyword args into a dict
for key, value in kwargs.items():
print(f"{key}: {value}")
describe(name="Rahim", age=25)
def mutable_default(items=[]): # ⚠️ classic Python trap — see below
items.append(1)
return items
*args gathers any number of extra positional arguments into a tuple inside the function, and **kwargs similarly gathers extra keyword arguments into a dict — together they let a function accept a genuinely flexible signature, commonly seen in wrapper/decorator functions that need to forward arbitrary arguments to another function untouched (def wrapper(*args, **kwargs): return original(*args, **kwargs)).
Mutable default arguments are a famous Python trap: a default value like items=[] is created once, when the function is defined, not each time it's called — every call that doesn't supply its own items shares and mutates the same list across calls, silently accumulating state between unrelated calls. The standard fix is def mutable_default(items=None): items = items if items is not None else [], checking for None and creating a fresh list inside the function body each time.