functools.wraps vs decorator Module for Signature Preservation

When you apply @functools.wraps to a decorator, inspect.signature() reports the correct parameter list and help() shows the right name and docstring. For a significant number of Python projects, that is all the signature preservation that matters. But the wrapper function's actual bytecode signature is still (*args, **kwargs). Tools that read the bytecode directly, frameworks that dispatch based on argument counts, and error messages from invalid calls can all behave differently than they would with the unwrapped function. The decorator module, wrapt, and boltons.funcutils each solve this problem in a different way. This article puts all four approaches side by side, compares what each one preserves and where each one falls short, and provides a decision framework for choosing the right tool.

The core tension is between metadata preservation and bytecode-level accuracy. functools.wraps handles the first. The third-party tools handle both. Understanding the difference requires knowing what Python reads when it introspects a function, which varies depending on which introspection method is used.

What "Signature Preservation" Means

A function's signature can be read through at least three different mechanisms, and each one returns different results for a decorated function depending on which preservation tool was used.

inspect.signature() is the high-level introspection function. It is aware of the __wrapped__ attribute that functools.wraps sets, and it follows that chain to report the original function's parameters. This is what help() uses internally, and it is what IDE tooltips typically rely on.

inspect.getfullargspec() reads the function's actual code object and defaults. It does not follow __wrapped__. For a wrapper using (*args, **kwargs), it reports exactly that: no named parameters, varargs named args, varkw named kwargs, no defaults.

Direct call validation is the third mechanism. When you call a function with an incorrect argument, Python validates the arguments against the function's actual compiled signature, not the one reported by inspect.signature(). This determines the error message.

Here is what this looks like in practice with a decorated function using functools.wraps:

import functools
import inspect

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_calls
def transfer(sender, receiver, amount, currency="USD"):
    """Transfer funds between two accounts."""
    return f"{amount} {currency} from {sender} to {receiver}"

# inspect.signature() follows __wrapped__ — CORRECT
print(inspect.signature(transfer))
# (sender, receiver, amount, currency='USD')

# inspect.getfullargspec() reads bytecode — INCORRECT
print(inspect.getfullargspec(transfer))
# FullArgSpec(args=[], varargs='args', varkw='kwargs',
#             defaults=None, kwonlyargs=[], kwonlydefaults=None,
#             annotations={})

# Invalid call — error references wrapper's *args, not original params
try:
    transfer(sender="Alice", recipient="Bob", amount=100)
except TypeError as e:
    print(e)
# transfer() got an unexpected keyword argument 'recipient'

The inspect.signature() result is correct because it follows __wrapped__. The getfullargspec() result is wrong because it reads the wrapper's bytecode. The error message names the function correctly (thanks to __name__ being copied) but the error itself comes from the wrapper passing the argument through to the original function, where it fails. This is the "leaky abstraction" that functools.wraps produces. For application code where these edge cases do not matter, this is fine. For library code, it can cause real problems.

Four Tools Compared

1. functools.wraps (Standard Library)

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

functools.wraps copies __name__, __doc__, __qualname__, __module__, __annotations__, and __type_params__ from the original function onto the wrapper. It updates the wrapper's __dict__ and adds __wrapped__. The wrapper's compiled signature stays as (*args, **kwargs). If you need a refresher on how variadic arguments like *args and **kwargs work, that background is essential for understanding why the wrapper's actual signature differs from the original. Signature accuracy depends entirely on the consuming tool's ability to follow __wrapped__.

2. decorator Module (pip install decorator)

from decorator import decorator

@decorator
def my_decorator(func, *args, **kwargs):
    return func(*args, **kwargs)

The decorator module by Michele Simionato takes a fundamentally different approach. Instead of wrapping the function in a generic (*args, **kwargs) closure and copying metadata, it generates a new function object whose source code contains the original function's exact parameter list. In versions prior to 5.0, this generation used exec to compile real bytecode with the correct parameters. Since version 5.0, the module leverages Python's Signature object internally, producing cleaner tracebacks when exceptions occur inside decorated functions. The generated wrapper still enforces the original's parameter constraints at call time, so invalid arguments are caught before the original function runs.

The key benefit is that inspect.signature() returns the correct parameter list and invalid argument errors produce clear messages. Note that in version 5.0 and later, the generated wrapper's __code__ object may still report generic arguments internally, so tools that read __code__.co_varnames directly (rather than using inspect.signature()) may not see the correct parameters.

from decorator import decorator
import inspect

@decorator
def log_calls(func, *args, **kwargs):
    print(f"Calling {func.__name__}")
    return func(*args, **kwargs)

@log_calls
def transfer(sender, receiver, amount, currency="USD"):
    """Transfer funds between two accounts."""
    return f"{amount} {currency} from {sender} to {receiver}"

# inspect.signature() returns the correct signature
print(inspect.signature(transfer))
# (sender, receiver, amount, currency='USD')

# Invalid calls are caught with clear error messages
try:
    transfer(sender="Alice", recipient="Bob", amount=100)
except TypeError as e:
    print(e)
# transfer() got an unexpected keyword argument 'recipient'

