Skip to content

typing._tp_cache runs the wrapped function twice when it raises TypeError #154115

Description

@fedonman

Bug report

While looking at the typing module I noticed that _tp_cache can run the function it wraps twice.

_tp_cache wraps __getitem__ and similar methods with an lru_cache. Some of the arguments passed to those methods can be unhashable, so it calls the cached version inside a try and falls back to calling the original function when a TypeError shows up, on the assumption that the TypeError came from trying to hash an unhashable argument.

The problem is that the wrapped function can also raise TypeError on its own, for reasons that have nothing to do with hashing. When that happens with hashable arguments, the function runs once inside the cached call and raises, the except TypeError catches it, and then the fallback runs the same function a second time before the error finally propagates.

A small reproduction:

import typing

calls = []

@typing._tp_cache
def f(x):
    calls.append(x)
    raise TypeError("unrelated to hashability")

try:
    f("value")
except TypeError:
    pass

print(calls)  # ['value', 'value'] -- f ran twice

It is also reachable through the public API. Any subscription whose cached __getitem__ validates its arguments and raises TypeError for hashable arguments hits this:

import typing

typing.List[int, str]               # too many arguments
typing.Optional[int, str]           # requires a single type
typing.Callable[[int], (int, str)]  # result must be a type

For typing.List[int, str] the whole __getitem__ body runs twice, including the per-argument _type_check calls, before the TypeError is raised.

The relevant code is in Lib/typing.py:

def inner(*args, **kwds):
    try:
        return _caches[func](*args, **kwds)
    except TypeError:
        pass  # All real errors (not unhashable args) are raised below.
    return func(*args, **kwds)

The fix is to only fall back when the arguments are actually unhashable, and to re-raise otherwise so the function is not called a second time.

Reproduced on current main.

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions