Python decorator is syntactic sugar. You can achieve everything without explicitly using the decorator. However, Using the decorator can help your code be more concise and readable. Ultimately, you write fewer lines of code by leveraging Python decorators.
Nevertheless, Python decorator isn’t a trivial concept to comprehend. Understanding Python decorators requires building blocks, including closure, function as an object, and deep knowledge of how Python code is executed.
Many online resources talk about Python decorators, but many tutorials only provide some sample code walkthroughs. Reading sample code could help to get a basic understanding of Python decorator. When it comes time to implement your decorator, we might still need clarification and clarity about the decorator concept and yet have to refer to the online resource to refresh our memory for detail.
Reading code sometimes doesn’t deepen memory but seeing images does. In this article, I want to help you understand Python decorator with some visualization and fun examples.
Python Functions are Objects

If we went to Python programming 101 class, we might get a drawing like this. We defined a variable, a representational name used to reference an object. The variable foo
points to an object at a given time. It can be a list [1,2,3,4]
, it can be a dictionary [“city”: “New York”]
, or it can a string “I like dumplings”
A less covered discussed topic in Python is that the variable foo
can also point to a function add()
. When the variable refers to a function, the foo
can pass around the same way you used other types like int, str, list, or dict.
What does that mean I can pass foo
around? This unlocks us to pass a function as an argument. Furthermore, we can return a function as a return type
# Python Functions are Objects
def I_am_1():
return 1
def return_1():
return I_am_1
def add(a, b):
return a + b
foo = add
del add
## let remove the original defintion
print(foo(return_1()(),2))
## ouput 3
print(foo(foo(return_1()(),2),3))
## output 6
Code walkthrough
- Function Definition: we defined three functions called
add
which adds two objects;I_am_1()
which simply returns number 1;return_1
which return the functionI_am_1
- Interesting Fact: Then we point to another variable
foo
to it. To prove that function is an object like the other types, we can even remove the original function reference namedadd
. The rest of the code is still runnable due tofoo
refers to the object for the function. - Result Explain:
return_1()()
looks wired at first. If we take a closer look, it is the same way as we are calling the function,return_1()
isI_am_1
as it just returns this function. In this case, we can thinkreturn_1()()=1
mentally, so it does not surprise us thefoo(1,2)
gives an output of 3. We can also passfoo(1,2)
to another function. In this case, we passed it to itself. Sincefoo(1,2)=3
, the outer function will operate asfoo(3,3)
. To get the result, we can pass the entire function with its arguments over and let the program execute at runtime to decipher everything.
Code Visualisation

Python Decorators Structure
A Python decorator transforms the functionality of the original object without modifying the original one. It is syntax sugar that allows users to extend the object’s ability more conveniently instead of duplicating some existing code. Decorators are a pythonic implementation of the decorator design pattern (Note: python decorator isn’t precisely the same as decorator design pattern).
We have discussed that we can return a function as a return type. Now, we extend that idea to demonstrate how a decorator works: we can return a function as a return type within another function.
To make our example more fun, we can create our decorator like a wrapper around a function and later stacks the multiple decorators.
Let’s first define our functions and use the example of making some dumplings. 🥟🥟🥟
## Python Decorators Basic -- I love dumplings
def add_filling_vegetable(func):
def wrapper(*args, **kwargs):
print("<^^^vegetable^^^>")
func(*args, **kwargs)
print("<^^^vegetable^^^>")
return wrapper
def add_dumplings_wrapper(func):
def wrapper(*args, **kwargs):
print("<---dumplings_wrapper--->")
func(*args, **kwargs)
print("<---dumplings_wrapper--->")
return wrapper
def dumplings(ingredients="meat"):
print(f"({ingredients})")
add_dumplings_wrapper(add_filling_vegetable(dumplings))()
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (meat)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
add_dumplings_wrapper(add_filling_vegetable(dumplings))(ingredients='tofu')
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (tofu)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
Code walkthrough
Function Definition: we defined three functions called add_filling_vegetable
, add_dumplings_wrapper
, and dumplings
. Within add_filling_vegetable
and add_dumplings_wrapper
, we define a wrapper function that wraps around the original function passed as an argument. Additionally, we can do additional things. In this case, we print some text before and after the original function. If the original function also defined parameters, we can pass them in from the wrapper function. We also leverage the magic *args
and **kwargs
to be more flexible
Result Explain
- we can call the default ingredients for
meat
by using the default oneadd_dumplings_wrapper(add_filling_vegetable(dumplings))()
, we can see the function are chaining together. It’s not readable, which we will fix shortly with decorator syntax. The core idea here is that we keep passing the function object as an argument to another. The function does two things: 1) continue doing the unmodified original function; 2) execute additional code. The execution of the entire code is like a round trip.