The decorator module is a single Python file with no dependencies, making it easy to vendor directly into a project. Its API requires a different decorator style than the standard functools.wraps pattern: the original function is passed as the first argument to the decorator function, rather than being captured via closure.

3. wrapt (pip install wrapt)

import wrapt

@wrapt.decorator
def my_decorator(wrapped, instance, args, kwargs):
    return wrapped(*args, **kwargs)

Graham Dumpleton's wrapt library creates transparent object proxies rather than generating new function objects. The decorated function is replaced by a proxy object that delegates all attribute access to the original function. This means every form of introspection returns the correct result: inspect.signature(), inspect.getfullargspec(), help(), and direct attribute access all behave as if no decorator were applied.

The wrapt wrapper function takes four arguments: wrapped (the original function), instance (the bound object when decorating a method, None for standalone functions), args, and kwargs. The instance argument is what makes wrapt handle the Python descriptor protocol correctly. A single @wrapt.decorator works with regular functions, instance methods, class methods, static methods, and classes without any changes to the decorator code.

import wrapt
import inspect

@wrapt.decorator
def log_calls(wrapped, instance, args, kwargs):
    print(f"Calling {wrapped.__name__}")
    return wrapped(*args, **kwargs)

@log_calls
def transfer(sender, receiver, amount, currency="USD"):
    """Transfer funds between two accounts."""
    return f"{amount} {currency} from {sender} to {receiver}"

print(inspect.signature(transfer))
# (sender, receiver, amount, currency='USD')

print(inspect.getfullargspec(transfer))
# FullArgSpec(args=['sender', 'receiver', 'amount', 'currency'],
#             varargs=None, varkw=None, defaults=('USD',), ...)

wrapt includes a C extension for performance-critical components, with an automatic fallback to a pure Python implementation when no compiler is available. It is the heaviest dependency of the four options but provides the highest level of correctness. For a thorough explanation of how Python resolves attribute access and why the descriptor protocol matters for decorators, see Python's Descriptor Protocol and Attribute Lookup Machinery.

4. boltons.funcutils (pip install boltons)

from boltons.funcutils import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Mahmoud Hashemi's boltons library provides a drop-in replacement for functools.wraps that generates a new function with the correct compiled signature, similar to the decorator module. Its primary advantage is API compatibility: the code looks identical to functools.wraps usage, so migrating existing decorators requires only changing the import line. boltons.funcutils also supports an injected parameter for decorators that add or remove arguments from the original signature, and a FunctionBuilder class for programmatic function construction.

DependenciesNone (stdlib)
inspect.signature()Correct (via __wrapped__)
inspect.getfullargspec()Wrong (*args, **kwargs)
help() outputCorrect
Error messagesDelayed (at original func)
@classmethod / @staticmethodIncomplete
API style@wraps(func) on wrapper
Decorator rewrite neededNo
DependenciesNone (single file)
inspect.signature()Correct (compiled)
inspect.getfullargspec()Varies by version
help() outputCorrect
Error messagesImmediate (at wrapper)
@classmethod / @staticmethodIncomplete
API style@decorator on caller
Decorator rewrite neededYes
DependenciesC extension + fallback
inspect.signature()Correct (proxy)
inspect.getfullargspec()Correct
help() outputCorrect
Error messagesImmediate (at proxy)
@classmethod / @staticmethodFull support
API style@wrapt.decorator on caller
Decorator rewrite neededYes
DependenciesPure Python
inspect.signature()Correct (compiled)
inspect.getfullargspec()Correct
help() outputCorrect
Error messagesImmediate (at wrapper)
@classmethod / @staticmethodIncomplete
API style@wraps(func) on wrapper
Decorator rewrite neededNo
Note

"Incomplete" for @classmethod/@staticmethod means the tool does not handle the descriptor protocol automatically. You may need additional code or specific stacking order to make the decorator work with class methods and static methods. wrapt is the only tool that handles this transparently through its instance parameter.

Decision Guide

The right tool depends on who will use the decorator and what correctness guarantees you need.

Use functools.wraps when you are writing decorators for your own application code, you control both the decorator and the functions it decorates, and you do not need bytecode-level signature accuracy. This covers the vast majority of real-world decorator usage. Zero dependencies, one line of code, and works with help(), inspect.signature(), and IDE tooltips.

Use boltons.funcutils.wraps when you want compiled-signature accuracy without rewriting your existing decorators. The API is identical to functools.wraps, so migration is a one-line import change. Good for projects that already use boltons or want minimal disruption.

Use the decorator module when you want compiled-signature accuracy with zero dependencies and are willing to adopt its closure-less decorator style. The single-file design makes it easy to vendor into projects that cannot add external dependencies. It has been stable since 2004 and is used by IPython, scipy, and other established projects.

Use wrapt when your decorator must work transparently with every callable type in Python: functions, instance methods, class methods, static methods, and classes. It is the only option that handles the descriptor protocol correctly without additional code. It is the right choice for library authors who do not control the code their decorators will be applied to, and for instrumentation or monkey-patching tools where correctness is non-negotiable.

