Typing mixed static/instance method overloads

stripe-python overloads methods based on them being called from static or instance context.

For example:

Account.delete("acc_123")
acc = Account.retrieve("acc_123")
acc.delete()

We use overloads with @staticmethod to type these (https://github.com/stripe/stripe-python/blob/master/stripe/api_resources/account.py#L3523-L3547).

pyright will start producing errors when @staticmethod annotation is inconsistently applied to overloads (Report the case where overloads use `@staticmethod` or `@classmethod` inconsistently · Issue #6592 · microsoft/pyright · GitHub). mypy has a similar check as well.

Is there a different supported approach to annotating methods like these? Is it possible to allow overloading based on the @staticmethod/@classmethod?

Out of curiosity, how is this functionality implemented? Normally, a method (even if it happens to be polymorphic) cannot be defined as both static and non-static. Is it done using a descriptor object? If so, that’s probably how it should be modeled in the stub.

Here’s a first cut at this approach:

**strong text**from typing import Any, Callable, Concatenate, Generic, ParamSpec, TypeVar, overload

S = TypeVar("S")
P = ParamSpec("P")
R = TypeVar("R")

class StaticMethod(Generic[P, R]):
    def __call__(self, sid: str, *args: P.args, **kwargs: P.kwargs) -> R:
        ...

class NonStaticMethod(Generic[P, R]):
    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
        ...

class StaticOrNot(Generic[S, P, R]):
    def __init__(self, cb: Callable[Concatenate[S, P], R]) -> None:
        ...

    @overload
    def __get__(self, object: None, owner: Any) -> StaticMethod[P, R]:
        ...

    @overload
    def __get__(self, object: S, owner: Any) -> NonStaticMethod[P, R]:
        ...

class Foo:
    @StaticOrNot
    def method(self, x: int, y: int) -> str:
        ...

Foo.method("", 1, 2)

f = Foo()
f.method(1, 2)
1 Like

It’s implemented via a decorator that conditionally forwards the call to a private @classmethod https://github.com/stripe/stripe-python/blob/master/stripe/util.py#L371

    @classmethod
    def _cls_delete(  # <------ static overload implementation
        cls, sid: str, **params: Unpack["Account.DeleteParams"]
    ) -> "Account":
      ...

    @overload
    @staticmethod
    def delete(
        sid: str, **params: Unpack["Account.DeleteParams"]
    ) -> "Account":
        ...

    @overload
    def delete(self, **params: Unpack["Account.DeleteParams"]) -> "Account":
        ...

    @class_method_variant("_cls_delete") # <------ decorator that performs the forwarding
    def delete(
        self, **params: Unpack["Account.DeleteParams"]
    ) -> "Account":
       ...

Thank you for your example, I’ll try to see if we can apply it.

I have never seen a similar API before so if you don’t mind explaining could you describe why you decided to implement these functions as both regular and static methods? While I can see that it gives you more flexibility, I don’t fully understand why that flexibility is desired.

At first the SDK had a static retrieve and an instance delete methods only.

acc = Account.retrieve("acc_123")  # GET api.stripe.com/v1/accounts/acc_123
acc.delete() # DELETE api.stripe.com/v1/accounts/acc_123

Unfortunately this required two HTTP calls where one was enough, so a static overload was added.
The new overload allowed operations to be performed in a single HTTP call.

Account.delete("acc_123") # DELETE api.stripe.com/v1/accounts/acc_123
1 Like

But why not name it e.g. retrieve_and_delete?

1 Like

If we were building the same API today we would’ve done it differently. The API in question has been in the hands of customers for years and not something we are willing to break.

1 Like

Okay, that’s fair. Hopefully you will find a solution – I don’t think the argument for adding something to the type system or to a specific type checker is very strong, given this history. :slight_smile:

1 Like

Yeah, I understand that what we do is unconventional. I was hoping we are missing some combination of supported type-hints that will work in this case.

@pakrym-stripe I am very sure that what @erictraut shared is the right answer. I also wrote type hints for similar situations in the past and they work like a charm.

1 Like