How to Access the Original Function from a Decorated Function Using __wrapped__

Every decorator in Python replaces a function with a wrapper. That wrapper sits between the caller and the original function, adding behavior on either side. But there are situations—testing, debugging, cache bypass, introspection—where you need to reach through the wrapper and call or inspect the original function directly. The __wrapped__ attribute is the standard mechanism for doing this, and it is available on any function whose decorator used functools.wraps.

Where __wrapped__ Comes From

The __wrapped__ attribute does not appear by magic. It is created by functools.wraps (or its underlying implementation, functools.update_wrapper) when a decorator is defined. Here is the minimal pattern:

import functools

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

@my_decorator
def add(a, b):
    """Add two numbers."""
    return a + b

# __wrapped__ is a direct reference to the original add function
print(add.__wrapped__)
# <function add at 0x...>

print(add.__wrapped__ is not add)  # True
print(add.__wrapped__(3, 4))       # 7 (no "Calling add" printed)

When functools.wraps(func) runs, it copies metadata from func to wrapper and then sets wrapper.__wrapped__ = func. That single attribute assignment is what makes the original function accessible after decoration. Without functools.wraps, no __wrapped__ attribute exists on the wrapper, and the original function is reachable only through the closure's internal state.

Note

__wrapped__ was added to functools.update_wrapper in Python 3.2. If you are working with code that targets older Python versions (which is increasingly rare), this attribute will not be present even if the decorator uses functools.wraps.

Calling the Original Function Directly

The simplest use of __wrapped__ is calling the original function without triggering any decorator behavior. This is useful when the decorator adds side effects—logging, authentication checks, rate limiting, timing—that you want to skip in a specific context:

import functools
import time

def slow_down(func):
    """Decorator that adds a 2-second delay before each call."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        time.sleep(2)
        return func(*args, **kwargs)
    return wrapper

@slow_down
def fetch_config(key):
    """Look up a configuration value."""
    config = {"debug": True, "timeout": 30, "retries": 3}
    return config.get(key)

# Normal call: waits 2 seconds
result = fetch_config("timeout")  # 30 (after 2s delay)

# Direct call via __wrapped__: no delay
result = fetch_config.__wrapped__("timeout")  # 30 (instant)

The __wrapped__ attribute returns the exact same function object that was passed into the decorator. It is not a copy—it is the original. Any state that the original function carries (default arguments, closures, attributes) is fully intact.

Bypassing functools.lru_cache

functools.lru_cache is itself a decorator, and it follows the same convention: it exposes the original function through __wrapped__. This lets you call the uncached version of a function when you need a guaranteed fresh result:

import functools

@functools.lru_cache(maxsize=128)
def expensive_query(user_id):
    """Simulate an expensive database lookup."""
    print(f"  [DB] Querying user {user_id}...")
    return {"id": user_id, "name": f"User {user_id}"}

# First call hits the database
result1 = expensive_query(42)
#   [DB] Querying user 42...

# Second call returns cached result (no DB hit)
result2 = expensive_query(42)
# (no output -- served from cache)

# Bypass the cache entirely via __wrapped__
result3 = expensive_query.__wrapped__(42)
#   [DB] Querying user 42...

# You can also clear the cache and rewrap with different settings:
expensive_query.cache_clear()
print(expensive_query.cache_info())
# CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)

This pattern is useful during development when you are iterating on the function's logic and need to see fresh results every time, or in test suites where cached results from one test could contaminate another.

Pro Tip

lru_cache also provides cache_clear() to flush the cache and cache_info() to check hit/miss statistics. Use __wrapped__ when you want a single uncached call without affecting the cache state. Use cache_clear() when you want to reset the cache entirely.

Navigating Stacked Decorators with inspect.unwrap

When multiple decorators are stacked on a single function and each one uses functools.wraps, each layer adds its own __wrapped__ attribute pointing to the function it received. This creates a chain:

import functools

def decorator_a(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("A: before")
        result = func(*args, **kwargs)
        print("A: after")
        return result
    return wrapper

def decorator_b(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("B: before")
        result = func(*args, **kwargs)
        print("B: after")
        return result
    return wrapper

@decorator_a
@decorator_b
def greet(name):
    """Say hello."""
    return f"Hello, {name}"

# The __wrapped__ chain:
# greet -> decorator_a's wrapper
# greet.__wrapped__ -> decorator_b's wrapper
# greet.__wrapped__.__wrapped__ -> original greet

print(greet.__wrapped__.__name__)              # greet
print(greet.__wrapped__.__wrapped__.__name__)  # greet
print(greet.__wrapped__.__wrapped__("Ada"))    # Hello, Ada

Manually traversing __wrapped__ through multiple layers is tedious and fragile. The inspect module provides unwrap() to follow the entire chain automatically:

import inspect

# Follow the entire chain to the original function
original = inspect.unwrap(greet)
print(original.__name__)    # greet
print(original("Ada"))      # Hello, Ada (no decorator output)

# Verify it's the innermost function
print(original is greet.__wrapped__.__wrapped__)  # True

inspect.unwrap() handles chains of any depth. It also protects against infinite loops: if it detects a cycle in the __wrapped__ chain (where a function's __wrapped__ eventually points back to itself), it raises a ValueError instead of looping forever.

Stopping Early with the stop Callback

inspect.unwrap() accepts an optional stop parameter—a callback that receives each object in the chain and returns True to stop unwrapping at that point. This is useful when you want to stop at a specific decorator layer rather than going all the way to the innermost function:

import functools
import inspect

def auth_required(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # authentication logic here
        return func(*args, **kwargs)
    wrapper._is_auth_decorator = True
    return wrapper

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

@auth_required
@log_calls
def delete_account(user_id):
    """Permanently delete a user account."""
    return f"Deleted {user_id}"

# Stop unwrapping at the auth layer (don't go deeper)
auth_layer = inspect.unwrap(
    delete_account,
    stop=lambda f: getattr(f, '_is_auth_decorator', False)
)
print(auth_layer._is_auth_decorator)  # True

# Go all the way to the original
original = inspect.unwrap(delete_account)
print(original.__name__)  # delete_account

The stop callback pattern is used internally by inspect.signature(), which stops unwrapping when it finds a function with a __signature__ attribute. This allows decorators that intentionally modify the function's signature to advertise their own parameter list rather than the original's.

Using __wrapped__ in Unit Tests

The primary real-world use case for __wrapped__ is testing decorated functions in isolation. When a decorator adds authentication checks, database transactions, network calls, or other side effects, you often want to test the function's core logic without those layers interfering:

import functools

def require_admin(func):
    """Decorator that checks admin privileges before execution."""
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_admin"):
            raise PermissionError("Admin access required")
        return func(user, *args, **kwargs)
    return wrapper

@require_admin
def reset_database(user, confirm=False):
    """Reset the entire database to its default state."""
    if not confirm:
        return "Reset cancelled: confirmation required"
    return "Database reset complete"


# --- Unit tests ---
import unittest

class TestResetDatabase(unittest.TestCase):

    def test_requires_confirmation(self):
        """Test the function logic without the admin check."""
        # Bypass the decorator entirely
        result = reset_database.__wrapped__(
            {"id": 1, "is_admin": False},
            confirm=False,
        )
        self.assertEqual(result, "Reset cancelled: confirmation required")

    def test_confirms_reset(self):
        """Test successful reset without needing admin credentials."""
        result = reset_database.__wrapped__(
            {"id": 1},
            confirm=True,
        )
        self.assertEqual(result, "Database reset complete")

    def test_admin_check_rejects_non_admin(self):
        """Test the decorator itself (integration test)."""
        with self.assertRaises(PermissionError):
            reset_database({"id": 2, "is_admin": False})

The first two tests exercise the function's logic in isolation by calling __wrapped__ directly. The third test is an integration test that verifies the decorator works correctly. This separation gives you fast, focused unit tests for the function's behavior and separate tests for the decorator's behavior, without one depending on the other.

When __wrapped__ Is Missing

Not every decorator uses functools.wraps. Third-party libraries, legacy code, or hand-rolled decorators may omit it entirely. In those cases, there is no __wrapped__ attribute, and you need alternative approaches to reach the original function.

Check Before You Access

Always use hasattr or getattr before accessing __wrapped__ to avoid AttributeError on decorators that did not use functools.wraps:

def safe_unwrap(func):
    """Return the original function if __wrapped__ exists, otherwise func."""
    return getattr(func, '__wrapped__', func)

# Works with functools.wraps decorators
original = safe_unwrap(reset_database)
print(original.__name__)  # reset_database

# Also works safely with undecorated functions
def plain_function():
    pass

result = safe_unwrap(plain_function)
print(result is plain_function)  # True

Reaching Through the Closure

When __wrapped__ is not available, the original function still exists inside the wrapper's closure. Every Python function has a __closure__ attribute that holds references to variables from the enclosing scope. You can inspect it to find the original function, though this approach is fragile and should be treated as a last resort:

# A decorator WITHOUT functools.wraps
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def multiply(a, b):
    """Multiply two numbers."""
    return a * b

# No __wrapped__ attribute exists
print(hasattr(multiply, '__wrapped__'))  # False

# The original function is captured in the closure
if multiply.__closure__:
    for cell in multiply.__closure__:
        content = cell.cell_contents
        if callable(content):
            print(content.__name__)  # multiply
            print(content(3, 4))     # 12
Warning

Accessing closure cells is an implementation detail of CPython. The order and contents of __closure__ are not guaranteed by the language specification, and they can change between Python versions or alternative interpreters. Always prefer __wrapped__ when it is available.

Patching a Broken Decorator

The cleanest solution when a third-party decorator does not use functools.wraps is to wrap the decorator itself to add the missing behavior:

import functools

def fix_decorator(broken_decorator):
    """Wrap a decorator that forgot functools.wraps."""
    @functools.wraps(broken_decorator)
    def fixed(func):
        result = broken_decorator(func)
        functools.update_wrapper(result, func)
        return result
    return fixed

# The broken third-party decorator
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# Create a fixed version
good_decorator = fix_decorator(bad_decorator)

@good_decorator
def divide(a, b):
    """Divide a by b."""
    return a / b

# Now __wrapped__ is available
print(divide.__name__)              # divide
print(divide.__doc__)               # Divide a by b.
print(divide.__wrapped__(10, 2))    # 5.0

This approach is reusable: fix_decorator can be applied to any decorator that omits functools.wraps, producing a corrected version that exposes __wrapped__ and preserves metadata.

Key Takeaways

  1. __wrapped__ is created by functools.wraps: When a decorator applies @functools.wraps(func) to its inner wrapper, the wrapper gets a __wrapped__ attribute that points directly to the original function. Without functools.wraps, this attribute does not exist.
  2. Call __wrapped__ to bypass the decorator: my_function.__wrapped__(args) invokes the original function without any decorator behavior. This works for logging, timing, authentication, rate limiting, and any other decorator that adds behavior before or after the original call.
  3. lru_cache follows the same convention: functools.lru_cache exposes the original function through __wrapped__, letting you call the uncached version directly. Use this for testing or when you need a fresh result without clearing the cache.
  4. Use inspect.unwrap() for stacked decorators: It follows the __wrapped__ chain through every layer of decoration to reach the innermost original function. The optional stop callback lets you halt at a specific layer instead of going all the way through.
  5. inspect.signature() follows __wrapped__ automatically: It reports the original function's parameter list even when the function is decorated, because it internally uses inspect.unwrap(). Pass follow_wrapped=False to see the wrapper's own *args, **kwargs signature.
  6. Always check before accessing: Use getattr(func, '__wrapped__', func) to safely handle both decorated and undecorated functions. Accessing __wrapped__ directly on a function without it raises AttributeError.
  7. Closure inspection is a fallback, not a strategy: When __wrapped__ is missing, you can find the original function in the wrapper's __closure__ cells, but this is fragile and CPython-specific. The better fix is to patch the decorator to add functools.wraps.

The __wrapped__ attribute is a small piece of Python's decorator infrastructure that solves a big practical problem. It turns opaque wrappers into transparent layers that you can peel back when you need to. The only requirement is that every decorator in the chain uses functools.wraps—one line of code per decorator that pays dividends every time someone needs to test, debug, or introspect the decorated function.