Skip to content

pyright regression: combining @classmethod and @retry throws error. #756

Open

Description

@mnakama

Combining @staticmethod and @retry decorators on a function breaks pyright type checking. It throws this error:

error: Argument missing for parameter "a" (reportCallIssue)

Apparently, it does not recognize that the cls parameter is automatically passed in when calling a @classmethod.

This is a regression, starting from f4a31e1 .

The latest commit that does not have this pyright warning is 930958f . The latest working release of tenacity is 9.1.4.

Steps to Reproduce

  1. Make a test.py file:
from tenacity import retry

@retry
async def some_func(a: int) -> None:
    pass

class Foo:
    @staticmethod
    @retry
    async def static_func(a: int) -> None: ...

    @classmethod
    @retry
    async def class_func(cls, a: int) -> None: ...

    @retry
    async def method_func(self, a: int) -> None: ...

    @classmethod
    @retry
    def sync_class_func(cls, a: int) -> None: ...

    @retry
    def sync_func(self, a: int) -> None: ...

async def test_async() -> None:
    c = Foo()

    # These all work fine.
    await some_func(1)
    await Foo.static_func(1)
    await c.method_func(1)
    c.sync_func(1)

    # pyright fails with `error: Argument missing for parameter "a" (reportCallIssue)`
    await Foo.class_func(1)

    # But if we use an instance instead of the class, it works fine.
    await c.class_func(1)

    # Same error with a sync function as with async.
    Foo.sync_class_func(1)
  1. Run pyright test.py

  2. Observe errors:

test.py:36:11 - error: Argument missing for parameter "a" (reportCallIssue)
test.py:42:5 - error: Argument missing for parameter "a" (reportCallIssue)
2 errors, 0 warnings, 0 informations

Activity

  1. devtechedge commented on Oct 14, 2026

    @devtechedge

    Thanks for the clean bisect to f4a31e1, it pointed straight at the _RetryDecorated return type.

    I reproduced it on main with mypy (the strict CI config) and pyright, and a standalone callable protocol fails the same way, so neither checker applies classmethod binding to a protocol object.

    Both checkers do still call the protocol's __get__, so my suggestion is to give retry an extra overload for callables with a first positional parameter that returns a sibling protocol, _RetryDecoratedMethod[S, P, R].

    Its __get__ has a self-typed overload that drops the first parameter when it is type[T], which makes Foo.method(1) and Foo().method(1) resolve for classmethods, sync and async.

    Instance methods also bind self precisely now, .retry, .statistics and .retry_with stay typed, and runtime behaviour is unchanged.

    The trade-off is that a plain function whose first parameter is annotated type[X] and is stored on a class would type-check as if bound, which looks rare to me.

    I have this locally with assert_type regression tests for classmethods, staticmethods, instance methods and plain functions, so I'm happy to open a PR if this direction works for you 🙂

  2. mnakama commented on Oct 15, 2026

    @mnakama
    Author

    neither checker applies classmethod binding to a protocol object.

    Interesting that someone is already working on a fix for pyright for this. I wonder if it's more appropriate to fix this upstream on pyright and mypy than to add a workaround to tenacity? I didn't realize it was an issue with the type checkers when I filed this issue, as I'm not well-versed in how they work 😅

    I'm happy to open a PR if this direction works for you

    I don't have any objection to it, but I'm not a maintainer for tenacity, so I can't officially answer whether your proposed workaround would be accepted. Your proposal is a bit beyond my technical comprehension 🙂

  3. devtechedge commented on Oct 15, 2026

    @devtechedge

    Good question! An upstream fix would be the cleanest long-term answer, but even if pyright and mypy change how they bind classmethod over protocol objects, it'll take a while to ship and for users to pick up those versions.

    Meanwhile tenacity users after 9.1.4 hit this today, and older checker versions would keep failing either way.

    The overload is small, purely typing-level with no runtime change, and can be dropped once the checkers handle it.

    Totally fair on the maintainer point, and thanks for the quick repro 🙂

    I'll open a PR with the regression tests so @jd can weigh in on the approach there, and I'll link any upstream pyright/mypy issue in the PR description.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions