Skip to content

Poor interactions with staticmethod and overload/union #15015

Open
@ksunden

Description

@ksunden

Bug Report

Behavior changed in v1.2.0 regarding how staticmethods are handled when invoked by doing foo = staticmethod(bar) (i.e. not as the @decorator syntax)

A realworld example of this appeared in matplotlib, which is wrapping a third party method as a staticmethod of one of our classes (pyparsing)

Possibly related:

#14953 (most recent change to staticmethod handling)
#14902 (typeshed sync, mentions staticmethod
#7781 (original report of staticmethod behavior

A pure python example (and extension) is provided below.

To Reproduce

from typing import overload, Callable, Any
@overload
def foo(a: int) -> None:
    ...
    
@overload
def foo(a: str) -> None: ...

def foo(a) -> None:
    ...

def func_factory(spam) -> Callable[[], Any]| Callable[[int], Any]:
    return lambda: None
    
def qux(i: int) -> int:
    return i

class Foo():
    foo = staticmethod(foo)
    bar = staticmethod(func_factory(None))
    
    @overload
    @staticmethod
    def baz(a: str) -> str: ...
    @overload
    @staticmethod
    def baz(a: int) -> int: ...
    
    @staticmethod
    def baz(a):
        return a
        
    qux = staticmethod(qux)

reveal_type(foo)  # Correctly 'Overload(def (a: builtins.int), def (a: builtins.str))'
reveal_type(Foo.foo) # Should match above, actually 'def (*Any, **Any) -> Any' on v1.1.1 and "def (a: builtins.int)" on v1.2.0

reveal_type(func_factory(None)) # Correctly 'Union[def () -> Any, def (builtins.int) -> Any]'
reveal_type(Foo.bar) # Should match above, actually 'def (*Any, **Any) -> Any' on v1.1.1 and "def (*<nothing>, **<nothing>) -> Any" on v1.2.0

reveal_type(Foo.baz) # Correctly "Overload(def (a: builtins.str) -> builtins.str, def (a: builtins.int) -> builtins.int)"

reveal_type(qux)  # Correctly "def (i: builtins.int) -> builtins.int"
reveal_type(Foo.qux) # Should match above, actually 'def (*Any, **Any) -> builtins.int' on v1.1.1 and correct on v1.2.0

Foo.foo("a") # Errors on v1.2.0

https://mypy-play.net/?mypy=latest&python=3.10&gist=c50c0fb3a6c10fbea317a31453b40e29

Expected Behavior

I would expect that the overload (or union) would carry forward to the decorated static method.

Actual Behavior

Prior to v1.2.0 (tested explicitly with v1.1.1:
All staticmethods constructed without @decorator syntax would simply get def (*Any, **Any) -> Any, which was at least not an error, though was throwing away type information.

In the case of a single non-overloaded function defined outside of the class, it gets the return type but not the parameter type.

In v1.2.0:

In the case of an overloaded method defined outside of the class, only the first overloaded definition is kept, resulting in incorrect errors for alternate signatures.

In the case of a Union type (such as from a factory function), an error is thrown on the call to staticmethod, even if all branches of the union satisfy the type staticmethod expects.
This was the actual case I saw with matplotlib.

In the case of a single non-overloaded function or an overload defined in the class (with @staticmethod on all overload branches) v1.2.0 behaves as expected.

main.py:21: error: Argument 1 to "staticmethod" has incompatible type "Union[Callable[[], Any], Callable[[int], Any]]"; expected "Callable[[VarArg(<nothing>), KwArg(<nothing>)], Any]"  [arg-type]
main.py:36: note: Revealed type is "Overload(def (a: builtins.int), def (a: builtins.str))"
main.py:37: note: Revealed type is "def (a: builtins.int)"
main.py:39: note: Revealed type is "Union[def () -> Any, def (builtins.int) -> Any]"
main.py:40: note: Revealed type is "def (*<nothing>, **<nothing>) -> Any"
main.py:42: note: Revealed type is "Overload(def (a: builtins.str) -> builtins.str, def (a: builtins.int) -> builtins.int)"
main.py:44: note: Revealed type is "def (i: builtins.int) -> builtins.int"
main.py:45: note: Revealed type is "def (i: builtins.int) -> builtins.int"
main.py:47: error: Argument 1 has incompatible type "str"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: v1.2.0 (compared to v1.1.1)
  • Mypy command-line flags: N/A
  • Mypy configuration options from mypy.ini (and other config files):N/A
  • Python version used: 3.11

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongtopic-descriptorsProperties, class vs. instance attributes

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions