python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.46k stars 2.83k forks source link

mypy on python 3.9 does not support @staticmethod in Protocols #15257

Open graingert opened 1 year ago

graingert commented 1 year ago

Bug Report

(A clear and concise description of what the bug is.)

To Reproduce

from __future__ import annotations

from typing import Protocol

class Demo(Protocol):
    @staticmethod
    def demo(v: int) -> int:
        pass

def returns_one(v: int) -> int:
    return 1

class Inheriting(Demo):
    demo = staticmethod(returns_one)

https://mypy-play.net/?mypy=latest&python=3.9&gist=1438683686842d8a03bfbc32646c7952&flags=strict

Expected Behavior

Success: no issues found in 1 source file

Actual Behavior

main.py:15: error: Incompatible types in assignment (expression has type "staticmethod[[int], int]", base class "Demo" defined the type as "Callable[[int], int]") [assignment]

Your Environment

graingert commented 1 year ago

this passes on 3.10, 3.11 and 3.12

https://mypy-play.net/?mypy=latest&python=3.10&gist=1438683686842d8a03bfbc32646c7952&flags=strict https://mypy-play.net/?mypy=latest&python=3.11&gist=1438683686842d8a03bfbc32646c7952&flags=strict https://mypy-play.net/?mypy=latest&python=3.12&gist=1438683686842d8a03bfbc32646c7952&flags=strict

graingert commented 1 year ago

note that using @staticmethod does work:

from __future__ import annotations

from typing import Protocol

class Demo(Protocol):
    @staticmethod
    def demo(v: int) -> int:
        pass

def returns_one(v: int) -> int:
    return 1

class Inheriting(Demo):
    @staticmethod
    def demo(v: int) -> int:
        return 1
ikonst commented 1 year ago

Likely "culprit" is https://github.com/python/typeshed/pull/9771: https://github.com/python/typeshed/blob/274f449edce829201ef63da2e566497b28d3d97c/stdlib/builtins.pyi#L126-L131 specifically

def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ...

which makes ... staticmethods overridable with regular callables?

@AlexWaygood tl;dr on why def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ... is not applicable to <3.10?

graingert commented 1 year ago

Python 3.9 doesn't have __call__

Python 3.9.16 (main, Dec  7 2022, 01:12:08) 
[GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> staticmethod(lambda: None).__call__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'staticmethod' object has no attribute '__call__'

but that shouldn't be considered by mypy because it's called via .__get__(...).__call__

AlexWaygood commented 1 year ago

@AlexWaygood tl;dr on why def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ... is not applicable to <3.10?

Staticmethods aren't callable on Python 3.9! The __call__ method was added in 3.10. If you do the following...

x = staticmethod(lambda: 42)

class Foo:
    @staticmethod
    def bar(): return 42

y = Foo.__dict__['bar']

... then you'll find that both x and y are callable on Python 3.10, but not on Python 3.9

ikonst commented 1 year ago

FWIW I also don't understand how the presence of __call__ makes it work.

AlexWaygood commented 1 year ago

note that using @staticmethod does work:

from __future__ import annotations

from typing import Protocol

class Demo(Protocol):
    @staticmethod
    def demo(v: int) -> int:
        pass

def returns_one(v: int) -> int:
    return 1

class Inheriting(Demo):
    @staticmethod
    def demo(v: int) -> int:
        return 1

I think this bug is just a specific manifestation of #4574. Looks like the same bug to me.

ikonst commented 1 year ago

Still unclear to me why it's "fixed" with the addition of __call__.

AlexWaygood commented 1 year ago

Still unclear to me why it's "fixed" with the addition of __call__.

I'd imagine it's because mypy applies some special-casing to methods decorated with @staticmethod that means that it considers those methods to be callable, even though that's not what the stubs say, and even though it's strictly true on Python <3.10 (the object returned by staticmethod.__get__ is callable on Python 3.9, but staticmethod instances themselves aren't). This special-casing probably predates the point even mypy added generalised support for descriptors, so it's possible it could be partly or fully removed now in 2023. I believe mypy only applies this special-casing to methods decorated with @staticmethod, not with direct calls to staticmethod, hence the bug.

As such, because it (incorrectly) sees methods decorated with @staticmethod as callable (due to the special-casing), it emits an error when a staticmethod in the superclass is overridden by something that doesn't have a __call__ method in the subclass.