Python Decorators ( args, kwargs) Oversimplified

abhinaya rajaram
Python in Plain English
8 min readApr 23, 2023

--

This article will help you understand the concept of decorators in Python programming and how best to use them.

Why use Decorators

Decorators: Decorators can help you avoid unnecessary repetitions across your codebase. You can change the way functions behave with very little refactoring effort.

How?: Repeated bits of code can be pulled together to become function decorators. This comes in handy while doing analytics and logging. For example, setting up validation and runtime checks, etc. Decorators make it easier to add additional functionality to a simple function, method, or class without having to alter its source code while keeping your code DRY.

Things you need to know before addressing the topic of decorators.

Basic Concept of Functions

Whenever you think of functions, think of them as objects. They can receive arguments or be passed as arguments.

  1. A function can also be assigned to a variable. You can call the function using the variable “time_travel()”.
def memory():
print("Going back memory lane")

time_travel = memory
time_travel()

# Output returned will be -->Going back memory lane

2. A function can be passed as an argument of another function.

def learn_python(func):
print("I will learn Python by knowing more about", end=' ')
func()

def topic_name():
print("Decorators")

learn_python(topic_name)

# Output returned will be--> I will learn Python by knowing more about Decorators

Python Decorator Basics

To understand the concept of decorators, think of the last time you received jewelry.

Did it come with a gift wrapper, a bow, the pink paper that holds the gift, and that shiny diamond ring you always wanted? Yep, hold that thought.

The pink paper can be compared to a wrapper function in Python. It is the innermost function since it holds the gift while the blue wrapping paper is the decorator and will always the outermost function. Finally, you have the gift i.e the function that is being decorated. See the below example.

What are we doing here → We are wrapping python1 (gift) with another function, which in this case is learn_something(i.e function decorator or outermost function).

2. How does it work: When learn_something() is called, it will return basic_nature(wrapper function or innermost function) back to the caller.

3. What is the decorator here learn_something is the decorator here. Please take note that the decorator is always the outermost function while basic_nature is the wrapper function.

def learn_something(f):    
def basic_nature():
return f()
return basic_nature

def python1():
return 'I want to learn python first'

Ways to call the Function :

Option 1 → you can assign the function to a variable and call the function by using the variable and add two brackets around it.

f = learn_something(python1) 
f()
#Output
'I want to learn python first'

Option 2 → Python allows you to prefix the function decorator with the “@” symbol and place it before the function that you want to wrap around.

So we want to wrap learn_something around python1, so we use a “@” symbol around learn_something and place it right above the gift i,e python1. Doing this will allow you to call the function python1 directly. See below.

def learn_something(f):    
def basic_nature():
return f()
return basic_nature
@learn_something

def python1():
return 'I want to learn python first'

python1()

Here is another example to showcase the flow of events to you:

  • function_decorator is the name of the decorator. Our blue wrapping paper.
  • wrapped_func is the name of the inner function, which is actually only used in this decorator definition.
  • func is the function that is being decorated. The actual gift.
def function_decorator(func):
def wrapped_func():
# Do something before the function is executed
func()
# Do something after the function has been executed
return wrapped_func
@function_decorator
def func():
pass

##################### Actual Example.########################
def function_decorator(func):
def wrapped_func():
print('Before test: I want a car if I pass the exam with flying colors')
func()
print('After test: Hmm, ok I can settle for a bicycle becaue I passed')
return wrapped_func
@function_decorator
def test():
print('I passed the exam but with average grades')

Every time you want to gift someone something, you have to make sure that the pink paper aka innermost function is big enough to handle the gift. Similarly, if the gift function here “test” takes “marks” as an argument, I will make sure our wrapped function aka pink paper has the ability to accommodate that. I will do that by adding args and kwargs. See sections below.

If you pass more arguments to a function than what was allotted or no arguments when its needed, you will end up getting the TypeError. To avoid this kind of error, you can use *args and **kwargs as an argument when you are unsure about the number of arguments to pass in the functions.

Function Decorators with Arguments

In the above example, python1 did not accept any arguments.

In other words, the function decorator (learn_something ) wraps around a function i.e. python1 that does not take any arguments. What if you now want to wrap it around a function that accepts arguments?

Let me illustrate this with an example. I am going to ask the user to pick two numbers between 1 to 3 and my script will take those numbers as input and bring in results from two lists based on the numbers entered. However, before bringing results, the script will first check whether the numbers are within the range.

colors = ['Green', 'Blue', 'Red']

response = [ 'is the color that will bring you luck', ' suits you astrologically',
' will do wonders for you']

