Python Function Arguments โ Every Way to Pass Data In, Explained Properly
Positional, keyword, default, *args, **kwargs, positional-only, keyword-only, and argument unpacking โ the exact order rules Python enforces, and the specific TypeErrors you'll hit while learning them.
Last Updated
March 2026
Read Time
20 min
Level
Beginner
What Are Function Arguments in Python?
A function without inputs can only ever do one fixed thing. Arguments are what let the same function behave differently every time it's called โ send_email("priya@example.com", subject="Invoice") and send_email("rohan@example.com", subject="Reminder") run identical code with different data plugged in. Arguments are the entire reason functions are reusable instead of just named blocks that always do the exact same thing.
Here's where Python genuinely stands apart from most beginner-friendly languages: it gives you five distinct ways to pass arguments into a function โ positional, keyword, default-valued, arbitrary positional (*args), and arbitrary keyword (**kwargs) โ and they can all coexist in a single function signature, in a specific, non-negotiable order. That flexibility is genuinely powerful once it clicks. Before it clicks, it's the source of some of the most confusing tracebacks a beginner will encounter, mostly because the error messages assume you already know the vocabulary they're using.
I still remember staring at TypeError: create_user() got multiple values for argument 'role' for a solid ten minutes before realising the actual problem: I'd passed role both positionally and as a keyword argument in the same call, and Python had absolutely no idea which one I actually meant. The error message is accurate. It's just written assuming you already know exactly what 'multiple values for one argument' means mechanically โ which, the first time you see it, you don't.
This guide walks through every argument-passing style Python supports, the exact order rules the interpreter enforces, argument unpacking with * and ** at the call site, and โ because it trips up almost everyone eventually โ how Python actually passes arguments under the hood, which is neither strictly 'by value' nor strictly 'by reference' the way those terms work in C or Java.
History โ How Python's Argument-Passing Syntax Grew Over Time
Python's argument system today is the product of several distinct additions over more than three decades, each solving a specific real-world pain point that developers had already run into.
- โถ
1991 โ Python 0.9.0 โ Basic positional arguments existed from the very first release. Call a function, values get matched to parameters strictly by position, in order โ the simplest and oldest form of argument passing in the language.
- โถ
Early 1990s โ Default Arguments โ Default parameter values (def f(x=10):) were present very early on, letting callers omit arguments that have a sensible fallback, without needing separate overloaded function definitions the way C++ or Java would eventually require.
- โถ
Early 1990s โ Keyword Arguments โ The ability to call a function by naming parameters explicitly (greet(name='Priya')) rather than relying purely on position was also part of Python's earliest design โ a genuinely progressive feature for its era, when most mainstream languages offered only positional calling.
- โถ
Early 1990s โ *args and **kwargs โ Variable-length argument collection using * and ** prefixes appeared early too, allowing functions like print() itself to accept any number of arguments without a fixed signature.
- โถ
2008 โ Python 3.0 (PEP 3102) โ Introduced keyword-only arguments โ parameters that must be passed by name, enforced using a bare * in the function signature. This gave library authors a way to force clarity at call sites for parameters where positional calling would be error-prone or ambiguous.
- โถ
2018 โ Python 3.8 (PEP 570) โ Introduced positional-only parameters, marked using a / in the signature. This let standard library functions (many of which had already behaved this way informally in C implementations) formally declare that certain parameters cannot be passed by keyword โ useful for letting a library rename an internal parameter later without breaking anyone who called it positionally.
- โถ
2026 โ Current state โ All five styles โ positional, keyword, default, *args, **kwargs โ plus positional-only and keyword-only markers, now coexist in one unified, precisely ordered grammar. It's more rules to learn upfront than most beginner tutorials admit, but each one solves a genuinely distinct real problem, not academic completeness for its own sake.
Key Characteristics of Python Function Arguments
Here are the 11 things worth knowing precisely about how Python handles arguments โ not the vague version, the exact one:
Arguments passed without a name are matched to parameters strictly left to right, in the order the function defines them. greet('Priya', 25) binds 'Priya' to the first parameter and 25 to the second, regardless of their names.
Arguments passed as name=value are matched by that exact name, regardless of position in the call. greet(age=25, name='Priya') works identically to greet('Priya', 25), because both keyword arguments are matched by name, not position.
A default value is only used when the caller omits that argument entirely. Passing any value at all for that parameter โ even an 'empty' one like None โ always overrides the default; Python never silently falls back to a default just because a value looks unusual.
Any extra unnamed arguments beyond the explicitly listed parameters get gathered into a tuple accessible inside the function under whatever name follows the asterisk โ conventionally args, though any valid name works.
Any extra named arguments not matching an explicit parameter get gathered into a dictionary, with argument names as keys. Conventionally called kwargs, though again the name itself is just a convention, not a requirement.
A function signature must follow this exact order: positional-only, then standard positional-or-keyword, then default-valued, then *args, then keyword-only, then **kwargs. Violating this order raises SyntaxError immediately, before the function is ever called.
Supplying a value for the same parameter both positionally and by keyword in one call raises TypeError: got multiple values for argument, because Python genuinely cannot tell which value you intended.
A single asterisk before a list or tuple (*my_list) unpacks it into separate positional arguments at the call site. A double asterisk before a dictionary (**my_dict) unpacks it into separate keyword arguments.
Parameters listed before a bare / in the signature can only be passed positionally โ attempting to pass them by keyword raises TypeError, even if the name matches exactly.
Parameters listed after a bare * (or after *args) in the signature must be passed by keyword โ attempting to pass them positionally raises TypeError, regardless of correct ordering.
Python doesn't copy argument values into the function; it passes a reference to the same object. Mutating a mutable argument (like appending to a list) inside a function affects the original object the caller still holds โ reassigning the parameter to a new object does not.
How Python Matches Arguments to Parameters โ Flowchart
Every single function call goes through the same internal matching process before the function body even starts running โ positional arguments get slotted in first, keyword arguments get matched by name, defaults fill whatever's left unfilled, and anything extra gets swept into *args or **kwargs. Here's that exact sequence.
Code Execution Flow โ from source to output
Key insight: this entire binding process happens before a single line inside the function body runs. If it fails at any decision point, the function never executes at all โ you get a clean traceback pointing at the call site, not a partially-run function with confusing half-completed side effects.
How Arguments Actually Work โ Order Rules, Unpacking, and Reference Semantics
Three things separate someone who's memorised *args and **kwargs as magic syntax from someone who actually understands argument passing: the strict ordering rule for signatures, how unpacking works at the call site (not just the definition), and what 'passed by reference' really means in Python โ because it is genuinely not the same as either C's pass-by-value or Java's pass-by-reference, despite people casually using both terms.
๐ข The Non-Negotiable Signature Order
A full Python function signature, using every available feature, follows exactly this order: def f(pos_only, /, standard, default=1, *args, kw_only, **kwargs):. Positional-only parameters come first, separated by a bare /. Then standard positional-or-keyword parameters. Then defaulted parameters. Then *args for extra positional values. Then keyword-only parameters, which can appear either after a bare * or right after *args itself. Finally **kwargs, always last. Writing a parameter with a default before one without a default โ def f(a=1, b): โ raises SyntaxError: parameter without a default follows parameter with a default immediately, because Python can't figure out what should happen if a caller supplies only one positional value.
๐ Unpacking at the Call Site โ Where * and ** Actually Shine
The * and ** prefixes aren't only for collecting arguments inside a definition โ they also work in reverse, at the call site, to spread out an existing collection. If you have coordinates = (10, 20) and a function def move(x, y):, calling move(*coordinates) unpacks the tuple into two separate positional arguments โ exactly equivalent to move(10, 20). Similarly, if user_data = {'name': 'Priya', 'age': 25} and a function def create_user(name, age):, calling create_user(**user_data) unpacks the dictionary into keyword arguments โ exactly equivalent to create_user(name='Priya', age=25). This pattern shows up constantly when forwarding arguments between wrapper functions and the functions they wrap, which is exactly how most decorators are implemented internally.
๐ Pass-by-Object-Reference โ Neither Value Nor Reference, Exactly
This confuses developers coming from both C-style and Java-style backgrounds, because Python's actual behaviour is its own thing. When you pass an argument, Python passes a reference to the same underlying object โ no copy is made. If that object is mutable (a list, dict, or set) and the function modifies it in place โ my_list.append(4) โ the caller's original object changes too, because there's only ever been one object the whole time. But if the function reassigns the parameter name to a brand new object โ my_list = [1, 2, 3] โ that reassignment only affects the local name inside the function; the caller's original variable still points at whatever it pointed at before. Immutable objects (strings, integers, tuples) behave as if passed by value in practice, simply because you can't mutate them in place at all โ any 'change' to a string parameter is actually creating a new string object and rebinding the local name to it.
Simple rule to remember: positional binds by order, keyword binds by name, defaults fill gaps, *args/**kwargs sweep up the leftovers, and mutating a mutable argument in place affects the caller โ reassigning it does not.
Positional vs Keyword vs Default vs *args vs **kwargs โ Key Differences
Each argument style solves a genuinely different problem. This table lines up exactly what each one does, how it's written, and when to reach for it.
Passing Arguments โ Python vs Other Languages
Python's argument flexibility is genuinely unusual compared to statically typed languages, and the contrast explains a lot about why Python APIs tend to look different from Java or C++ ones.
Here's my honestly opinionated take: Python's native keyword arguments are one of its most underrated design decisions, and I genuinely miss them whenever I write Java. A call like create_report(start_date='2026-01-01', end_date='2026-03-31', include_charts=True) is self-documenting at the call site โ you don't need to open the function definition to know what each value means. Java developers simulate this with builder patterns or wrapper config objects, which works, but it's noticeably more ceremony to get the same clarity Python gives you for free with plain keyword syntax.
Advantages and Disadvantages of Python's Argument System
This much flexibility is a genuine strength, but it comes with real trade-offs, especially around clarity and API design discipline.
Argument Binding Architecture โ From Call Site to Local Scope
The diagram below shows exactly where each argument style lives in the pipeline between writing a function call and that function's body actually starting to run.
Every Argument Style in One File โ Plus the Errors They Produce
Here's a function using every style together, followed by the two most common argument-related errors and exactly what triggers them.
def build_profile(user_id, /, name, role="member", *tags, verified=False, **extra_info):
print(f"ID: {user_id} (positional-only)")
print(f"Name: {name}")
print(f"Role: {role}")
print(f"Tags: {tags}")
print(f"Verified: {verified} (keyword-only)")
print(f"Extra info: {extra_info}")
build_profile(101, "Ananya", "admin", "beta-tester", "early-access", verified=True, city="Pune")Output
ID: 101 (positional-only) Name: Ananya Role: admin Tags: ('beta-tester', 'early-access') Verified: True (keyword-only) Extra info: {'city': 'Pune'}Now the two errors that trip up almost everyone at some point โ reproduced exactly:
def create_user(name, role="user"):
print(f"{name} created with role {role}")
# Error 1: same argument supplied twice
create_user("Rohan", "admin", role="editor")
# Error 2: a required argument never supplied
# create_user(role="admin")Output
Traceback (most recent call last): File "argument_errors.py", line 5, in <module> create_user("Rohan", "admin", role="editor") TypeError: create_user() takes from 1 to 2 positional arguments but 3 were givenThat specific error happens because "admin" was already passed positionally for role, and then role="editor" tried to bind it again by keyword โ but since there are only 2 total parameters and 3 positional-looking values were supplied, Python actually reports the positional-count mismatch first. Removing the extra positional value and calling create_user("Rohan", role="editor") instead resolves it cleanly. The commented-out second case โ calling with only role="admin" and no name โ would raise TypeError: create_user() missing 1 required positional argument: 'name', since name has no default and nothing was ever supplied for it.
Practice This Code โ Live Editor
Line-by-Line Explanation
- โถ
user_id, /,โ Everything before the/is positional-only. Callingbuild_profile(user_id=101, ...)would raiseTypeError: build_profile() got some positional-only arguments passed as keyword arguments: 'user_id'. - โถ
name, role="member"โ Standard positional-or-keyword parameters, withroledefaulting to"member"if omitted. - โถ
*tagsโ Collects any additional positional arguments beyondnameandroleinto a tuple โ here,('beta-tester', 'early-access'). - โถ
verified=Falseafter*tagsโ This makesverifiedkeyword-only. It cannot be supplied positionally, only asverified=Trueor similar. - โถ
**extra_infoโ Collects any remaining keyword arguments not matching a named parameter โ here, justcity="Pune"โ into a dictionary.
Where Argument Flexibility Actually Matters โ Real-World Applications
This isn't abstract syntax trivia โ Python's argument system shapes how real, widely-used APIs are designed:
- โถ
๐ Web Framework Configuration โ Django model fields and view functions lean heavily on keyword arguments and defaults โ CharField(max_length=100, blank=True, null=False) reads clearly precisely because every value is named at the call site.
- โถ
๐จ Decorators and Function Wrapping โ Every decorator that needs to work on functions with unknown signatures relies on *args and **kwargs to transparently forward whatever arguments the wrapped function actually needs, without knowing them in advance.
- โถ
๐ Data Science Library APIs โ pandas.read_csv() accepts dozens of optional keyword arguments (sep, header, dtype, na_values) with sensible defaults โ exactly the kind of API that would be unworkable without native default and keyword argument support.
- โถ
๐ง Configuration and Settings Objects โ Functions that accept **kwargs as a flexible settings bag let calling code pass only the options it cares about, letting the function apply sensible defaults for everything else.
- โถ
๐งช Test Fixtures and Parametrised Tests โ pytest fixtures and parametrised test functions frequently use default arguments and keyword-only parameters to keep test setup explicit and readable, avoiding ambiguous positional test data.
- โถ
๐ผ๏ธ GUI and Plotting Libraries โ matplotlib.pyplot.plot() accepts a long tail of optional style-related keyword arguments (color, linewidth, linestyle) โ exactly the pattern **kwargs and defaults are built for.
- โถ
๐ API Client Wrappers โ Functions wrapping third-party API calls often forward **kwargs directly into an underlying HTTP request library, letting callers pass through headers, timeout, or auth options without the wrapper needing to know every possible option in advance.
- โถ
๐งฎ Mathematical/Statistical Utility Functions โ Functions like a custom sum_all(*numbers) or average(*values) use *args to accept any number of numeric inputs without forcing callers to build a list first.
Why This Deserves Its Own Deep Dive, Not Just a Side Note
Most tutorials cover arguments as a quick aside inside a broader 'functions' lesson. Here's why that undersells the topic:
- โถ
๐ It's Where a Huge Share of TypeErrors Come From โ missing required positional argument, got multiple values for argument, and unexpected keyword argument are three of the most common tracebacks in real Python codebases, and all three are purely argument-binding issues, not logic bugs.
- โถ
๐ผ API Design Interviews Test This Directly โ Being asked to design a function signature that accepts flexible configuration, or to explain why a mutable default argument is dangerous, is a genuinely common mid-level interview question.
- โถ
๐ง It Explains Library Code You Read Daily โ Once *args, **kwargs, and keyword-only parameters actually make sense, reading the source of libraries like requests or pandas stops feeling like decoding a puzzle.
- โถ
๐ง It's Foundational for Writing Decorators โ Every non-trivial decorator needs to forward arbitrary arguments to the function it wraps, which is impossible to do cleanly without a solid grasp of *args and **kwargs.
- โถ
๐ It Clarifies a Genuinely Common Point of Confusion โ The 'pass by reference vs pass by value' confusion is one of the most frequently mis-explained topics online. Getting it right here prevents a category of bugs involving unexpectedly mutated lists and dictionaries.
- โถ
๐ฎ๐ณ It's a Standard Deep-Dive Topic in Indian Python Courses โ Beyond the basic def introduction, Indian engineering curricula and interview-prep bootcamps typically dedicate a full separate session to *args/**kwargs and argument order โ exactly the scope of this guide.
Argument Unpacking โ * and ** at the Call Site, Explained Further
Unpacking deserves its own close look, because the same symbols (* and **) mean something different depending on whether they appear in a function definition or at a call site โ a distinction that confuses a lot of learners initially.
- โถ
In a function definition โ *args and **kwargs COLLECT extra arguments into a tuple and dictionary respectively. This is the 'gathering' direction.
- โถ
At a function call โ *some_list and **some_dict SPREAD an existing collection out into separate individual arguments. This is the 'scattering' direction โ the exact opposite operation, using identical symbols.
- โถ
Combining both directions โ A very common real pattern: def wrapper(*args, **kwargs): return original_function(*args, **kwargs). Here the wrapper collects whatever it's called with, then immediately spreads those same values back out to call another function โ this exact line is the backbone of most decorator implementations.
- โถ
Unpacking multiple collections in one call โ Python allows combining several unpacked collections in a single call: combined = [*list_a, *list_b] merges two lists, and merged = {**dict_a, **dict_b} merges two dictionaries, with keys from dict_b overriding matching keys from dict_a if there's a conflict.
Python Function Arguments โ Interview Questions
These come up constantly in Python interviews, from fresher screening rounds to mid-level backend interviews focused on API design.
Practice Questions โ Test Your Understanding
Work through these before checking the answers. Predicting exact output and exact error text is the real test here.
1. What is the output of: def f(*args): return sum(args) \n print(f(1, 2, 3, 4))
Easy2. What is the output of: def f(**kwargs): return kwargs \n print(f(a=1, b=2))
Easy3. What happens when you run: def f(a, b, /): return a + b \n print(f(a=1, b=2))?
Medium4. Trace this: def f(a, b=5, *args): return (a, b, args) \n print(f(1, 2, 3, 4))
Medium5. What does this print? def f(a, *, b): return a + b \n print(f(1, b=2))
Medium6. What does this print, and why? def modify(lst): lst.append(4) \n numbers = [1, 2, 3] \n modify(numbers) \n print(numbers)
Medium7. What does this print, and why does it differ from the previous question? def reassign(lst): lst = [9, 9, 9] \n numbers = [1, 2, 3] \n reassign(numbers) \n print(numbers)
Hard8. What is the exact error if you call def f(a, b, c): pass as f(1, 2, 3, 4)?
EasyConclusion โ Master the Order, and the Errors Stop Being Scary
Every one of the confusing tracebacks covered in this guide โ missing required positional argument, got multiple values for argument, got an unexpected keyword argument โ traces back to the exact same underlying mechanism: Python trying, and failing, to bind the values you supplied to the parameters your function actually declared. Once that binding process is genuinely clear in your head, these errors stop being cryptic and start being precise, useful diagnostics that point you straight at the fix.
If you're a complete beginner, the priority is getting comfortable with plain positional and keyword arguments first โ that alone covers the overwhelming majority of function calls you'll write. If you're moving into writing reusable libraries or decorators, the priority shifts toward *args, **kwargs, and unpacking, since forwarding arguments cleanly between functions is where these tools genuinely earn their complexity.
The habit worth carrying forward: whenever a function signature starts accumulating more than three or four parameters, especially a mix of optional ones, reach for keyword arguments at the call site even where positional would technically work. It costs a few extra characters and buys real clarity for whoever reads that call six months from now โ quite possibly you.
Arguments are the actual interface of every function you write. Get the order rules solid, understand what unpacking does in both directions, and know exactly when mutation quietly leaks back to the caller โ everything else about calling functions confidently follows from that. ๐