Intro

Python as a nifty trick for sending a variable number of arguments to a function and it comes in two flavors, *args and **kwargs.
I always get confused when thinking about this off the top of my head and end up googling an explanation, so here’s a simple explanation.

Example:

def pizza_order(*args, **kwargs):
	# logic for making order here 
	
	
pizza_order("Abdu", "margherita")
pizza_order("Abdu", "funghi", truffle_oil=False) #
pizza_order("Abdu", "funghi", "margherita", "smirna") # 
 

Explanation

If we took a regular function that only has two parameters, then that function only expects two arguments when called, no more and no less.
For the previous example that’d be customer_name and order, passing a third argument will throw a TypeError. // This function has no option/default parameters in its signature.
Ex:

def pizza_order(customer_name, order):
	# logic here
	print(f"{customer_name}'s order is a {order}")

Adding optional parameters obviously doesn’t scale well, so that’s where *args comes into play.

*args

This tells Python that a function accepts an arbitrary number of positional arguments after the other parameters are provided.
So for our example, the function would only accept positional args passed after the customer_name and order.

It does this by collecting the positional arguments into a tuple called args that can be unpacked like any other tuple.

def pizza_order(customer_name, order, *args):
	# logic here
	print(f"{customer_name}'s order is a {order} with following toppings: {args}")
 
pizza_order("Abdu", "funghi", "black olives", "hot honey", "olive oil")

The * in *args is what informs Python to pack the extra positional arguments into a variable named args. This variable can be named anything you like, but it’s convention, and likely pythonic, to name it args.

What about extra arguments that we want to assign to specific keywords? For that we need **kwargs.

**kwargs

The ** here tells python to pack all the keyword arguments that come after any positional arguments into a dictionary.
The keys for the dictionary are the ones in the function call when passing the arguments.

For example:

def pizza_order(customer_name, order, **kwargs):
	# logic here
	print(f"{customer_name}'s order is a {order} with following options:")
	for k, v in kwargs:
		print(f"- {k}: {v}")
 
pizza_order("Abdu", "funghi", cheese="quattro formagge",olives=False, hot_honey=True, extra_evo=True)

Together

When using both *args and **kwargs together, the latter must come last.

def pizza_order(customer_name, order, *args, **kwargs):
	# logic here

Always pass positional arguments before keyword arguments, and they must come after the static arguments. // This also applies to the function's definition as well.


References