DEV Community

ak0047
ak0047

Posted on

Understanding the Asterisk (*) in Python Function Arguments

Introduction

Have you ever seen function arguments in Python with a * or ** and wondered what they mean?
I’ve definitely seen them — and to be honest, I used to just ignore them because they looked confusing.

Recently, I came across them again and decided it was finally time to understand what’s really going on.
Here’s what I learned about how and when to use * and ** in Python functions.


When to Use * and **

We use * and ** in function arguments when we want to reuse the same logic, but the number or names of arguments might differ.

In short, these allow us to define functions that don’t fix the number or names of arguments — making our code much more flexible.

Let’s go through the different ways to use them 👇


*args: Collecting Multiple Positional Arguments

You can use a single asterisk (*) to collect any number of positional arguments into a tuple.
That means the function can accept a variable number of arguments without breaking.

def greet(*args):
    for name in args:
        print(f"Hello, {name}!")

greet("Alice") 
# Hello, Alice!

greet("Alice", "Bob", "Charlie")
# Hello, Alice!
# Hello, Bob!
# Hello, Charlie!
Enter fullscreen mode Exit fullscreen mode

By convention, we name it args, but you can actually use any name.

This pattern is useful when you don’t know in advance how many arguments will be passed — for example, logging, combining results, or batch processing.

Note that any argument after *args must be passed as a keyword argument:

def sample(*args, x, y):
    print(args, x, y)

sample(1, 2, 3, x=4, y=5)  # ✅ OK
# sample(1, 2, 3, 4, 5)    # ❌ Error: x and y must be keyword arguments
Enter fullscreen mode Exit fullscreen mode

**kwargs: Collecting Keyword Arguments as a Dictionary

A double asterisk (**) allows a function to collect any number of keyword arguments into a dictionary.

def show_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

show_info(name="Alice", age=25, country="Japan")
# name: Alice
# age: 25
# country: Japan
Enter fullscreen mode Exit fullscreen mode

The name kwargs stands for keyword arguments, but again — any name works.

This is handy when you want to handle optional parameters or when a function may receive additional options in the future.

Keep in mind: **kwargs must appear last in the function definition.

# ✅ OK
def sample(a, b, **kwargs):
    print(a, b, kwargs)

# ❌ Invalid
# def sample(a, b, **kwargs, c):
#     print(a, b, kwargs, c)
Enter fullscreen mode Exit fullscreen mode

The Lone *: Forcing Keyword-Only Arguments

If you include a bare * in your function definition, all parameters after it must be passed as keyword arguments.

def move(x, y, *, speed=1.0, direction="forward"):
    ...

# ✅ OK
move(10, 20, speed=2.0, direction="backward")

# ❌ Error: speed and direction must be passed as keyword arguments
# move(10, 20, 2.0, "backward")
Enter fullscreen mode Exit fullscreen mode

This helps clearly separate main parameters from optional ones, making your function calls more readable and less error-prone.


Argument Unpacking with * and **

You can also use * and ** when calling a function, not just when defining one.
This technique is known as argument unpacking — it lets you “expand” a list, tuple, or dictionary into separate arguments.

Example 1: Unpacking a list or tuple

def add(a, b, c):
    return a + b + c

numbers = [1, 2, 3]
result = add(*numbers)  # same as add(1, 2, 3)
print(result)  # 6
Enter fullscreen mode Exit fullscreen mode

Example 2: Unpacking a dictionary

def show_profile(name, age, country):
    print(f"{name} ({age}) from {country}")

profile = {"name": "Alice", "age": 25, "country": "Japan"}
show_profile(**profile)
# same as show_profile(name="Alice", age=25, country="Japan")
Enter fullscreen mode Exit fullscreen mode

Practical Example: Shared Logic with Different Arguments

Here’s a simple but powerful use case — when multiple functions share the same workflow, but take different arguments.

def process_anything(func, *args, **kwargs):
    print("=== Start process ===")
    result = func(*args, **kwargs)
    print("=== End process ===")
    return result

def process_user(user_id, *, active=True, notify=False):
    print("Processing user:", user_id, active, notify)

def process_order(order_id, *, priority=False):
    print("Processing order:", order_id, priority)

process_anything(process_user, 123, notify=True)
# === Start process ===
# Processing user: 123 True True
# === End process ===

process_anything(process_order, 999, priority=True)
# === Start process ===
# Processing order: 999 True
# === End process ===
Enter fullscreen mode Exit fullscreen mode

Why this works

  • process_anything() can call any function, regardless of how many arguments it takes
  • Shared steps (logging, setup, teardown) live in one place
  • Great for decorators, wrappers, or utility functions

When to Use Each

Situation Syntax Why it helps
Variable number of arguments *args Flexibility
Collecting optional parameters **kwargs Easy to extend
Forcing keyword arguments def func(*, option=...) Safer and clearer
Passing a list/dict as arguments *obj / **obj Clean and concise
Reusing logic for different functions *args, **kwargs Improves reusability

Summary

  • *args: collects positional arguments
  • **kwargs: collects keyword arguments
  • * alone: makes following parameters keyword-only
  • Use * / ** when calling a function to unpack arguments
  • Perfect for “same logic, slightly different arguments” cases

Final Thoughts

I used to avoid * and ** because they looked intimidating — but after digging in, I realized how powerful and practical they are.
I wish I’d learned about them earlier; they would’ve made some of my past code so much cleaner.
From now on, I’m definitely going to use them more often.


💬 How about you?

  • Have you used *args or **kwargs in your own projects?
  • What’s a situation where they saved you from writing repetitive code?

Top comments (0)