1️⃣ Why *args and **kwargs exist

Problem:
You don’t always know how many arguments a function will receive.

Solution:

  • *args
    • Collects extra positional arguments
    • Store in touple
    • Normal args must come before *args
  • **kwargs
    • Collects extra keyword arguments
    • Store in dictionary
    • normal arg → *args → **kwargs

2️⃣ *args (positional arguments)

def show(*args):
	print(type(args))  # <class 'tuple'>
    print(args) # (1, 2, 3)
 
show(1, 2, 3)
 
 
### 🔹 Mixing normal args and `*args`
def demo(a, b, *args):
    print(a) # 1
    print(b) # 2
    print(args) # (3, 4, 5)
 
demo(1, 2, 3, 4, 5)

3️⃣ **kwargs (keyword arguments)

def show(**kwargs):
    print(kwargs)
 
show(a=1, b=2) # {'a': 1, 'b': 2}
 
 
def mix(a, b, *args, **kwargs):
    print(a, b) # 1 2
    print(args) # (3, 4)
    print(kwargs) # {'x': 10, 'y': 20}
 
mix(1, 2, 3, 4, x=10, y=20)

4️⃣ Unpacking arguments with * & ** (reverse of collecting)

# Unpacking list/tuple using 
nums = [1, 2, 3]
print(*nums) # 1 2 3
 
# Unpacking dict
data = {"a": 1, "b": 2}
def show(a, b):
    print(a, b) # 1 2
 
show(**data)
 
# Forwarding arguments (very important)
Used heavily in decorators, wrappers, frameworks.
 
def wrapper(*args, **kwargs):
    return original(*args, **kwargs)