def check_if_input_in_range(func):
global response
def consider_two_positons(num1, num2):
print(" This will pick values from the list colors & list response stored in position {} and {}".format(num1, num2))
if num1>= len(colors):
raise Exception("Value entered in colorsis out of the range for the function {} to work".format(func.__name__))
elif num2>= len(response):
raise Exception("Value entered in response is out of the range for the function {} to work".format(func.__name__))
else:
pass
return func(num1, num2)

return consider_two_positons

@check_if_input_in_range
def bring_results(num1, num2):
return colors[num1]+ response[num2]
print(bring_results(1, 1))

#Output
#This will pick values from the list colors & list response stored in position 1 and 1
#Blue suits you astrologically

The function “bring_results” does not change the name, therefore we can use it as is, but we will check for in-range input before displaying the response.

Another way to write this would be:

from functools import wraps

def check_if_input_in_range(threshold1, threshold2):
def wrap(func):
@wraps(func)
def func_wrapper(x, y):
if x >= threshold1 or y >= threshold2:
raise Exception(" x has to be less than {} and y has to be less than {} \
for function {} to work".format(threshold1,threshold2, func.__name__))
res = func(x, y)
return res
return func_wrapper
return wrap

@check_if_input_in_range(len(colors), len(response))
def bring_results(x, y):
return movies[x]+ response[y]

bring_results(1, 1)

Chaining Decorators

To chain decorators in Python, we can apply multiple decorators to a single function by placing them one after the other, with the inner decorator being applied first.


def double_decorator(func):
def increase_by_two():

number = func()

return number * 2

return increase_by_two

def small_decorator(func):
def decrease_by_one():

number = func()

return number + 0.56


return decrease_by_one


@double_decorator
@small_decorator



def get_number():
return 2

print(get_number())

Need for args & kwargs

Have you encountered this error before?

TypeError: adding() takes 3 positional arguments but 5 were given

If you pass more arguments to a function than what was allotted, you will end up getting the TypeError. To avoid this kind of error, you can use *args and **kwargs as an argument when you are unsure about the number of arguments to pass in the functions.

Introduction to *args and **kwargs in Python

In Python, we can pass a variable number of arguments to a function using special symbols. There are two special symbols:

  1. *args (Non-Keyword Arguments) →*args allows you to pass variable number of non-keyworded arguments upon which tuple-like operations can be performed.
  2. **kwargs (Keyword Arguments) →**kwargs allows you to pass a variable number of keyword arguments to a function upon which dictionary-like operations can be performed. This means that instead of the order in which they appear, the importance is in the label that each variable has.
  3. Similarity →*args and **kwargs are special keyword that allows the function to take a variable-length argument.
  4. Difference →**kwargs creates a dictionary whereas *args creates a tuple.
  5. By using **kwargs you can name your arguments which would not be possible in *args.
#Using args with a list
list = []
def pass_or_fail(*args):
for marks in args:
if marks < 40:
result = 'Fail'
elif marks >= 40:
result = 'Pass'
list.append(result)
return list

pass_or_fail(80,30,100)

#Output =['Pass', 'Fail', 'Pass']


def Tom_Check(*args):
print(['Yes' for n in args if n == 'Tom'])

Tom_Check('Jack','Tom', 'Jerry')

#Output ['Yes']

What you are seeing is that the function can take any number of arguments. When you use the * in front of a variable, it will convert any number of inputs into a tuple. You can access each argument according to its index. For example, you could get the first value by doing args[0], etc

def decorator_func(decorated_func):
def wrapper_func(*args, **kwargs):
print(f'there are {len(args)} positional arguments and {len(kwargs)} keyword arguments')
return decorated_func(*args, **kwargs)

return wrapper_func

@decorator_func
def names_and_age(year1, year2, name1='Joe', name2='Harry Potter'):
return f'{name1} was released in {year1} and {name2} was released in {year2}'

print(names_and_age(1997, 1986, name1="Titanic", name2="Top Gun"))

#Output -->there are 2 positional arguments and 2 keyword arguments
#Output -->Titanic was released in 1997 and Top Gun was released in 1986

The wrapper function above takes the argument as *args and **kwargs which means that a tuple of positional arguments or a dictionary of keyword arguments can be passed of any length. This makes it a general decorator that can decorate a function having any number of arguments.

Summing Up

Creating our own decorator is a very powerful concept in Python. In this post, we learnt how to implement a decorator in Python for use with the decorator syntax.

More content at PlainEnglish.io.

Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord.

--

--