Python Decorators, *args, and **kwargs Explained

By | December 14, 2017

Questions about decorators, *args, and **kwargs come up often, but don’t always have concise and comprehensive answers. Official documentation tends to explain things in terms of textbook definitions and proper syntax, with an example or two if you’re lucky. If you already know a programming language or two that may be fine, but a newcomer will want to know why we use these things along with some specific examples. I put together two demo scripts that embellishes some Stackexchange answers for this reason. Knowing these patterns will add to your programming maturity and help you start to understand the machinery of this high level language. Let’s begin with decorators.

Flask app

Flask app in 5 lines

Decorators

These are for when you need to alter the function of some existing object or class (functions are first class objects in Python). A decorator is accomplished in Python by placing an “@somefunction” line above the original function you wish to modify.

Here is an example decorator script. You might want to use this pattern if you have a function to call, but also want to ensure other things or done immediately before or after (and you don’t want to rewrite it with a slight change – DRY!). For example, many web apps will use a “@require_login” decorator to ensure users are logged in before directing them to a page. You can also use a decorator to benchmark your functions if you are concerned about performance.


def power_decorator(original_function):

		def wrapper(passed_in_input):
			print('--- in the wrapper function ---')
			print('loop on number', passed_in_input)
			return 2 ** original_function(passed_in_input)
		
		return wrapper # the power_decorator function requires a return to carry back the "wrapped up" print_number call
		
@power_decorator
def print_number(input_number):
	print('--- back in the original function ---')
	return input_number

def demo():
	print("example python decorator script.\n")
	print("loop through integers 0-8 and call the decorated function each time...\n")
	
	for num in range(0, 9):
		print("result is: %s" % str(print_number(num)))
		print('\n')
		
if __name__ == '__main__':
	demo()

This example was inspired by a Real Python post┬áthat has both simpler and more complex snippets. I changed the structure and variables to make it more clear what’s happening in the code every step of the way. Paste this example into your text editor or whatever environment you’re using and play around with it. Or just run it. You should get output like this:

wrapper demo output

*I took out the “about to return the result” print statement.

I think this example makes it more clear where the code flows step by step.

*args and **kwargs

So you might know by now that *args is used when you’re not sure how many parameters will be passed into the function you’re creating (many data science libraries make ample use of optional parameters since they need to satisfy so many conventions). And you might have even gleaned that **kwargs is used for named arguments in the form of a key:value dictionary mapping (and that it can be used when calling the function or when defining it). But how and why are they used? And how are they used alongside other arguments in such a way that the interpreter knows what goes where? Try running the script below and see the output, it speaks for itself.


# a collection of examples found on the internet

    # adapted from: https://stackoverflow.com/a/3394898
def print_everything(*args):
	for count, thing in enumerate(args):
		print('{0}. {1}'.format(count, thing))

	print('\n')

def table_things(**kwargs):
	for name, value in kwargs.items():
		print( '{0} = {1}'.format(name, value))

	print('\n')

# adapted from: https://www.saltycrane.com/blog/2008/01/how-to-use-args-and-kwargs-in-python/
def demo_func(formalarg, b='default string value', *args, key1, key2):
	for x in formalarg, b, args, key1, key2:
		print(x)

	print('\n')

def demo_func_2(formalarg, b='default string value', *args, **kwargs):
	for x in formalarg, b, args:
		print(x)

	for key in kwargs:
		print("key: %s, value: " % key, kwargs[key])

	print('\n')
		
def demo():
    print('demo functions that use *args and *kwargs' + '\n')
    # 1
    print_everything('all', 'the', 'things')

    # 2
    table_things(drink='red wine', entre='spaghetti', side=None)

    # 3
    test_dict = {"key1": "value1", "key2": "value2"}
    demo_func("this is a regular parameter/argument", 'b_override', '*args argument 1', '*args argument 2', **test_dict)

    # 4
    demo_func_2("this is a regular parameter/argument", 'b_override', '*args argument 1', '*args argument 2', hobby='Python',
                name='John Doe', bonustip='kwargs is a dictionary, and dictionaries are unordered. so dont rely on the order of your kwargs!')

    # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
    print('todo: extend this concept to classes and their subclasses')  #
    # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #

if __name__ == '__main__':
    demo()

You can see how each function processes the input arguments by simply running the .py file. In short, here’s what’s happening:

  1. *args is the only parameter, but three strings are passed in and able to be handled separately with enumerate (creating an integer for each string).
  2. We don’t pass in a dictionary, but rather a series of named values. Once inside the function, the variable **kwargs can be treated like a dictionary with .items() to access the key and value pairs.
  3. Ok now we have a mix of regular parameters with *args and **kwargs. The first two are nothing special here, I just put them there so it resembles a real function you might see. The first two arguments slot into the first two defined parameters. That leaves 2 unaccounted parameters with 3 arguments (‘*args argument 1’, ‘*args argument 2’, **test_dict) being passed in. **kwargs is used with the function call, so any parameter names that match the dictionary key will be evaluated as the value to the key. The remaining arguments go to *args since it can accept a variable length of inputs.
  4. The first two arguments are matched up with the first two slots in the function just like in the previous example, leaving just *args and **kwargs to be matched up next. **kwargs all have assigned values, so it takes any inputs with assignment (x=’some string’). That leaves any danglers to *args.

Notes:

  • Default arguments should come after the “regular” arguments or formal arguments, and be before *args and **kwargs.

Happy coding.

Update 7.23.18:

I’m adding another version of this demo decorator for my own sake and for the sake of perhaps explaining it a better way. This is more or less the same code, but the wrapper function returns a tuple so you can see the variables side by side. Finally, I call a semantically identical function without a decorator to show what the output would look like otherwise.

def power_decorator(original_function):

		def wrapper(passed_in_input):
			print('--- in the wrapper function ---')
			print('loop on number', passed_in_input)

			# assign a variable to the output of the original function, or the function that was decorated
			n = original_function(passed_in_input)
			
			# returns a tuple of the exponent, n, and the result of 2^n
			return (n, 2 ** n)
		
		return wrapper # the power_decorator function requires a return to carry back the "wrapped up" print_number call
		
@power_decorator
def print_number(input_number):
	print('--- back in the original function ---')
	return input_number

# no decorator here anymore
def print_number_plain(input_number):
	print('--- back in the original function ---')
	return input_number

def demo():
	print("example python decorator script.\n")
	print("loop through integers 0-8 and call the decorated function each time...		\n")
	
	for num in range(0, 9):
		print("result is: %s" % str(print_number(num)))
		print('\n')

	print("Now, without a decorator. Using the last iteration of the for loop (8).		\n")
	print(print_number_plain(num))
		
if __name__ == '__main__':
	demo()

Leave a Reply

Your email address will not be published.