The Decorator That Saved Me 500 Lines of Code
How one Friday afternoon code review taught me what decorators actually do
Hi, I'm Rachel.
Three months into my first Python job at DataFlow Analytics, I was drowning in my own code.
Not broken code. Working code. Code that did exactly what it was supposed to do. But every function I wrote started with timing setup, wrapped the actual work in try-catch blocks, logged everything, and measured performance. The boilerplate overwhelmed the logic.
I had 47 functions that looked like this:
def process_user_data(user_id):
start_time = time.time()
logger.info(f"Starting process_user_data for {user_id}")
try:
result = fetch_and_transform(user_id) # The actual work
logger.info(f"Completed in {time.time() - start_time:.2f}s")
return result
except Exception as e:
logger.error(f"Error: {e}")
raise
Same pattern, different function names. I knew this violated DRY principles, but I didn't know what to do about it.
My "Clever" Solution
I tried extracting the pattern into a helper function. Now every function called my helper, passing in the function name as a string and the actual work as a parameter. I'd traded copy-paste boilerplate for wrapper boilerplate. The line count barely changed, and honestly, it looked worse.
The Friday Afternoon That Changed Everything
Elena, our senior engineer, was doing code review. She stopped at my helper functions.
"Walk me through your thinking here."
I explained my refactoring. How I'd identified the pattern. How I'd extracted it.
"Good instinct," she said. "But you're doing manually what decorators were designed to do."
She showed me this:
@log_and_time
def process_user_data(user_id):
return fetch_and_transform(user_id)
@log_and_time
def generate_report(report_id):
return build_report(report_id)
I stared at the screen. Each function was now just... the actual work. The logging, timing, and error handling had vanished into that single @log_and_time
line.
"Wait," I said. "What's actually happening?"
The Lightbulb Moment
"A decorator is just a function that wraps another function," Elena explained. "When Python sees that @
symbol, it's doing this behind the scenes:"
# You write:
@log_and_time
def process_user_data(user_id):
return fetch_and_transform(user_id)
# Python actually does:
process_user_data = log_and_time(process_user_data)
It clicked. The @
syntax was just shorthand. My function got passed to the decorator, which returned a wrapped version. That wrapped version—with all the logging and timing—replaced my original function.
A decorator was exactly what I'd been trying to build with my helper function. But instead of me manually wrapping each function call, Python automated it. The pattern I'd discovered on my own had a name, and the language had built-in syntax for it.
What I Learned
Over the next week, I refactored. Those 47 functions became 47 clean functions with a single decorator. I deleted 500+ lines of repetitive code.
But more importantly, I learned decorators aren't magic. They're a solution to a specific problem: "I need to add the same behavior to multiple functions without modifying each one."
The decorator pattern existed in my code all along—I'd just been implementing it manually. Python decorators are simply the language's way of making that pattern clean and reusable.
Now when I see repetitive setup or teardown code, I don't reach for helper functions. I ask: "Am I wrapping behavior around other behavior?" If yes, that's a decorator.
The best code patterns aren't clever tricks. They're formalizations of things you were already trying to do.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Comments
Post a Comment