Skip to content

Using function_wrapper evaluates the wrapped function's annotations in Python 3.14, which can cause exceptions #331

@contrast-jproberts

Description

@contrast-jproberts

Bug Report

I tried to use the wrapt.function_wrapper on google.genai.models.Models.edit_image and saw a TypeError exception. Here's a minimal reproducer, since google-genai is a large package.

# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "wrapt>=2.1.2",
# ]
# ///
import wrapt


@wrapt.function_wrapper
def passthrough(wrapped, instance, args, kwargs):
    return wrapped(*args, **kwargs)


def f(*, a: list[int]) -> list[int]:
    return a


def list():
    return


f = passthrough(f)

Here's the output from running the script. This only reproduces with the C extension disabled.

> uv run -p 3.14 repro.py
Installed 1 package in 1ms
Traceback (most recent call last):
  File "/Users/jamesroberts/Projects/repro.py", line 23, in <module>
    f = passthrough(f)
  File "/Users/jamesroberts/.cache/uv/environments-v2/repro-737eeeff054c4226/lib/python3.14/site-packages/wrapt/wrappers.py", line 755, in __call__
    return self._self_wrapper(self.__wrapped__, self._self_instance, args, kwargs)
           ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jamesroberts/.cache/uv/environments-v2/repro-737eeeff054c4226/lib/python3.14/site-packages/wrapt/patches.py", line 166, in _wrapper
    return FunctionWrapper(target_wrapped, target_wrapper)
  File "/Users/jamesroberts/.cache/uv/environments-v2/repro-737eeeff054c4226/lib/python3.14/site-packages/wrapt/wrappers.py", line 984, in __init__
    super(FunctionWrapper, self).__init__(wrapped, None, wrapper, enabled, binding)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jamesroberts/.cache/uv/environments-v2/repro-737eeeff054c4226/lib/python3.14/site-packages/wrapt/wrappers.py", line 629, in __init__
    super(_FunctionWrapperBase, self).__init__(wrapped)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/Users/jamesroberts/.cache/uv/environments-v2/repro-737eeeff054c4226/lib/python3.14/site-packages/wrapt/wrappers.py", line 124, in __init__
    object.__setattr__(self, "__annotations__", wrapped.__annotations__)
                                                ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jamesroberts/Projects/repro.py", line 15, in __annotate__
    def f(*, a: list[int]) -> list[int]:
                ~~~~^^^^^
TypeError: 'function' object is not subscriptable

The script exits successfully in Python 3.13 and earlier.

Investigation

A big change in Python 3.14 is deferred evaluation of annotations. I found a variety of resources explaining this change, including the release notes, a HOWTO article, a new annotationslib library for supporting annotations, and a third party RealPython article. From the RealPython article, the section on data descriptors was useful: https://realpython.com/python-annotations/#annotations-as-data-descriptors . In it, they explain the default behavior in Python 3.14 when accessing __annotations__ is to call the object's __annotate__ attribute with the VALUE format, which evaluates annotations to runtime values, similar to the behavior in earlier Python versions. In the reproducer script, list is defined after f, but before the function_wrapper is applied. When the function_wrapper is called on f, f.__annotations__ resolves list to the local function and raises an error when evaluating the subscript operation.

Possible fix

It doesn't seem safe for wrapt to access the __annotations__ attribute in 3.14+. In the CPython PR that added this change, functools.wraps is updated to copy the wrapped callable's __annotate__ attribute onto the wrapper instance. I think applying the same approach here for Python 3.14+ would be an improvement. There's at least one known gap with that solution though: python/cpython#124342, and I'm not sure whether that approach would cause issues with staticmethods, classmethods or other callables that wrapt supports.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions