Skip to content

singledispatch.register fails for lru_cache decorated functions #132064

Description

@lczyk

Bug report

Function decorated with both @singledispatch.register and @lru_chache fails with a cryptic error message.

EDIT: In python 3.14a6. It works in python 3.9~3.13.

Bug description:

import sys
from functools import lru_cache, singledispatch, wraps
from typing import Any, Callable

def wrapped_lru_cache[T: Callable](maxsize: int = 1024) -> Callable[[T], T]:
    def decorator(func: T) -> T:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            return lru_cache(maxsize=maxsize)(func)(*args, **kwargs)
        return wrapper  # type: ignore
    return decorator

if len(sys.argv) > 1 and sys.argv[1] == "--broken-in-py314":
    # does not work in python 3.14+
    lru = lru_cache
else:
    # workaround
    lru = wrapped_lru_cache

@singledispatch
def fun(x: str | tuple) -> int:
    raise TypeError(f"Input must be str or tuple not a {type(x)}")

@fun.register
@lru(maxsize=1024)  # <- this
def _(x: str) -> int:
    return len(x)

@fun.register
def _(x: tuple) -> int:
    sum = 0
    for e in x:
        sum += fun(e)
    return sum

if __name__ == "__main__":
    print(f"{fun("hello")=}")
    print(f"{fun(("hello", "sailor"))=}")

python ./singledispatch_mwe.py, but python ./singledispatch_mwe.py --broken-in-py314 produces the following error:

Traceback (most recent call last):
  File ".../singledispatch_mwe.py", line 25, in <module>
    @fun.register
     ^^^^^^^^^^^^
  File "~/.local/share/uv/python/cpython-3.14.0a6-macos-aarch64-none/lib/python3.14/functools.py", line 963, in register
    argname, cls = next(iter(get_type_hints(func, format=Format.FORWARDREF).items()))

In python 3.9~3.13 both of the above work.

CPython versions tested on:

3.9~3.14

Operating systems tested on:

