Why I Regret Using *args and **kwargs in Python — And What You Should Do Instead

They look clean and powerful, but *args and **kwargs can make your code harder to understand, test, and maintain. Here’s what I learned…

Why I Regret Using *args and **kwargs in Python — And What You Should Do Instead
Photo by Osama Madlom on Unsplash

I Thought I Was Being Clever — Turns Out, I Was Just Being Lazy.

Why I Regret Using *args and **kwargs in Python — And What You Should Do Instead

They look clean and powerful, but *args and **kwargs can make your code harder to understand, test, and maintain. Here’s what I learned the hard way—and the better alternatives you should use.

When I first discovered *args and **kwargs, I felt like I’d unlocked a secret cheat code in Python.

“Look at me!” I thought. “My functions can take anything. I’m writing flexible, reusable code like a pro!”

But fast forward a couple of years — and a few late-night debugging sessions — and I’ve come to regret overusing this pattern. What once felt like elegance now looks like a trap I fell into.

Let me explain why I no longer reach for *args and **kwargs by default—and what I do instead.


Why We Fall in Love With *args and **kwargs

Before I bash them, let’s be fair. *args and **kwargs are not bad—they’re just often used wrong.

They let you:

Pass variable numbers of arguments to a function.
Write highly flexible and DRY code.
Delegate parameters to other functions (e.g., decorators or wrappers).

For example:

def log_event(event_name, *args, **kwargs): 
    print(f"{event_name}: {args} | {kwargs}")

It’s tempting, right? One function that handles it all.

But here’s the catch…

The Hidden Costs of Overusing *args and **kwargs

What looks like elegant abstraction can quickly spiral into chaos. Here’s what I learned the hard way:

1. They Kill Readability

You lose the biggest advantage Python offers: explicit, readable code.

Compare:

def create_user(name, age, is_active):

with:

def create_user(*args, **kwargs):

Now no one (not even you in 3 months) knows what this function expects without diving deep into the internals.

2. Debugging Becomes a Nightmare

With **kwargs, it's often unclear:

Which arguments are expected
Which ones are ignored
Whether you’re missing a required field or passing a typo

Errors are no longer caught by the function signature — they show up deep in your logic, or worse, silently fail.

3. You Sacrifice IDE and Linter Support

Modern IDEs (like PyCharm or VSCode) can’t suggest or autocomplete argument names hidden inside **kwargs. Static checkers like mypy struggle too.

In contrast, explicitly declared arguments give you:

  • Type hints
  • Autocompletion
  • Refactor safety

Why throw all that away?

4. You Accidentally Introduce Bugs

I once had this function:

def send_email(*args, **kwargs): 
    # send email logic

Then I passed it a dict like this:

params = {'to': 'user@example.com', 'subject': 'Hi'} 
send_email(**params)

It silently ignored a missing 'body' field I forgot to include—because nothing enforced it. The email went out... blank.

Yikes.

What to Do Instead: Explicit is Better Than Implicit

Python’s Zen says it best:

“Explicit is better than implicit.”

Here’s what I recommend now:

1. Use Named Arguments

Define your expected arguments explicitly, even if there are many.

def create_user(name: str, age: int, is_active: bool = True):

This helps both humans and machines understand what’s going on.

2. Use Data Classes or Pydantic Models for Complex Input

If you’re passing a lot of related data, encapsulate it.

from dataclasses import dataclass 
 
@dataclass 
class UserData: 
    name: str 
    age: int 
    is_active: bool = True 
 
def create_user(user: UserData):

This pattern improves structure, validation, and type checking — all in one shot.

3. Only Use *args and **kwargs in Specific Patterns

Some valid use-cases include:

Writing decorators
Wrapping or forwarding calls (e.g., in wrapper functions)
Frameworks where parameters are dynamic and not known in advance

Even then, document your expected kwargs clearly, like:

def handle_request(**kwargs): 
    """ 
    Expected kwargs: 
        - method: str 
        - url: str 
        - headers: dict 
    """

Takeaway: Don’t Trade Clarity for Convenience

The truth is, I used *args and **kwargs not for power—but for laziness.

I didn’t want to define parameters. I didn’t want to think through what my function actually needed. So I punted that decision to runtime.

It made my code more brittle, harder to debug, and worse for collaboration.

Now, I write clearer, more predictable code — and my future self (and teammates) thank me for it.

So, Should You Never Use *args and **kwargs?

No. But treat them like salt in cooking:

  • A little can bring out flavor
  • Too much ruins the dish

Reach for them when it makes sense, not just because you can.

Photo by Blake Guffin on Unsplash