Perfect.
DAY 14 is one of the highest ROI days for interviews and real-world bugs.

If you master today:

  • Default-argument bugs disappear forever
  • You’ll confidently read any function signature
  • You’ll handle *args/**kwargs like a senior engineer
  • You’ll explain Python call semantics clearly on a whiteboard

🧠 DAY 14 — Function Arguments & Call Semantics

(positional-only, keyword-only, defaults, *args, **kwargs)


🔑 CORE IDEA OF DAY 14

Python argument passing is deterministic, ordered, and rule-based — not flexible magic.

Everything today flows from:

  1. Binding rules
  2. Evaluation time
  3. Mutability

1️⃣ The Golden Rule: How Python Binds Arguments

When a function is called, Python binds arguments in this order:

  1. Positional-only arguments
  2. Positional-or-keyword arguments
  3. *args (extra positional)
  4. Keyword-only arguments
  5. **kwargs (extra keywords)

This happens before function body executes.


2️⃣ Basic Binding Example

def f(a, b, c):
    print(a, b, c)

f(1, 2, 3)
f(a=1, b=2, c=3)
f(1, b=2, c=3)

All valid.

But:

f(a=1, 2, 3)   # ❌ SyntaxError

Why?

  • Positional arguments must come before keyword arguments.

3️⃣ Default Arguments (EVALUATED ONCE — NOT PER CALL)

🔥🔥🔥 This is one of the most important Python rules 🔥🔥🔥

def f(x=[]):
    x.append(1)
    return x
print(f())
print(f())
print(f())

Output:

[1]
[1, 1]
[1, 1, 1]

WHY THIS HAPPENS

  • Default values are evaluated at function definition time
  • The same object is reused across calls
  • This is by design, not a bug

4️⃣ The Correct Pattern (INTERVIEW EXPECTED)

def f(x=None):
    if x is None:
        x = []
    x.append(1)
    return x

Why this works:

  • None is immutable
  • New list created per call

5️⃣ Why Python Did This (Design Rationale)

Python chose:

  • Predictability
  • Performance
  • Simpler implementation

Changing this would:

  • Break backward compatibility
  • Add hidden allocations
  • Make behavior less explicit

Senior interviewers expect this explanation.


6️⃣ *args — Variable Positional Arguments

def f(*args):
    print(args)

f(1, 2, 3)

Inside function:

args == (1, 2, 3)   # tuple

Important:

  • args is a tuple
  • Immutable
  • Safe to reuse

7️⃣ **kwargs — Variable Keyword Arguments

def f(**kwargs):
    print(kwargs)

f(a=1, b=2)

Inside function:

kwargs == {"a": 1, "b": 2}   # dict

Important:

  • kwargs is a new dict
  • Created at call time

8️⃣ Mixing Arguments (ORDER MATTERS)

Correct order in function definition:

def f(pos1, pos2, /, p_or_k, *, kw1, kw2, **kwargs):
    ...

Order:

positional-only → normal → *args → keyword-only → **kwargs

9️⃣ Positional-Only Arguments (/) — ADVANCED BUT IMPORTANT

def f(a, b, /):
    print(a, b)

f(1, 2)        # OK
f(a=1, b=2)    # ❌ TypeError

Why this exists:

  • API stability
  • Performance
  • C-extension compatibility

Used heavily in:

  • Built-ins
  • Standard library
  • NumPy / Pandas APIs

🔟 Keyword-Only Arguments (*) — SAFETY TOOL

def f(a, *, b, c):
    print(a, b, c)

f(1, b=2, c=3)   # OK
f(1, 2, 3)       # ❌

Why useful:

  • Prevent argument confusion
  • Self-documenting APIs
  • Safer refactoring

11️⃣ Argument Packing & Unpacking

Packing

def f(*args, **kwargs):
    ...

Unpacking

args = (1, 2)
kwargs = {"c": 3}

f(*args, **kwargs)

This is call-time syntax, not magic.


12️⃣ Mutability + Arguments = Subtle Bugs

def f(x):
    x.append(1)

lst = []
f(lst)
print(lst)

[1]

But:

def g(x):
    x = x + [1]

lst = []
g(lst)
print(lst)

[]

Why?

  • First mutates object
  • Second rebinds local name

(You’ve already seen this — now you see it in call semantics.)


🔥 INTERVIEW TRAPS (DAY 14)

Q1

def f(a, b=10, c=20):
    print(a, b, c)

f(1, c=30)

✔ Output: 1 10 30


Q2

def f(*args):
    args += (1,)

a = (1, 2)
f(a)
print(a)

(1, 2)
(Args is local tuple, rebinding)


Q3

def f(**kwargs):
    kwargs["x"] = 1

d = {}
f(**d)
print(d)

{}
(New dict created)


🧠 DAY 14 MENTAL CHECKLIST

Before writing a function:

  1. Are defaults immutable?
  2. Should some args be keyword-only?
  3. Do I need positional-only for API safety?
  4. Am I mutating passed-in objects?
  5. Will future callers misuse this signature?

📝 DAY 14 ASSIGNMENT (MANDATORY)

1️⃣ Predict output (NO RUNNING):

def f(x, y=[]):
    y.append(x)
    return y

print(f(1))
print(f(2))
print(f(3, []))

2️⃣ Explain clearly:

  • Why default arguments are evaluated once
  • Why *args is a tuple but **kwargs is a dict

3️⃣ Interview-style design question:

When would you intentionally use positional-only arguments?


🔜 DAY 15 PREVIEW (🔥🔥🔥)

DAY 15 — Closures & Decorators (Cell Objects, Free Variables, Wrapping)

This is where:

  • Decorators stop feeling magical
  • Late-binding bugs finally make sense
  • Functional programming clicks

When ready, say 👉 “START DAY 15”