Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
188: Preserving Metadata with functools.wraps
I was digging through some legacy code yesterday and noticed something weird. We had a set of functions decorated with a performance timer, but when the automated documentation generator ran, every single one of those functions was listed as being named "wrapper" and had no description. It was a mess.
If you've been writing decorators, you've probably noticed that we're essentially swapping one function for another. Let's recreate that scenario so I can show you exactly where the wheels fall off.
The Identity Crisis
Let's say I want to time how long a specific function takes to run. I'll write a quick decorator for that. I'll apply it to a function that simulates a slow API call—something with a proper docstring, because in a real codebase, that's how your teammates know what the function actually does.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"Executed {func.__name__} in {end - start:.4f}s")
return result
return wrapper
@timer
def fetch_user_profile(user_id):
"""Fetches a user profile from the remote database."""
time.sleep(1.5)
return {"id": user_id, "name": "Jane Doe"}
# Let's call it once to make sure it works
fetch_user_profile(123)
On the surface, this works perfectly. The timing is accurate, and the return value is correct. But here is where things get strange. If I try to inspect the function—which is exactly what IDEs like PyCharm or VS Code do when you hover over a function call—this is what happens:
print(fetch_user_profile.__name__)
# Output: wrapper
print(fetch_user_profile.__doc__)
# Output: None
Wait, what? I named the function fetch_user_profile, and I wrote a clear docstring. But Python is telling me the name is "wrapper" and the documentation is gone. This is because @timer literally replaced my original function with the wrapper function defined inside the decorator. The original metadata was tossed in the trash.
Why This Actually Breaks Things
You might think, "Who cares? The code still runs." But in a professional environment, this is a nightmare. If you use a tool like Sphinx to generate your API docs, your entire manual will just say "wrapper" for every decorated function. Even worse, if you're using a debugging tool or a logging framework that relies on __name__ to track where an error occurred, your logs will just be a sea of "wrapper" errors, leaving you with no idea which function actually crashed.
Restoring the Paper Trail
Python's standard library has a built-in fix for this in the functools module. It's a decorator for your decorator called wraps. It essentially copies the metadata from the original function onto the wrapper function.
Let's try that and see if it fixes our identity crisis:
import time
from functools import wraps
def timer(func):
@wraps(func) # This is the magic line
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"Executed {func.__name__} in {end - start:.4f}s")
return result
return wrapper
@timer
def fetch_user_profile(user_id):
"""Fetches a user profile from the remote database."""
time.sleep(1.5)
return {"id": user_id, "name": "Jane Doe"}
print(fetch_user_profile.__name__)
# Output: fetch_user_profile
print(fetch_user_profile.__doc__)
# Output: Fetches a user profile from the remote database.
There we go. By adding @wraps(func), we've told Python: "Take everything that makes fetch_user_profile unique—its name, its docstring, its module—and slap it onto the wrapper." Now the function behaves like the wrapper, but it looks like the original. It's a small addition, but it's the difference between a "script" and professional, maintainable software.
📋 Practical Task
Exercise: Fixing the Metadata Leak in a Permission Checker
You are working on a web application where several functions are decorated with a @require_admin decorator to restrict access. However, the team's introspection tool is failing because all the restricted functions are reporting their name as "check_permissions".
Below is the buggy code. Your task is to modify the require_admin decorator so that it preserves the metadata of the functions it wraps.
from functools import wraps
def require_admin(func):
# TODO: Add the necessary decorator here to preserve metadata
def check_permissions(*args, **kwargs):
user_role = "guest" # Simulating a guest user
if user_role != "admin":
print("Access Denied: Admin role required.")
return None
return func(*args, **kwargs)
return check_permissions
@require_admin
def delete_database():
"""Permanently deletes all records from the database."""
print("Database deleted!")
@require_admin
def update_system_config():
"""Updates the global system configuration settings."""
print("Config updated!")
# Test your fix:
print(f"Function 1 name: {delete_database.__name__}")
# Should be 'delete_database', not 'check_permissions'
print(f"Function 2 doc: {update_system_config.__doc__}")
# Should be 'Updates the global system configuration settings.', not None
There are no comments for now.