- for
add_dumplings_wrapper(add_filling_vegetable(dumplings))(ingredients=’tofu’)
it changes the default ingredients frommeat
totofu
by passing an additional argument. In this case,*args
and**kwargs
is very useful for resolving the complexity. The wrapper functions don’t have to unwrap to know the context of the argument. As a decorator, it is safe to pass down the actual function without modifying them.

So far, we haven’t touched the decorator syntax yet. Let’s make a small change in how we define the original function and call it fancy_dumplings
.
## Stack decorator, the order matters here
@add_dumplings_wrapper
@add_filling_vegetable
def fancy_dumplings(ingredients="meat"):
print(f"({ingredients})")
fancy_dumplings()
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (meat)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
fancy_dumplings(ingredients='tofu')
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (tofu)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
Now decorator simplifies how we call all the functions and makes it more readable. We only need to call fancy_dumplings
only once. it is much cleaner visually than nesting multiple functions horizontally.
What can I use decorators for?
Great! How to create the Python decorators are clear to us now. What can I use decorators for?
There are many potential practical use cases for Python decorators. It makes your code easier to read and dynamically. Note you don’t necessarily need to go decorators. We can always implement a wrapper around another function to achieve the same goal. The decorator is just syntactic sugar.
You can build your time decorator to evaluate the performance for a given function. The following is a timer example from Primer on Python Decorators
import functools
import time
def timer(func):
"""Print the runtime of the decorated function"""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__!r} in {run_time:.10f} secs")
return value
return wrapper_timer
It measures the time to execute a given function. Let’s stack it with our dumplings example.
@timer
@add_dumplings_wrapper
@add_filling_vegetable
def fancy_dumplings(ingredients="meat"):
print(f"({ingredients})")
fancy_dumplings()
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (meat)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
# Finished 'wrapper' in 0.0000334990 secs
You can keep stacking the decorators to achieve your goal by simply calling the original function without worrying about wrapping other functions around when calling, as we have provided the sequence of the decorator when we define the original function.

Final Thoughts
There are many possibilities you can leverage the Python decorators. Essentially, it’s a wrapper to alert the behavior of the original function. It depends on your perspective to judge how practical the decorator is, but it shouldn’t be a syntax that feels foreign to you. I hope with some visualization, the concept of decorators becomes more straightforward to understand. Please let me know if you have any comments on this story.
About Me
I hope my stories are helpful to you.
For data engineering post, you can also subscribe to my new articles or becomes a referred Medium member that also gets full access to stories on Medium.
More Articles

6 Side Project Ideas for New and Experienced Data Engineers
Data engineers can work on some side projects to get experience. Those projects could initiate impressive discussions to help you land a dream job. We will introduce 6 data engineering side project ideas regardless of your experience.

Uncovering the Truth About Apache Spark Performance: coalesce(1) vs. repartition(1)
We will discuss a neglected part of Apache Spark Performance between coalesce(1) and repartition(1), and it could be one of the things to be attentive to when you check the Spark job performance.

Unlocking the Secrets of Slowly Changing Dimension (SCD): A Comprehensive View of 8 Types
Slowly Changing Dimension (SCD) is critical to dimensional modeling. We will discuss the eight types of SCDs. By the end, you will clearly understand each type and be able to differentiate between SCDs in dimensional modeling.