Pro Tip

If you are undecided, start with functools.wraps. It is the standard, it is free, and it solves the problem for the introspection tools that developers interact with daily (help(), IDE tooltips, inspect.signature()). Upgrade to a third-party tool only when you encounter a concrete case where bytecode-level accuracy matters, such as a framework that dispatches based on argument counts or a testing tool that reads getfullargspec().

Key Takeaways

  1. functools.wraps preserves metadata, not the compiled signature. It copies attributes and adds __wrapped__, which inspect.signature() follows. The wrapper's actual bytecode signature remains (*args, **kwargs). This is sufficient for help(), IDE tooltips, and documentation generators, but not for tools that read bytecode directly or frameworks that dispatch based on argument inspection.
  2. The decorator module generates a new function that enforces the original's parameter constraints. It uses a closure-less API style that passes the original function as the first argument. Since version 5.0 it leverages the Signature object internally for cleaner tracebacks. Its closure-less API requires rewriting existing decorators, but the result is cleaner and the module has no dependencies.
  3. wrapt uses transparent object proxies and is the only tool that handles every callable type. The instance parameter in its wrapper function signature enables correct behavior with instance methods, class methods, static methods, and classes. Its C extension provides near-zero overhead. It is the safest choice for library code and instrumentation tools.
  4. boltons.funcutils.wraps is a drop-in replacement for functools.wraps with compiled-signature accuracy. It preserves the same API, so existing decorators need only an import change. It also provides FunctionBuilder for advanced function construction and an injected parameter for decorators that modify argument lists.
  5. Choose based on your audience. Application code that you control: functools.wraps. Library code with a no-dependency requirement: the decorator module. Library code that must be transparent with all callable types: wrapt. Existing functools.wraps decorators that need an upgrade: boltons.

Signature preservation is not a single binary property. It is a spectrum. functools.wraps sits at one end: fast, free, and sufficient for the tools developers interact with daily. wrapt sits at the other: thorough, correct at every introspection level, and transparent with every callable type in the language. The decorator module and boltons occupy the middle ground, providing compiled-signature accuracy without the full proxy machinery. Knowing where each tool sits on that spectrum lets you choose the right level of correctness for each project.

How to Choose the Right Signature Preservation Tool

  1. Identify your decorator's audience. Determine whether the decorator will be used in application code you control or in a library consumed by external users. Application code that you control has different correctness requirements than library code applied to code you do not control.
  2. Check whether bytecode-level accuracy matters. Test whether any framework, testing tool, or dispatch mechanism in your project reads inspect.getfullargspec() or the function's __code__ object directly. If everything in your stack uses inspect.signature(), bytecode-level accuracy is not required.
  3. Evaluate callable type requirements. Determine whether your decorator needs to work with regular functions only, or also with instance methods, class methods, static methods, and classes. If it must handle all callable types transparently, wrapt is the only tool that does this without additional code.
  4. Assess dependency tolerance. Decide whether your project can accept a third-party dependency. functools.wraps has zero dependencies. The decorator module is a single file with no dependencies. boltons is a larger utility library. wrapt includes a C extension.
  5. Select the tool that matches your requirements. Use functools.wraps for application code with no special requirements. Use the decorator module or boltons for compiled-signature accuracy without rewriting decorators. Use wrapt for library code that must be transparent with every callable type.

Frequently Asked Questions

Does functools.wraps preserve the function signature at the bytecode level?

No. functools.wraps copies metadata attributes like __name__ and __doc__ and adds a __wrapped__ reference, but the wrapper's actual bytecode signature remains (*args, **kwargs). The correct signature is only visible because inspect.signature() follows the __wrapped__ chain. Tools that read bytecode or use inspect.getfullargspec() without following __wrapped__ will still see the generic signature.

What does the decorator module do differently from functools.wraps?

The decorator module by Michele Simionato generates a new function object that enforces the original function's exact parameter constraints at call time. Invalid arguments produce clear TypeError messages before the original function runs. Since version 5.0, it uses Python's Signature object internally rather than exec tricks, producing cleaner tracebacks. It also uses a closure-less style that eliminates one level of nesting.

When should I use wrapt instead of functools.wraps?

Use wrapt when your decorator needs to work transparently with functions, instance methods, class methods, static methods, and classes. wrapt creates transparent object proxies with a C extension for performance and handles the Python descriptor protocol correctly. It is the safest choice for library authors whose decorators will be applied to code they do not control.

What is the error message difference between functools.wraps and the decorator module?

With functools.wraps, passing an invalid keyword argument produces a TypeError referencing the original function's name (because the call passes through to the real function). With the decorator module, the error is caught before the original function runs because the generated wrapper has the original parameter list compiled in. The error message directly reflects the correct parameter names.

Which signature preservation tool should I use for application code?

For application code where you control both the decorators and the decorated functions, functools.wraps is sufficient. It ships with Python, has zero dependencies, adds one line of code, and works correctly with inspect.signature(), help(), and IDE tooltips. Reserve the decorator module, wrapt, or boltons for library code or situations where bytecode-level signature accuracy matters.