macOS

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Apr 4, 2025
  2. lczyk commented on Apr 4, 2025

    @lczyk
    Author

    See here (PR to my own package) for implemented workaround, tested with tox for py39~py314 on ubuntu, macOS and windows (-latest).

  3. lczyk commented on Apr 4, 2025

    @lczyk
    Author

    Also, i'd say there are at least two things wrong here:

    1. The error message is very cryptic (i'm still not 100% sure what exactly is wrong)
    2. Either lru_cache is not wrapping the function properly or .register is not picking up the type hints correctly (maybe a fallback to the old behaviour?)
  4. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Apr 4, 2025
  5. picnixz commented on Apr 4, 2025

    @picnixz
    Member

    @rhettinger Was singledispatch designed to work on LRU-decorated functions?

  6. lczyk commented on Apr 4, 2025

    @lczyk
    Author

    for reference, it worked as far back as i've tested, aka down to 3.9. i do realise it might not have been intended to work though, and so this is not necessarily a feature.

  7. picnixz commented on Apr 4, 2025

    @picnixz
    Member

    Oh so you mean that it's a 3.14+ issue?

  8. lczyk commented on Apr 4, 2025

    @lczyk
    Author

    yes! sorry, reading back i was not clear about that 😅 (added an EDIT to the original report to make it clearer.)

  9. picnixz commented on Apr 4, 2025

    @picnixz
    Member

    As it uses get_type_hints(func, format=Format.FORWARDREF), I'm asking first @JelleZijlstra. I can have a look tomorrow though

  10. rhettinger commented on Apr 4, 2025

    @rhettinger
    Contributor

    functools.lru_cache copies essential attributes from the wrapped function with functools.update_wrapper. Its current default tuple of attributes is WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__', '__annotate__', '__type_params__'). That tuple was last updated by @JelleZijlstra in 2024. Perhaps it should include __annotations__ as well. Also contact @ambv to verify that he expects this to work.

  11. JelleZijlstra commented on Apr 5, 2025

    @JelleZijlstra
    Member

    This is indeed a consequence of the implementation of PEP 649/PEP 749, specifically this section: https://peps.python.org/pep-0749/#wrappers-that-provide-annotations .

    It is fixable in _functoolsmodule.c by adding an entry to lru_cache_getsetlist for __annotations__, providing both a getter and a setter that mirror those for the __annotations__ attribute on functions. The setter should unconditionally set __annotations__ in the __dict__; the getter should return __annotations__ from the __dict__ if available, else try to call __annotate__ if it's in the dict, and cache the result. Anyone interested in implementing that?

  12. rhettinger commented on Apr 6, 2025

    @rhettinger
    Contributor

    @JelleZijlstra, The lru_cache has a pure python implementation as well. The wrapping of both the pure python version and the C version is done in functools.py with update_wrapper. So, I don't think a C edit is a complete or correct solution.

    Can this be fixed for the general case by expanding WRAPPER_ASSIGNMENTS to include __annotations__? The guidance says, "The annotate and annotations attributes should both be supplied with semantics consistent to those of the wrapped object." So it looks to me like __annotations__ should be added back to WRAPPER_ASSIGNMENTS:

    diff --git a/Lib/functools.py b/Lib/functools.py
    index 714070c6ac9..489b3e1071c 100644
    --- a/Lib/functools.py
    +++ b/Lib/functools.py
    @@ -30,7 +30,7 @@
     # wrapper functions that can handle naive introspection
     
     WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
    -                       '__annotate__', '__type_params__')
    +                       '__annotate__', '__annotations__', '__type_params__')
     WRAPPER_UPDATES = ('__dict__',)
     def update_wrapper(wrapper,
                        wrapped,

    Looking at the lru_cache wrapping code in pure python, I don't see any part of the current issue that is specific to the lru_cache. AFAICT, this would happen to any wrapped function.

  13. JelleZijlstra commented on Apr 6, 2025

    @JelleZijlstra
    Member

    No, adding __annotations__ to WRAPPER_ASSIGNMENTS would be incorrect because it would immediately force evaluation of the annotations of any wrapped function.

  14. JelleZijlstra commented on Apr 6, 2025

    @JelleZijlstra
    Member

    The Python implementation of lru_cache already works on 3.14, so it doesn't need any changes to fix this issue. (To test, take the OP's script, comment out the lines in functools.py that import the C implementation, and run the script.)

  15. rhettinger commented on Apr 6, 2025

    @rhettinger
    Contributor

    Another possibility is for singledispatch.register to follow the suggestion in the docs for update_wrapper, "To allow access to the original function for introspection and other purposes (e.g. bypassing a caching decorator such as lru_cache(), this function automatically adds a wrapped attribute to the wrapper that refers to the function being wrapped."

    Presumably, lru_cache isn't the only decorator that was broken by the edit to WRAPPER_ASSIGNMENTS.

  16. JelleZijlstra commented on Apr 7, 2025

    @JelleZijlstra
    Member

    I found a better fix for the original issue reported here: #132195. This makes annotationlib use the __annotate__ function if present, even if __annotations__ is not also present. This should help not only @lru_cache, but any other decorator that uses update_wrapper on a wrapper that is not a Python function.

    With that fix, it's still the case though that lru_cache-decorated functions don't have a direct __annotations__ attribute when the C version is used:

    >>> @functools.lru_cache
    ... def f(x: int): pass
    ... 
    >>> f.__annotations__
    Traceback (most recent call last):
      File "<python-input-2>", line 1, in <module>
        f.__annotations__
    AttributeError: 'functools._lru_cache_wrapper' object has no attribute '__annotations__'. Did you mean: '__annotate__'?
    >>> f.__wrapped__.__annotations__
    {'x': <class 'int'>}
    >>> f.__annotate__(1)
    {'x': <class 'int'>}
    

    This could impact users who (against recommendations) access __annotations__ directly instead of using annotationlib.get_annotations or (in previous Python versions) inspect.get_annotations. To fix it, we could add a dedicated __annotations__ descriptor to the C version of functools._lru_cache_wrapper. Is that worth it? I'm not sure.

  17. added a commit that references this issue on Apr 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.14bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions