Some functions take exactly two values and refuse a third. Others, like print(), happily swallow whatever you hand them. The difference comes down to two small symbols, * and **, and learning Python args kwargs is how you write that second kind. This post covers default values, keyword arguments, *args, **kwargs, and the ordering rules that hold them all together.
“Sparse is better than dense.”
Tim Peters, The Zen of Python (PEP 20)
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 15 minutes
Think about ordering a pizza. Some choices are required (the size), some have a sensible default (medium, if you say nothing), and some are extra toppings you can pile on in any number. A Python function works the same way. With python args kwargs you can write one function that handles all of that: required values, optional ones with a fallback, and a pile of extras you did not know about when you wrote the function. *args gathers extra positional arguments into a tuple. **kwargs gathers extra keyword arguments into a dictionary.
Here is the part that confuses everyone at first: the names args and kwargs are just convention. The real work is done by the * and ** in front of them. This post walks through both sides of the story: how you define a function to accept flexible arguments, and how each call you make maps onto that definition. Every example is short, tested, and shows real output.
The functions tutorial covered plain parameters. Now we answer the follow-up questions. What if a function should accept any number of arguments? What if some arguments are optional? What if you want to force callers to name their arguments so the code reads clearly? Python has a clean answer for each one. The syntax looks a little odd until you see it in action, so let us see it in action.
Table of Contents
Default Arguments
A default argument is a fallback. You give the parameter a value in the function definition, and if the caller does not pass one, Python uses your value. Think of the “medium” pizza size again: if you stay quiet, you still get a sensible result. This lets you write one function that is short to call in the common case and still flexible when someone needs to override a setting. To make it concrete, imagine three users named Rahul, Viraj, and Aditi signing up on your site: one takes the default access level, the other two need something custom.
The diagram maps how Python routes a call onto a function signature. Positional arguments fill the parameters left to right, keyword arguments match by name, *args scoops up any extra positional values into a tuple, and **kwargs scoops up any extra named values into a dictionary. The order shown (positional-only, then regular, then defaults, then *args, then keyword-only, then **kwargs) is the exact order Python demands in a function signature. Get this order wrong and Python throws a SyntaxError before your code even runs, which is one of the first walls beginners hit.
📄 defaults.py: parameters with fallback values
def create_user(name, role="viewer", active=True):
return {"name": name, "role": role, "active": active}
# All three ways to call
print(create_user("Rahul")) # Uses both defaults
print(create_user("Viraj", "editor")) # Overrides role
print(create_user("Aditi", "admin", False)) # Overrides both
▶ Output
{'name': 'Rahul', 'role': 'viewer', 'active': True}
{'name': 'Viraj', 'role': 'editor', 'active': True}
{'name': 'Aditi', 'role': 'admin', 'active': False}
What happened here: the first call passed only a name, so role and active fell back to their defaults. The other two calls overrode them one at a time. One small rule to remember: default parameters must come after the required ones. def f(a, b=1) is fine, but def f(a=1, b) is a SyntaxError. That makes sense once you picture it. Python fills arguments left to right, so the optional ones have to sit at the end.
Keyword Arguments
Think of filling out a courier form. The boxes are labelled Name, Address, and Phone, so it does not matter which box you fill first: the label decides where each value goes. Keyword arguments work the same way. You attach the parameter name at the call site, and Python matches by name instead of position. Below, we send a meeting invite to our colleague Viraj without worrying about argument order.
📄 keyword.py: name your arguments for clarity
def send_email(to, subject, body, cc=None, urgent=False):
print(f"To: {to}")
print(f"Subject: {subject}")
print(f"Urgent: {urgent}")
if cc:
print(f"CC: {cc}")
# Keyword arguments, order does not matter
send_email(
subject="Meeting Tomorrow",
to="viraj@example.com",
body="Please confirm attendance.",
urgent=True,
)
▶ Output
To: viraj@example.com Subject: Meeting Tomorrow Urgent: True
What happened here: we passed the arguments by name, in a scrambled order, and Python still matched each one to the right parameter. That is the whole point. Keyword arguments make a call read like a sentence. send_email(urgent=True) tells you exactly what the True is for. send_email(True) leaves you guessing. A good habit: once a function has more than two or three parameters, name them at the call site.
*args: Variable Positional Arguments
Sometimes you do not know how many values the caller will hand you. A “sum these numbers” function might get two numbers today and twenty tomorrow. Picture a grocery bag: it does not care whether you drop in one apple or ten, it just holds whatever you put in. That is *args. The * tells Python to take every leftover positional argument and pack them into a single tuple.
📄 args.py: accept any number of positional arguments
def total(*numbers):
"""Sum any number of arguments."""
print(f"Received: {numbers} (type: {type(numbers).__name__})")
return sum(numbers)
print(total(1, 2, 3))
print(total(10, 20, 30, 40, 50))
print(total()) # Zero arguments, still valid
# Mix with regular parameters
def announce(event, *participants):
print(f"Event: {event}")
for p in participants:
print(f" - {p}")
announce("Python Meetup", "Rahul", "Anvi", "Viraj")
▶ Output
Received: (1, 2, 3) (type: tuple) 6 Received: (10, 20, 30, 40, 50) (type: tuple) 150 Received: () (type: tuple) 0 Event: Python Meetup - Rahul - Anvi - Viraj
What happened here: *args caught every extra positional argument and bundled them into a tuple, even when there were none (you get an empty tuple, not an error). The name args is only a convention. You could write *numbers or *items and it would behave the same way. The asterisk is what does the work. Once inside the function, the result is just an ordinary tuple you can loop over or pass to sum(). The meetup example works the same way: three friends named Rahul, Anvi, and Viraj register, and all three land in the participants tuple.
**kwargs: Variable Keyword Arguments
If *args is the grocery bag for loose values, **kwargs is a box of labelled jars. Each extra argument arrives with a name attached, and Python packs them into a dictionary where the name is the key. This is perfect when a function takes a bunch of optional, named settings and you do not want to list every single one in the signature. Say two developers named Pravin and Aviraj create profiles on your platform and each fills in different fields: one function handles both.
📄 kwargs.py: accept any number of keyword arguments
def build_profile(name, **details):
"""Build a user profile from arbitrary fields."""
profile = {"name": name}
profile.update(details)
return profile
p1 = build_profile("Pravin", age=27, city="Mumbai", role="Backend")
p2 = build_profile("Aviraj", age=24, stack="React")
print(p1)
print(p2)
▶ Output
{'name': 'Pravin', 'age': 27, 'city': 'Mumbai', 'role': 'Backend'}
{'name': 'Aviraj', 'age': 24, 'stack': 'React'}
What happened here: each name=value pair after the first argument landed inside the details dictionary, keyed by its name. Just like *args, the word kwargs is only a convention. The double asterisk is the real worker. You meet this pattern everywhere in real Python, from Django views to Flask routes to any function that needs flexible, optional configuration.
Combining *args and **kwargs
The full power of Python args kwargs shows up when you use both in the same function. Think of a restaurant order: the table number is required, then any number of dishes, then named special instructions like “spice: low” or “no onion”. A required parameter first, then *args for the loose extras, then **kwargs for the named extras. A logging helper is the classic example: it needs a level, then any number of message pieces, then any number of named details.
📄 combined.py: the flexible function signature
def log(level, *messages, **metadata):
prefix = f"[{level.upper()}]"
msg = " ".join(str(m) for m in messages)
extras = " | ".join(f"{k}={v}" for k, v in metadata.items())
output = f"{prefix} {msg}"
if extras:
output += f" ({extras})"
print(output)
log("info", "Server started", port=8080, env="production")
log("error", "Connection failed", "retrying...", host="db.local")
log("debug", "Request received")
▶ Output
[INFO] Server started (port=8080 | env=production) [ERROR] Connection failed retrying... (host=db.local) [DEBUG] Request received
What happened here: the first positional value became level, every extra positional value piled into the messages tuple, and every named value piled into the metadata dictionary. One signature, three very different calls, and each one printed cleanly. That flexibility is exactly why wrappers and decorators reach for *args and **kwargs so often.
Keyword-Only Arguments
Some arguments are dangerous to pass by position because nobody can tell what they mean. A bare True sitting in a call could be anything. Think of a bank cheque: the amount only counts when it sits in the box labelled for it, not scribbled in a corner. A keyword-only argument works the same way, it forces the caller to spell out the name. You create one by putting a bare * in the signature: everything after it must be passed by name.
📄 keyword_only.py: force callers to name arguments
# Everything after * must be passed by keyword
def connect(host, port, *, timeout=30, ssl=True):
print(f"Connecting to {host}:{port} (timeout={timeout}, ssl={ssl})")
connect("db.local", 5432) # Works
connect("db.local", 5432, timeout=10, ssl=False) # Works
# This FAILS, timeout and ssl must be keyword arguments
try:
connect("db.local", 5432, 10, False)
except TypeError as e:
print(f"Error: {e}")
▶ Output
Connecting to db.local:5432 (timeout=30, ssl=True) Connecting to db.local:5432 (timeout=10, ssl=False) Error: connect() takes 2 positional arguments but 4 were given
What happened here: the bare * drew a line in the signature. Everything after it, timeout and ssl, must be passed by name. The first two calls work because they name those arguments. The last call tries to sneak 10 and False in by position, so Python refuses with a TypeError. This is exactly the bug you want to prevent: a call like connect("db.local", 5432, 10, False) where nobody can tell what 10 and False mean without opening the function definition.
Positional-Only Arguments
This is the mirror image of the last section. It works like a queue at a railway ticket counter: your spot in the line decides your turn, and nobody asks your name. A bare / in the signature marks every parameter before it as positional-only, which means the caller cannot pass them by name. Why would you want that? It lets you rename a parameter later without breaking anyone, because no caller is allowed to depend on the old name. Many built-in functions work this way, which is why len(obj) works but len(obj=mylist) does not.
📄 positional_only.py: parameters before / are positional-only (Python 3.8+)
def power(base, exp, /):
"""Both arguments must be positional."""
return base ** exp
print(power(2, 10)) # Works
try:
print(power(base=2, exp=10)) # Fails!
except TypeError as e:
print(f"Error: {e}")
▶ Output
1024 Error: power() got some positional-only arguments passed as keyword arguments: 'base, exp'
What happened here: power(2, 10) works because both values are passed by position. The moment we try power(base=2, exp=10), Python rejects it: the / made base and exp positional-only, so naming them is not allowed. You will not need this every day, but it is good to recognise the error message when a built-in or a library function throws it at you.
Python args kwargs: The Complete Parameter Order
Think of a train: engine first, passenger coaches in the middle, guard van at the end. The coupling order never changes. Python parameter types line up with the same discipline, and this one function shows all six slots at once.
📄 complete_signature.py: all parameter types in the required order
def everything(pos_only, /, regular, default="hi", *args, kw_only, **kwargs):
"""Shows all parameter types in their required order."""
print(f"pos_only: {pos_only}")
print(f"regular: {regular}")
print(f"default: {default}")
print(f"args: {args}")
print(f"kw_only: {kw_only}")
print(f"kwargs: {kwargs}")
everything(1, 2, 3, 4, 5, kw_only="must_name", extra="bonus")
▶ Output
pos_only: 1
regular: 2
default: 3
args: (4, 5)
kw_only: must_name
kwargs: {'extra': 'bonus'}
What happened here: this one function shows every parameter type lined up in the order Python insists on: positional-only (before /), regular, defaults, *args, keyword-only (after *args or a bare *), then **kwargs. The good news is you almost never need all six at once. Most real functions use a few regular parameters and maybe a default or two. Keep this example in your back pocket for the rare day you need the full stack.
The Don’t-Do-This Section
Every Python args kwargs guide owes you this warning, because it is the single most famous Python catch around arguments. A default value is built once, when the function is defined, not fresh on every call. If that default is a mutable object like a list, every call that relies on the default shares the same list. It is like a “shared notepad” stapled to the function: each call scribbles on the same page.
🚫 Mutable default argument: the classic trap
def append_to(item, target=[]):
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2], wait, WHAT?
print(append_to(3)) # [1, 2, 3], the list is SHARED
✅ Use None as the default and build the list inside
def append_to(item, target=None):
if target is None:
target = []
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [2], fresh list each time
What happened here: in the first version, the default list was created one time and reused, so each call kept appending to the same shared list. That is why the second call printed [1, 2] instead of [2]. The fix is a tiny ritual you will use again and again: make the default None, then build a fresh list inside the function. Now every call that relies on the default starts with a clean, empty list. The same trick applies to dictionaries and sets, any mutable default.
Common Mistakes
Mistake 1: Positional argument after keyword argument
🚫 SyntaxError
# greet(name="Rahul", 28) # SyntaxError: positional argument follows keyword argument
Once you name an argument, every argument after it must be named too. Python cannot work out where a stray positional value belongs, so it stops you at parse time. Put your positional arguments first, then your keyword ones.
Mistake 2: Using *args when you should use a list parameter
If the function always processes a collection of items, accept a list: def process(items: list). Use *args only when the caller should pass items as separate arguments, not wrapped in a list.
Best Practices
- DO use keyword arguments at call sites for functions with 3+ parameters
- DO use
*to force keyword-only arguments for boolean flags:def delete(id, *, force=False) - DO use
Noneas default for mutable types (lists, dicts, sets) - DON’T reach for
*argsand**kwargseverywhere, since explicit named parameters are easier to read and document - DON’T use mutable default arguments (
[],{}) - DON’T put required parameters after
*argsunless they’re keyword-only
Conclusion
Back to the pizza. Required size, a sensible default, and any number of extra toppings: that is the whole mental model for python args kwargs. Default values make parameters optional. Keyword arguments make a call read like plain English. *args and **kwargs give you room for the things you cannot predict. Keyword-only and positional-only parameters let you set firm rules about how callers use your function. The one fact worth memorising is the order Python demands: positional-only, regular, defaults, *args, keyword-only, **kwargs. Get that right and the rest falls into place.
Next up: Scope and the LEGB Rule, where you will see how Python decides which variable a name actually refers to. And if you want the full roadmap, from absolute basics all the way to AI/ML, browse the Python + AI/ML tutorial series home.
Practice Exercises
- Exercise 1: Write a function using
*argsreturning the average. - Exercise 2: Write a
**kwargsfunction printing key-value pairs. - Exercise 3: Build a logging function with message,
*args, and**kwargs.
Frequently Asked Questions
What is the difference between *args and **kwargs?
*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dictionary. Example: def f(*args, **kwargs) accepts both f(1, 2) (args=(1,2)) and f(a=1) (kwargs={‘a’: 1}). That pairing is the heart of Python args kwargs.
Can I use any name instead of args and kwargs?
Yes. The names args and kwargs are just conventions. What matters is the * (single asterisk) and ** (double asterisk). You could write *numbers or **options and it works the same way.
Why are mutable default arguments dangerous?
Python evaluates default values once at function definition time, not each call. A mutable default like [] is shared across all calls that use the default. Appending to it in one call affects all subsequent calls. Use None and create a fresh object inside the function.
What is a keyword-only argument in Python?
A parameter that must be passed by name, not position. Place it after * in the function signature: def f(a, *, flag=True). Calling f(1, False) fails; you must use f(1, flag=False). Great for boolean flags.
What order do parameters go in a Python function?
The required order is: positional-only (before /), regular parameters, parameters with defaults, *args, keyword-only (after *), **kwargs. Most functions only use a subset of these.
Interview Questions on Python Function Arguments
If you can walk through these without peeking, you are ready for this topic in an interview.
Q: What does the single asterisk do in a function call, like total(*nums)?
In a call, * unpacks. If nums is [1, 2, 3], then total(*nums) is the same as total(1, 2, 3): the list is spread into separate positional arguments. It is the mirror image of *args in a definition, which packs separate arguments into a tuple. The same mirroring applies to ** with dictionaries.
Q: You call greet("Aditi", name="Aditi") and Python raises TypeError: greet() got multiple values for argument 'name'. What went wrong?
The positional value "Aditi" already filled the first parameter, name. The keyword argument name="Aditi" then tried to fill the same parameter a second time, and Python refuses. The fix is to pass the value exactly once, either by position or by keyword, never both.
Q: You are writing a decorator that must wrap functions with completely different signatures. How do you define the wrapper?
Define it as def wrapper(*args, **kwargs) and call the wrapped function with func(*args, **kwargs). The wrapper packs whatever it receives, then unpacks it unchanged into the real function. This forwarding pattern is the single most common real-world use of *args and **kwargs.
Q: A teammate defines def report(*rows, sep) and every caller crashes with TypeError: report() missing 1 required keyword-only argument: 'sep'. Why does this happen, and what is the fix?
Any parameter declared after *rows is keyword-only, so a value passed by position just gets swallowed into rows and sep stays unfilled. Either give it a default, def report(*rows, sep=", "), or update every call site to pass it by name, like report(r1, r2, sep=" | ").
Q: Your team keeps shipping bugs from calls like delete_records(True, False, True) because nobody remembers what each boolean means. How do you redesign the signature?
Put a bare * after the required parameters so all the flags become keyword-only: def delete_records(table, *, cascade=False, dry_run=True, force=False). Old positional calls now fail loudly with a TypeError instead of silently doing the wrong thing, and every new call reads like documentation.
Q: Can *args and **kwargs receive zero arguments, and how would you check inside the function?
Yes. If the caller passes no extras, args is an empty tuple () and kwargs is an empty dict {}, not None and not an error. Check with a simple truthiness test like if not args:. This is exactly why print() with zero arguments still works.
Further reading: the official Python documentation is the authoritative source on this.
Related Posts
Previous: Python: Functions, def, Parameters, Return Values
Next: Python: Scope and the LEGB Rule (Local, Enclosing, Global)
Series Home: Python + AI/ML Tutorial Series

No comment