Python functions are objects, and like any object, they can carry custom attributes. A function might have func.is_admin_only = True set on it, or func.version = "2.1", or func.retry_count = 3. When a decorator wraps that function, these custom attributes need to survive the transition. This article explains the mechanics of how functools.wraps handles custom attributes through __dict__ merging, when attributes get lost despite using @wraps, how to add new custom attributes from inside a decorator, and how to extend the default attribute transfer to include things like __defaults__ and __kwdefaults__.
Custom Attributes on Functions
In Python, functions are objects with a __dict__ attribute that stores arbitrary key-value pairs. You can set and read custom attributes on any function just like you would on any other object:
def process_payment(order_id, amount):
"""Process a payment for the given order."""
return {"order": order_id, "amount": amount, "status": "paid"}
# Set custom attributes
process_payment.requires_auth = True
process_payment.api_version = "v2"
process_payment.max_retries = 3
# Read them back
print(process_payment.requires_auth) # True
print(process_payment.__dict__)
# {'requires_auth': True, 'api_version': 'v2', 'max_retries': 3}
These attributes live in the function's __dict__. Frameworks use this pattern extensively. Flask stores route metadata on view functions. Celery stores task configuration on task functions. Testing frameworks attach markers to test functions. When a decorator wraps one of these functions, the custom attributes need to remain accessible on the decorated version.
How functools.wraps Handles __dict__
functools.wraps performs two distinct operations on attributes. It assigns certain attributes by directly overwriting them on the wrapper (this covers __name__, __doc__, __module__, __qualname__, __annotations__, and __type_params__). It updates the wrapper's __dict__ by merging the original function's __dict__ into it using dict.update().
This merge is what carries custom attributes over:
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
def process_payment(order_id, amount):
"""Process a payment."""
return {"order": order_id, "amount": amount}
# Set custom attributes BEFORE decoration
process_payment.requires_auth = True
process_payment.api_version = "v2"
# Now decorate
process_payment = timer(process_payment)
# Custom attributes survived!
print(process_payment.requires_auth) # True
print(process_payment.api_version) # v2
print(process_payment.__name__) # process_payment
The __dict__ merge copies all key-value pairs from the original function's __dict__ into the wrapper's __dict__. Since requires_auth and api_version were set before decoration, they exist in the original function's __dict__ at the time functools.wraps runs, so they get merged into the wrapper.
The merge uses dict.update(), which means if the wrapper already has an attribute with the same name, the value from the original function overwrites it. This is rarely an issue in practice because freshly defined wrapper functions have an empty __dict__.
The reason the merge is shallow -- a one-time snapshot rather than a live link -- comes directly from how Python objects work. Every function has its own __dict__, and dict.update() copies values by reference, not by creating a synchronized binding. After the merge, the original function and the wrapper each hold independent dictionaries that happen to contain the same values. Changing one does not change the other, for the same reason that a = b.copy(); a['x'] = 1 does not modify b.
This matters when you debug attribute-related issues in decorated code. If an attribute seems to be missing on the wrapper, the question to ask is not "did @wraps fail?" but "did the attribute exist on the original function at the exact moment @wraps ran?" That timing question leads directly to the next section.
The Timing Problem
The __dict__ merge happens once, at decoration time, when functools.wraps executes. This creates a timing dependency: attributes set on the function before decoration are copied; attributes set after decoration are not -- because the name in the namespace now points to the wrapper, not the original function.
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@timer
def fetch_data():
"""Fetch remote data."""
pass
# Setting attribute AFTER decoration -- this goes on the wrapper, not the original
fetch_data.cache_ttl = 300
# The wrapper has it (because we just set it on the wrapper)
print(fetch_data.cache_ttl) # 300
# But the original function does NOT have it
print(fetch_data.__wrapped__.cache_ttl) # AttributeError!
After decoration, fetch_data points to the wrapper. Setting fetch_data.cache_ttl = 300 adds the attribute to the wrapper's __dict__. The original function, accessible via fetch_data.__wrapped__, never received the attribute. This is typically fine -- the attribute lives on the object that callers interact with. But it is important to understand the direction of the merge: original-to-wrapper at decoration time, not continuously synchronized.
When using the @decorator syntax, attributes set on the function before the @ line are not possible because the function does not exist yet. The @ syntax applies the decorator immediately after the def block. To set attributes before decoration, you must decorate manually:
def fetch_data():
"""Fetch remote data."""
pass
# Set attributes before manual decoration
fetch_data.requires_auth = True
fetch_data.cache_ttl = 300
# Now decorate manually -- @wraps will merge these into the wrapper
fetch_data = timer(fetch_data)
print(fetch_data.requires_auth) # True
print(fetch_data.cache_ttl) # 300
Walk through the execution order to see exactly when each attribute transfer occurs:
fetch_data.__dict__. The function object exists, so this works.__dict__ before any decorator runs.timer, @functools.wraps(func) calls wrapper.__dict__.update(func.__dict__). Both requires_auth and cache_ttl are in func.__dict__ at this moment, so both get copied into the wrapper's __dict__. The name fetch_data is then rebound to the wrapper.Manual decoration is the only way to set attributes on the original function before @wraps runs. When you use @decorator syntax, the function does not exist until the def block completes, and the decorator fires immediately after. There is no window to set attributes between function creation and decoration. If attributes must come from outside the decorator, manual decoration is the tool.
Why Not Other Approaches
The __dict__ merge via functools.wraps is not the only way to preserve or transfer metadata through decoration. Engineers coming from other patterns sometimes reach for alternatives. Each has trade-offs that make the @wraps approach the standard for most decorator use cases.
Reading through __wrapped__ instead of copying
Since functools.wraps sets a __wrapped__ attribute pointing to the original function, you could skip the __dict__ merge entirely and read attributes from func.__wrapped__ at call time. This avoids the timing problem because you are always reading from the source of truth. The cost is that every consumer of the decorated function must know to follow the __wrapped__ chain -- and the chain can be arbitrarily deep in stacked decorators. Framework dispatchers that read getattr(func, 'requires_auth', False) would need to walk the chain manually, turning a simple attribute lookup into a recursive traversal. The __dict__ merge exists precisely to surface attributes at the outermost layer so consumers do not need to know about the decorator chain at all.
Class-based decorators with __getattr__
A callable class can act as a decorator and proxy attribute access to the wrapped function via __getattr__:
class Timer:
def __init__(self, func):
self._func = func
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
start = time.perf_counter()
result = self._func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{self._func.__name__} took {elapsed:.4f}s")
return result
def __getattr__(self, name):
return getattr(self._func, name)
This gives live attribute access -- changes to the original function's attributes are visible through the wrapper in real time, unlike the one-time merge. But class-based decorators introduce a different object type into the call chain: the decorated function is now an instance of Timer, not a function. This breaks inspect.isfunction(), changes behavior with descriptors (the decorated object will not bind as a method in a class body without implementing __get__), and may surprise tools that assume decorated functions remain functions. For library and framework code where compatibility matters, function-wrapping-function with @wraps has fewer edge cases.
Storing metadata externally
Instead of attaching attributes to the function at all, you can store metadata in a separate registry keyed by function identity:
import functools
_metadata: dict[int, dict] = {}
def tag(key, value):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
_metadata.setdefault(id(wrapper), {})[key] = value
return wrapper
return decorator
This avoids the timing problem and the __dict__ merge entirely. Metadata lives in a central dictionary, keyed by the wrapper's id(). The trade-off is indirection: consumers must import and query the registry instead of using getattr. It also introduces lifecycle complexity -- if wrapper functions are garbage-collected, stale entries remain in the registry unless cleanup is handled separately, and id() values can be reused by CPython for new objects after the original is collected. This pattern appears in some plugin systems but is overkill for the typical case where a decorator just needs to stamp a few attributes on a wrapper.
The __dict__ merge via functools.wraps is the Python standard because it optimizes for the common case: attributes set before or during decoration should be readable from the outermost wrapper with a plain getattr. It requires no special consumer code, preserves function identity for introspection tools, and works with the descriptor protocol. The alternatives above solve real problems in specific contexts (live proxying, external registries), but they add complexity that is rarely justified when the one-time snapshot approach works.
Adding New Attributes from Inside a Decorator
A decorator can add its own custom attributes to the wrapper. Since the wrapper is the object that replaces the function in the namespace, any attribute set on it becomes accessible to callers. Set attributes on the wrapper after the @functools.wraps(func) call to avoid having them overwritten by the __dict__ merge:
import functools
def track_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
wrapper.call_count += 1
return func(*args, **kwargs)
# Add custom attributes AFTER @wraps (so merge doesn't overwrite them)
wrapper.call_count = 0
wrapper.reset_count = lambda: setattr(wrapper, 'call_count', 0)
return wrapper
@track_calls
def process(data):
"""Process incoming data."""
return len(data)
process([1, 2, 3])
process([4, 5])
print(process.call_count) # 2
process.reset_count()
print(process.call_count) # 0
print(process.__name__) # process (metadata preserved)
The call_count and reset_count attributes are set on the wrapper after @functools.wraps(func) has already merged the original function's __dict__. This means the decorator's own attributes are added on top of whatever the original function carried.
If the original function has an attribute with the same name as one your decorator adds (such as call_count), the decorator's assignment will overwrite the merged value. If this is a concern, check hasattr(wrapper, attr_name) before setting.
Extending WRAPPER_ASSIGNMENTS
By default, functools.wraps assigns these six attributes: __module__, __name__, __qualname__, __doc__, __annotations__, and __type_params__ (added in Python 3.12). There are other function attributes -- such as __defaults__ (default values for positional parameters) and __kwdefaults__ (default values for keyword-only parameters) -- that are not copied by default. You can extend the assignment list by passing a custom assigned tuple to functools.update_wrapper:
import functools
EXTENDED_ASSIGNMENTS = functools.WRAPPER_ASSIGNMENTS + (
'__defaults__',
'__kwdefaults__',
)
def full_copy_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
functools.update_wrapper(
wrapper, func,
assigned=EXTENDED_ASSIGNMENTS
)
return wrapper
@full_copy_decorator
def connect(host: str, port: int = 5432, *, ssl: bool = True) -> None:
"""Connect to the database."""
pass
print(connect.__name__) # connect
print(connect.__defaults__) # (5432,)
print(connect.__kwdefaults__) # {'ssl': True}
The EXTENDED_ASSIGNMENTS tuple starts with all the standard assignments and adds __defaults__ and __kwdefaults__. These are not included by default because not all callable wrappers support them -- class instances used as decorators do not have __defaults__ -- but for function-to-function wrapping, they are safe to include.
Copying __defaults__ onto the wrapper does not change the wrapper's actual execution behavior. The wrapper still accepts *args, **kwargs and forwards them to the original function. The copied __defaults__ exists as metadata only -- it is visible to tools that read it, but the wrapper does not use it for argument binding.
validate has a custom attribute validate.schema = "v3" set on it. A decorator that uses @functools.wraps(func) wraps validate. After decoration, you run validate.__wrapped__.schema = "v4". What is the value of validate.schema?func.retry_limit = 3 set on it before decoration. Inside the decorator, the developer writes wrapper.retry_limit = 5 and then calls functools.update_wrapper(wrapper, func). What is decorated_func.retry_limit?@decorator_a, @decorator_b, @decorator_c (bottom). decorator_b does not use @functools.wraps. decorator_c sets wrapper.tag = "c". Does decorator_a's wrapper have the tag attribute?Building an Attribute-Marking Decorator
A common pattern is a decorator that marks functions with metadata attributes for later discovery by a framework. For example, marking functions as requiring specific permissions, belonging to a specific API version, or being eligible for caching:
import functools
def require_role(*roles):
"""Mark a function as requiring specific user roles."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.required_roles = roles
return wrapper
return decorator
def cacheable(ttl=60):
"""Mark a function as cacheable with a given TTL."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.is_cacheable = True
wrapper.cache_ttl = ttl
return wrapper
return decorator
@require_role("admin", "editor")
@cacheable(ttl=300)
def update_article(article_id, content):
"""Update an article's content."""
return {"id": article_id, "status": "updated"}
# Both decorators' attributes are accessible
print(update_article.required_roles) # ('admin', 'editor')
print(update_article.is_cacheable) # True
print(update_article.cache_ttl) # 300
print(update_article.__name__) # update_article
Both decorators add their own custom attributes to their respective wrappers. Because @require_role wraps the result of @cacheable, the __dict__ merge in @require_role's @functools.wraps call copies is_cacheable and cache_ttl from the @cacheable wrapper into the @require_role wrapper. Then @require_role adds required_roles on top. All three attributes end up on the final decorated function.
Custom Attributes in Stacked Decorators
When decorators are stacked, the __dict__ merge propagates custom attributes upward through the chain -- as long as every decorator in the chain uses @functools.wraps. Each decorator's @wraps call merges the __dict__ from the function it receives, which includes any attributes set by decorators below it in the stack.
import functools
def decorator_a(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.added_by_a = True
return wrapper
def decorator_b(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.added_by_b = True
return wrapper
@decorator_a
@decorator_b
def my_function():
"""My function."""
pass
# Both attributes survive to the outermost wrapper
print(my_function.added_by_a) # True
print(my_function.added_by_b) # True
print(my_function.__name__) # my_function
The flow is: decorator_b wraps my_function and sets added_by_b on its wrapper. decorator_a receives that wrapper, and its @functools.wraps call merges the __dict__ (which contains added_by_b) into its own wrapper. Then decorator_a sets added_by_a on top. The final result has both attributes.
If a decorator in the chain does not use @functools.wraps, the __dict__ merge does not happen at that layer, and any attributes set by decorators below it will be lost for all decorators above.
This code stacks two decorators. The developer expects send_email.__name__ to be "send_email", but it prints "wrapper" instead. The requires_auth attribute works fine. What went wrong with the name?
import functools
def require_auth(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Checking auth...")
return func(*args, **kwargs)
wrapper.requires_auth = True
return wrapper
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@require_auth
@log_calls
def send_email(to, subject):
"""Send an email notification."""
pass
print(send_email.__name__) # Expected: "send_email" -- Actual: "wrapper"
print(send_email.requires_auth) # Expected: True -- Actual: True (this one works)Reading Custom Attributes at Runtime
A framework or dispatcher that reads custom attributes from decorated functions can use getattr with a default value to safely check for the presence of decorator-assigned metadata:
def dispatch(func, user, *args, **kwargs):
"""Execute func if the user has the required roles."""
required = getattr(func, 'required_roles', ())
if required and user.get('role') not in required:
raise PermissionError(
f"User '{user.get('name')}' lacks required role"
)
ttl = getattr(func, 'cache_ttl', None)
if ttl is not None:
print(f"Caching enabled with TTL={ttl}s")
return func(*args, **kwargs)
# The dispatcher reads attributes without knowing which decorators set them
dispatch(update_article, {"name": "Kandi", "role": "admin"}, "art-42", "New content")
The getattr with a default ensures that functions without the attribute do not raise an AttributeError. This pattern makes the attribute-checking code safe to run on both decorated and undecorated functions.
Every concept in this article reduces to one idea: functools.wraps takes a one-time snapshot of the original function's identity and merges it onto the wrapper. The snapshot includes standard metadata (__name__, __doc__, etc.) via direct assignment and custom attributes via __dict__.update(). After the snapshot, the original and wrapper are independent objects. Everything else -- timing, ordering, stacking, attribute marking -- is a consequence of when and how that snapshot happens.
When you encounter an attribute-related bug in decorated code, the debugging question is always the same: was this attribute present in the right __dict__ at the exact moment the snapshot was taken?
Key Takeaways
- functools.wraps merges
__dict__from the original function into the wrapper. Custom attributes stored in the original function's__dict__before decoration are carried over to the wrapper throughdict.update(). This is theupdatedparameter in action, and it defaults to('__dict__',). - Timing matters: attributes must exist before decoration. The
__dict__merge happens once, at decoration time. Attributes set on the original function before the decorator runs are copied. Attributes set after decoration go on the wrapper directly, not on the original function. - Decorators can add their own attributes to the wrapper. Set them after the
@functools.wraps(func)line to ensure the__dict__merge does not overwrite them. These decorator-assigned attributes are then available to callers and to any decorator stacked above. - Custom attributes propagate upward through stacked decorator chains. Each
@functools.wrapscall merges the__dict__from the function it receives, which includes attributes set by decorators lower in the stack. Every decorator in the chain must use@wrapsfor this propagation to work. - Extend WRAPPER_ASSIGNMENTS for additional standard attributes. Pass a custom
assignedtuple tofunctools.update_wrapperto include attributes like__defaults__and__kwdefaults__that are not copied by default. - Use
getattrwith a default when reading custom attributes. This makes code safe to run on functions that may or may not have been decorated, avoidingAttributeErroron undecorated functions. - The merge is a shallow copy, not a live link. Changes to the original function's
__dict__after decoration do not propagate to the wrapper. Changes to the wrapper's__dict__after decoration do not propagate to the original function. They are independent dictionaries after the merge.
Custom attributes on functions are a lightweight, powerful pattern for framework integration, metadata marking, and decorator communication. The __dict__ merge performed by functools.wraps carries them through decoration automatically -- as long as the timing is right and every decorator in the chain participates. Understanding the merge mechanics turns custom attributes from a fragile convention into a reliable tool for building extensible Python systems.
Frequently Asked Questions
Does functools.wraps copy custom attributes from the wrapped function?
Yes, but only if those attributes exist on the function at the time functools.wraps runs. functools.wraps merges the wrapped function's __dict__ into the wrapper's __dict__ using dict.update(). Any custom attributes stored in the function's __dict__ before decoration (such as func.is_admin = True) will be carried over. Attributes set after decoration will not be transferred because wraps has already executed.
What is the difference between assigned and updated in functools.wraps?
The assigned parameter specifies attributes that are directly overwritten on the wrapper -- the value from the wrapped function replaces whatever the wrapper had. By default, this includes __name__, __doc__, __module__, __qualname__, __annotations__, and __type_params__ (added in Python 3.12). The updated parameter specifies attributes that are merged using dict.update() -- the wrapped function's values are added to the wrapper's existing values. By default, this includes only __dict__.
How do I add custom attributes to a decorated function?
Set the attribute on the wrapper function after defining it but before returning it from the decorator. Because functools.wraps copies the original function's __dict__ into the wrapper's __dict__ first, any custom attribute you set on the wrapper afterward will persist on the final decorated function. If the original function already had an attribute with the same name, your assignment on the wrapper will overwrite it.
Can I make functools.wraps copy __defaults__ and __kwdefaults__?
Yes. Pass a custom assigned tuple to functools.wraps or functools.update_wrapper that includes __defaults__ and __kwdefaults__ alongside the standard attributes. These are not copied by default because not all callable objects have them (classes used as wrappers do not), but functions do. Adding them to assigned causes functools.wraps to copy the default argument values from the original function onto the wrapper.
Why do custom attributes set after decoration not appear on the wrapper?
After decoration, the name in the namespace points to the wrapper, not the original function. Setting an attribute on what you think is the original function is setting it on the wrapper. If you set an attribute on the original function object itself (accessed via __wrapped__), it will not appear on the wrapper because __dict__ merging only happens once, at decoration time. To add attributes after decoration, set them on the wrapper directly.