python decorators

Enabling Code Reuse with Decorators – Part 1

In this new post, we are going to talk about Python decorators and how to write custom decorators for your python apps.

Starting today, I am creating a series of in-depth posts on decorators,  my first post would be a short introduction to decorators, we would understand how decorators work in Python and learn how to implement decorators that apply to functions.

i. Introduction to decorators and decorating python functions (this article).
ii.Writing class decorators and decorating class methods.
iii. Decorators that take arguments.


Before we dive into decorators, let us refresh our knowledge of nested functions and closures.

  • What are nested functions?
  • What are  non-local variables?
  • What are closures?


Nested Function

A nested function is simply a function within another function.
To define a nested function, just initialize another function within a function using def.


A non-local variable is a variable that works inside nested functions, here the variable does not belong to the inner function but the outer function.

def outerFunction(num):
    def innerFunction():
        return num + 2
    return innerFunction()


In the above code, the num is the local variable.
The innerFunction is only available and nested within the outerFunction, if you try to call the innerFunction alone you get the error displayed below innerFunction()

NameError: name 'innerFunction' is not defined


When a function is nested inside another function then closure is created.
A closure simply causes the inner function to remember the state of its environment when called which means the inner function will have access to variables and parameters of outer function even after the outer function is returned.

Important Points to Remember about Closures

  • There should be a nested function i.e. function inside a function.
  • The inner function must refer to a non-local variable or the local variable of the outer function.
  • The outer function must return the inner function.
def outer(name,email):
    def inner():
        #using non-local variable email and name
        return f"Hello! {name}, your email address is {email}"
    #return inner function
    return inner 

func = outer('Mac','')
'Hello! Mac, your email address is'


A decorator is a function that accepts a function as input and returns a new function as output.

If we had a function called hello and a decorator called timeit, then the following would decorate hello with timeit.

x = timeit(hello)

Python has a special syntax on how to decorate a function using the @ operator, so the code above could be written like this

def hello():

The decorator @timeit simply receives a function and returns usually a different function.

def timeit(python_function):
    return python_function

THAT ASIDE, Python has inbuilt decorators such as @staticmethod, @classmethod, and @property that works in the same way.

class A:
    def class_method(cls):
    def static_method():

I recently wrote an article on decoding the python @staticmethod and @classmethod, you can check it out here.


It is very important to preserve the properties of the original function or function metadata while writing decorators and this could be achieved by applying the @wraps decorator from the functools library.

We are going to design 2 decorators.

  • A @timeit decorator that mimicks %timeit(ipython magic function), it would measure the time a function takes to execute and prints the duration.
  • @validate_data decorator that validates the user input(it checks if the mobile number is a valid Nigerian phone number) and raises an error if the input exceeds the required length.
LET’S get  started

1. @timeit decorator

import time 
from functools import wraps

def timeit(function_to_be_decorated):
    """A decorator that reports execution time """
    def wrapper(*args,**kwargs):
        start = time.time()
        function_result = function_to_be_decorated(*args, **kwargs)
        end = time.time()
        time_spent = end - start
        print(f"time spent executing this code {time_spent}")
        return function_result
    return wrapper

def calculate_interest(principal, time, rate):
    """simple code to calculate simple interest"""
    SI = principal *  time *rate
    return SI

calculate_interest(50006906, 12, 0.006)
time spent executing this code 4.5299530029296875e-06


Explaining the code above.

The code inside a decorator typically involves creating a new function that accepts any arguments using *args and **kwargs, as shown with the wrapper() function above.
Inside this function, you place a call to the original input function and return its result. However, we also placed our extra code timing.

start = time.time() 
end = time.time() 
time_spent = end - start

The newly created function wrapper is returned as a result and takes the place of the original function.
The use of *args and **kwargs is there to make sure that any input arguments can be accepted.
The use of the decorator @wraps(function_to_be_decoratedin the solution is important because it preserves

  • The original function properties such as name, docs, and annotations.
simple code to calculate simple interest
  • Makes the wrapped function available to you in the __wrapped__ attribute.
    #you could access the wrapped function directly by doing this:
    print(calculate_interest.__wrapped__(50006906, 12, 0.006))



2. @validate_data  decorator

def validate_data(func):
    def wrapper(*args, **kwargs): 
        data = func(*args, **kwargs) 
        if len(data) > 13:
            print("invalid phone number")
            return data
    return wrapper

def input_mobile_number(state_code, mobile_no):
    if state_code == "NIG":
        mobile_no = mobile_no[1:]
        phone_no = "+234" + mobile_no
        return phone_no

invalid phone number


Importance of decorators

The use of decorators are much but the most useful cases are:

  • Enforcing access control and authentication, e.g. flask @login_required decorator.
from flask import Flask, g, request, redirect, url_for
import functools
app = Flask(__name__)

def login_required(func):
    """Make sure user is logged in before proceeding"""
    def wrapper_login_required(*args, **kwargs):
        if g.user is None:
            return redirect(url_for("login", next=request.url))
        return func(*args, **kwargs)
    return wrapper_login_required

def edit_blogpost():
  • Logging
  • Caching and Memoization.
  • Data Validation and Runtime checks.
  • Code Reuse (DRY principle).


In my next post, we look at creating class decorators and decorating class functions.

Thanks for reading, please share with your network, if you found this post helpful, comment, and share your thoughts in the comment section.

Don’t forget to subscribe to my newsletter.

Further Reading





Leave a Reply

Your email address will not be published. Required fields are marked *