mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Mypy Gives `Incompatible Definitions` Error When Dynamically Typed

Open adam-grant-hendry opened this issue 3 years ago • 3 comments

Bug Report

Discussed in https://github.com/python/typing/discussions/1235

Originally posted by adam-grant-hendry August 4, 2022 Related to this SO question, I'm trying to understand why

but

I would have assumed since nothing is statically typed, that mypy would not produced any errors. In fact, the same code on the pyright playground shows no errors.

I though perhaps the error could be due to the potential for a keyword collision, which I gleaned from PEP 692. e.g. If I made the name of the 4th arguments the same, each could fail the same way if I tried to pass foo as a keyword argument:

>>> a = Foo()
>>> a.run(1, 2, 'baz', foo='bar')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: func() got multiple values for argument 'foo'

but if I keep the argument names different, then the above would work if I tried to call Bar.run(1, 2, 'baz', foo='bar'), but break for Foo.

However, I still would originally assume mypy would do nothing because nothing has been statically typed:

By default, mypy will not type check dynamically typed functions. This means that with a few exceptions, mypy will not report any errors with regular unannotated Python.

To Reproduce

Please see the mypy playground (and pyright playground) gists above.

Expected Behavior

mypy issues no errors.

Actual Behavior

mypy issues the error:

Definition of "run" in base class "Foo" is incompatible with definition in base class "Bar"

Your Environment

  • Mypy version used: mypy 0.971 (compiled: yes)
  • Mypy command-line flags: None
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.8.10, x64-bit
  • Operating system and version: Windows 10, version 20H2

adam-grant-hendry avatar Aug 08 '22 23:08 adam-grant-hendry

A lot of real world code breaks Liskov substitution principle by changing parameter names. This breaks LSP since arguments can be passed in via keyword. PEP 570 introduced positional-only parameters, which helps solve this problem, but Python 3.7 is still around and use of PEP 570 is not yet widespread. To avoid annoying false positives, mypy has heuristics for when it considers parameter names relevant and when it doesn't. My guess is adding kwargs probably flips some heuristic's decision (which is understandable, presence of **kwargs clearly indicates that you intend to pass arguments in via keyword). Although I'd need to look at code to be sure.

By default, mypy will not type check dynamically typed functions. This means that with a few exceptions, mypy will not report any errors with regular unannotated Python.

Incompatible super class definitions is one such exception, as class Bar(Parent): def run(self): ... will show you.

hauntsaninja avatar Aug 08 '22 23:08 hauntsaninja

@hauntsaninja Thanks for your feedback.

Ya, Liskov violation makes sense to me, but then I wasn't sure why the first example (different keyword arg names, but no **kwargs) still passes with mypy...?

I'm not sure which takes higher priority: dynamic typing or Liskov violation. I would have thought dynamic typing for backwards-compatibility/gradual typing.

If you're able to find the heuristic, please show me! I'd like to be able to explain the StackOverflow OP what's going on.

adam-grant-hendry avatar Aug 09 '22 00:08 adam-grant-hendry

Spent some time tracing it, logic is here: https://github.com/python/mypy/blob/0db44d24423a554e5813e3194e64e0a3a6e795fd/mypy/subtypes.py#L1335 https://github.com/python/mypy/blob/0db44d24423a554e5813e3194e64e0a3a6e795fd/mypy/types.py#L1792

hauntsaninja avatar Aug 09 '22 00:08 hauntsaninja