Python Essentials for AI Engineer

Course Content

Python Essentials for AI Engineer

6 sections · 48 lessons

What are *args and **kwargs?


What you need to know

Packing in a definition

Python
def log(*args, **kwargs):    print(args)       # a tuple of positional arguments    print(kwargs)     # a dict of keyword argumentslog("upi", "failed", level="INFO", retry=True)# ('upi', 'failed')# {'level': 'INFO', 'retry': True}

The names args and kwargs are only convention; the stars do the work. The order in a definition is fixed: normal parameters, *args, keyword-only parameters, **kwargs.

Unpacking at a call site

Python
def create(model, temperature, max_tokens):    return f"{model} {temperature} {max_tokens}"positional = ["chat-small", 0.2, 256]config = {"model": "chat-large", "temperature": 0.0, "max_tokens": 512}print(create(*positional))       # chat-small 0.2 256print(create(**config))          # chat-large 0.0 512defaults = {"temperature": 0.2, "max_tokens": 256}override = {"max_tokens": 1024}print({**defaults, **override})  # {'temperature': 0.2, 'max_tokens': 1024}

{**a, **b} merges dicts, with later keys winning — the same result as a | b in Python 3.9+.

The cost of flexibility

A function that takes **kwargs accepts any name, including typos. call(temprature=0) does not fail; the value is just never used. For a public function, an explicit signature is safer; use **kwargs when you really are forwarding arguments you don't own.

A real-life example

LLM APIs fail now and then with timeouts and rate limits. A generic retry wrapper must work for any function with any arguments, which is exactly what *args and **kwargs are for:

Python
import timedef with_retry(fn, *args, attempts=3, delay=0.1, **kwargs):    for attempt in range(1, attempts + 1):        try:            return fn(*args, **kwargs)             # forward everything        except TimeoutError:            if attempt == attempts:                raise            time.sleep(delay * 2 ** (attempt - 1)) # 0.1s, 0.2s, ...calls = {"n": 0}def flaky_llm(prompt, *, temperature=0.2):    calls["n"] += 1    if calls["n"] < 3:        raise TimeoutError("upstream timeout")    return f"answer to {prompt!r} at T={temperature}"print(with_retry(flaky_llm, "Where is my refund?", temperature=0.0))# answer to 'Where is my refund?' at T=0.0print(calls["n"])     # 3

with_retry never needs to know flaky_llm's signature. The same wrapper works for an embedding call, a vector-database query or a file upload. Notice that attempts and delay are keyword-only (they come after *args), so they never collide with the wrapped function's arguments.

Follow-up questions to expect

  • "What order do parameters go in?" — Positional parameters, *args, keyword-only parameters, then **kwargs.
  • "Can you use names other than args and kwargs?" — Yes, *items and **options work the same. The convention just helps readers.
  • "What is the risk of **kwargs?" — Misspelt keyword arguments are silently accepted, and the signature no longer documents what the function needs.