DEV Community

Cover image for What Are Python Decorators Anyway?
Mark Harless
Mark Harless

Posted on

What Are Python Decorators Anyway?

For the past week-or-so, I've been teaching myself Python and Flask. I've been all over the place with what I want to really dig deep into and, I hope, I'm not the only beginner who's been making this mistake.

Lately, I've found Python decorators to be a bit confusing. A lot of the YouTube explanations I found tended to be very convoluted and add an extra layer of complexity to something that shouldn't be that complicated. So I'm dedicating this blog post to explain what these decorators do in the simplest way. Minimum effort!

Python functions, and in many other programming languages, are objects. This means they can be passed around and used in your program. One of the places it can be passed to are arguments to other functions.

Consider this block of code:

def divide(a, b):
  return a / b

divide(10, 2)
# 5.0
Enter fullscreen mode Exit fullscreen mode

Above we've created a super simple function that accepts two arguments and divides them and returns that value. But what if a user ran divide(10, 0)? We know it would error out because you can't divide by zero. So let's fix that:

def divide(a, b):
  if b == 0:
    return 'You cannot divide by zero' 
  return a / b

divide(10, 0)
# You cannot divide by zero
Enter fullscreen mode Exit fullscreen mode

If the second number is equal to 0, we will hit our return statement of You cannot divide by zero and the function will end there. This 100% works but what if, for some reason or another, you didn't want to add that if statement to your function? I'm sure there are several reasons in which I'm drawing blanks on but... wow what if? Well, this is where decorators come into play.

Python decorators allow us to extend the functionality of our functions without altering the function itself. Let's create our first decorator:

def zero_checker(function):
  def inner(a, b):
      if b == 0:
          return 'You cannot divide by zero'
      return function(a, b)
  return inner

def divide(a, b):
  return a / b
Enter fullscreen mode Exit fullscreen mode

In the code above, the function zero_checker, also our decorator, accepts another function as an argument. In the next line of code, we're creating another function called inner. The name isn't important but you'll sometimes see developers naming it "wrapper". Inside that function is where we apply our zero checker logic. Remember, once a function reaches its first return statement, Python escapes the function. This means only one return statement can be executed per function invocation.

Now, this code doesn't do anything special because we have not connected the two functions. Consider this code:

def zero_checker(function):
  def inner(a, b):
      if b == 0:
          return 'You cannot divide by zero'
      return function(a, b)
  return inner

def divide(a, b):
  return a / b

divide = zero_checker(divide)

print(divide(10, 0))
Enter fullscreen mode Exit fullscreen mode

The second to last line of code is where we apply our decorator. We are setting the variable divide to zero_checker and passing it our divide function.

Let's take it one step at a time.

When Python reaches the last line of code print(divide(10, 0)), it knows to apply our decorator. So it goes up to the top line and works its way down. It then creates inner() and checks to see if b is equal to zero. It is equal to zero so it returns You cannot divide by zero.

Let's take a look at the same example except replace print(divide(10, 0)) with print(divide(10, 2))

Python sees the print statement at the end and jumps up to the zero_checker function because we declared divide = zero_checker(divide). It creates inner() and does the check. This time it is not true so it skips that block of code and returns our passed function divide thus executing it and returning 5.

Since Python developers have used lines similar to divide = zero_checker(divide) we can remove it completely and just add this one line of code above our divide function:

@zero_checker
def divide(a, b):
  return a / b
Enter fullscreen mode Exit fullscreen mode

I know it can be difficult grasping this concept via a blog post and sometimes many YouTube videos touch on so many subjects before getting to decorators so I suggest you cut and paste the complete code below to this Python visualizing tool!

def zero_checker(function):
  def inner_function(a, b):
      if b == 0:
          return 'You cannot divide by zero'
      return function(a, b)
  return inner_function

@zero_checker
def divide(a, b):
  return a / b

print(divide(10, 0))

Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
mburszley profile image
Maximilian Burszley • Edited

A one-liner:

They're functions that wrap functions to provide additional functionality.

In your opener with Flask, what its route function does is register the views with the framework so you're not doing something like:

import flask

def index() -> flask.Flask:
    return flask.make_response('', 200)

app = flask.Flask('app')
app.add_url_rule('/', 'index', index)

It makes the framework a little more declarative.