Mypy Gives `Incompatible Definitions` Error When Dynamically Typed
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
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 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.
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