Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No errors on typed function internals if function definition itself is untyped; I'd argue that's probably incorrect behavior, but at the very least it's *super* confusing and *very* hard to diagnose when it happens #10895

Closed
posita opened this issue Jul 30, 2021 · 10 comments
Labels
bug mypy got something wrong

Comments

@posita
Copy link
Contributor

posita commented Jul 30, 2021

Issue description

This doesn't generate an error:

# test_case7.py
from typing import List

def untyped_func_def_with_typed_body():
    a: List[int] = [0, 1, 2]
    key: str = "asdf"
    a[key]  # <- No error, despite internals being typed

This does:

# test_case8.py
from typing import List

def typed_func_def_with_typed_body() -> None:
    a: List[int] = [0, 1, 2]
    key: str = "asdf"
    a[key]  # <- *Now* this generates an error

That maybe technically "documented", but that's really esoteric and completely unintuitive. I've spent hours chasing this down.


Archived original flailing to arrive at core problem

UPDATE 2: Okay, this just keeps getting weirder and weirder. Consider:

# test_case3.py

def func():
    a = [0, 1, 2]
    a["asdf"]  # <- This doesn't generate an error

b = [0, 1, 2]
b["asdf"]  # <- But this does?!

Runtime:

% mypy --config=/dev/null test_case3.py
/dev/null: No [mypy] section in config file
test_case3.py:8: error: No overload variant of "__getitem__" of "list" matches argument type "str"
test_case3.py:8: note: Possible overload variants:
test_case3.py:8: note:     def __getitem__(self, SupportsIndex) -> int
test_case3.py:8: note:     def __getitem__(self, slice) -> List[int]
Found 1 error in 1 file (checked 1 source file)

UPDATE 1: You don't need the @overloads. It still doesn't work without them. Here's the simplified case:

# test_case2.py
from typing import List, Union

class GetItemTest:
    def __getitem__(self, key: Union[int, slice]) -> Union[int, List[int]]:
        return list(range(10))[key]

def test_a():
    a = GetItemTest()
    a["asdf"]  # <- Why isn't this a typing error?

Original case (same effect):

# test_case1.py
from typing import List, Union, overload

class GetItemTest:
    @overload
    def __getitem__(self, key: int) -> int:
        ...
    @overload
    def __getitem__(self, key: slice) -> List[int]:
        ...
    def __getitem__(self, key: Union[int, slice]) -> Union[int, List[int]]:
        return list(range(10))[key]

def test_a():
    a = GetItemTest()
    a["asdf"]  # <- Why isn't this a typing error?

Runtime:

% python --version
Python 3.9.6
% pip list
Package           Version
----------------- --------
mypy              0.910
mypy-extensions   0.4.3
pip               21.2.1
setuptools        57.4.0
toml              0.10.2
typing-extensions 3.10.0.0
% mypy --version
mypy 0.910
% mypy --config=/dev/null test_case1.py
/dev/null: No [mypy] section in config file
Success: no issues found in 1 source file

I can't prove it, but I feel like this used to work. It seems pretty basic.

@posita posita added the bug mypy got something wrong label Jul 30, 2021
@posita
Copy link
Contributor Author

posita commented Jul 30, 2021

With --verbose if helpful:

% rm -fr .mypy_cache ; mypy --verbose --config=/dev/null test_case1.py
/dev/null: No [mypy] section in config file
LOG:  Could not load plugins snapshot: @plugins_snapshot.json

LOG:  Mypy Version:           0.910
LOG:  Config File:            /dev/null
LOG:  Configured Executable:  /private/tmp/test_case/.venv/bin/python
LOG:  Current Executable:     /private/tmp/test_case/.venv/bin/python
LOG:  Cache Dir:              .mypy_cache
LOG:  Compiled:               True
LOG:  Exclude:                
LOG:  Found source:           BuildSource(path='test_case1.py', module='test_case', has_text=False, base_dir='/private/tmp/test_case')
LOG:  Could not load cache for test_case: test_case.meta.json
LOG:  Metadata not found for test_case
LOG:  Parsing test_case1.py (test_case)
LOG:  Could not load cache for typing: typing.meta.json
LOG:  Metadata not found for typing
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/typing.pyi (typing)
LOG:  Could not load cache for builtins: builtins.meta.json
LOG:  Metadata not found for builtins
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/builtins.pyi (builtins)
LOG:  Could not load cache for collections: collections/__init__.meta.json
LOG:  Metadata not found for collections
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/collections/__init__.pyi (collections)
LOG:  Could not load cache for sys: sys.meta.json
LOG:  Metadata not found for sys
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/sys.pyi (sys)
LOG:  Could not load cache for abc: abc.meta.json
LOG:  Metadata not found for abc
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/abc.pyi (abc)
LOG:  Could not load cache for types: types.meta.json
LOG:  Metadata not found for types
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/types.pyi (types)
LOG:  Could not load cache for _typeshed: _typeshed/__init__.meta.json
LOG:  Metadata not found for _typeshed
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/_typeshed/__init__.pyi (_typeshed)
LOG:  Could not load cache for ast: ast.meta.json
LOG:  Metadata not found for ast
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/ast.pyi (ast)
LOG:  Could not load cache for io: io.meta.json
LOG:  Metadata not found for io
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/io.pyi (io)
LOG:  Could not load cache for typing_extensions: typing_extensions.meta.json
LOG:  Metadata not found for typing_extensions
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/typing_extensions.pyi (typing_extensions)
LOG:  Could not load cache for _collections_abc: _collections_abc.meta.json
LOG:  Metadata not found for _collections_abc
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/_collections_abc.pyi (_collections_abc)
LOG:  Could not load cache for importlib.abc: importlib/abc.meta.json
LOG:  Metadata not found for importlib.abc
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/importlib/abc.pyi (importlib.abc)
LOG:  Could not load cache for importlib.machinery: importlib/machinery.meta.json
LOG:  Metadata not found for importlib.machinery
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/importlib/machinery.pyi (importlib.machinery)
LOG:  Could not load cache for array: array.meta.json
LOG:  Metadata not found for array
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/array.pyi (array)
LOG:  Could not load cache for mmap: mmap.meta.json
LOG:  Metadata not found for mmap
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/mmap.pyi (mmap)
LOG:  Could not load cache for os: os/__init__.meta.json
LOG:  Metadata not found for os
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/os/__init__.pyi (os)
LOG:  Could not load cache for _ast: _ast.meta.json
LOG:  Metadata not found for _ast
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/_ast.pyi (_ast)
LOG:  Could not load cache for codecs: codecs.meta.json
LOG:  Metadata not found for codecs
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/codecs.pyi (codecs)
LOG:  Could not load cache for importlib: importlib/__init__.meta.json
LOG:  Metadata not found for importlib
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/importlib/__init__.pyi (importlib)
LOG:  Could not load cache for posix: posix.meta.json
LOG:  Metadata not found for posix
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/posix.pyi (posix)
LOG:  Could not load cache for subprocess: subprocess.meta.json
LOG:  Metadata not found for subprocess
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/subprocess.pyi (subprocess)
LOG:  Could not load cache for os.path: os/path.meta.json
LOG:  Metadata not found for os.path
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/os/path.pyi (os.path)
LOG:  Could not load cache for posixpath: posixpath.meta.json
LOG:  Metadata not found for posixpath
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/posixpath.pyi (posixpath)
LOG:  Could not load cache for genericpath: genericpath.meta.json
LOG:  Metadata not found for genericpath
LOG:  Parsing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/genericpath.pyi (genericpath)
LOG:  Loaded graph with 25 nodes (0.187 sec)
LOG:  Found 2 SCCs; largest has 24 nodes
LOG:  Processing SCC of size 24 (subprocess posix _ast os importlib.machinery importlib.abc typing_extensions io ast _typeshed types abc typing genericpath codecs mmap array _collections_abc sys posixpath collections os.path importlib builtins) as inherently stale
LOG:  Writing subprocess /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/subprocess.pyi subprocess.meta.json subprocess.data.json
LOG:  Cached module subprocess has changed interface
LOG:  Writing posix /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/posix.pyi posix.meta.json posix.data.json
LOG:  Cached module posix has changed interface
LOG:  Writing _ast /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/_ast.pyi _ast.meta.json _ast.data.json
LOG:  Cached module _ast has changed interface
LOG:  Writing os /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/os/__init__.pyi os/__init__.meta.json os/__init__.data.json
LOG:  Cached module os has changed interface
LOG:  Writing importlib.machinery /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/importlib/machinery.pyi importlib/machinery.meta.json importlib/machinery.data.json
LOG:  Cached module importlib.machinery has changed interface
LOG:  Writing importlib.abc /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/importlib/abc.pyi importlib/abc.meta.json importlib/abc.data.json
LOG:  Cached module importlib.abc has changed interface
LOG:  Writing typing_extensions /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/typing_extensions.pyi typing_extensions.meta.json typing_extensions.data.json
LOG:  Cached module typing_extensions has changed interface
LOG:  Writing io /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/io.pyi io.meta.json io.data.json
LOG:  Cached module io has changed interface
LOG:  Writing ast /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/ast.pyi ast.meta.json ast.data.json
LOG:  Cached module ast has changed interface
LOG:  Writing _typeshed /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/_typeshed/__init__.pyi _typeshed/__init__.meta.json _typeshed/__init__.data.json
LOG:  Cached module _typeshed has changed interface
LOG:  Writing types /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/types.pyi types.meta.json types.data.json
LOG:  Cached module types has changed interface
LOG:  Writing abc /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/abc.pyi abc.meta.json abc.data.json
LOG:  Cached module abc has changed interface
LOG:  Writing typing /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/typing.pyi typing.meta.json typing.data.json
LOG:  Cached module typing has changed interface
LOG:  Writing genericpath /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/genericpath.pyi genericpath.meta.json genericpath.data.json
LOG:  Cached module genericpath has changed interface
LOG:  Writing codecs /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/codecs.pyi codecs.meta.json codecs.data.json
LOG:  Cached module codecs has changed interface
LOG:  Writing mmap /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/mmap.pyi mmap.meta.json mmap.data.json
LOG:  Cached module mmap has changed interface
LOG:  Writing array /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/array.pyi array.meta.json array.data.json
LOG:  Cached module array has changed interface
LOG:  Writing _collections_abc /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/_collections_abc.pyi _collections_abc.meta.json _collections_abc.data.json
LOG:  Cached module _collections_abc has changed interface
LOG:  Writing sys /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/sys.pyi sys.meta.json sys.data.json
LOG:  Cached module sys has changed interface
LOG:  Writing posixpath /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/posixpath.pyi posixpath.meta.json posixpath.data.json
LOG:  Cached module posixpath has changed interface
LOG:  Writing collections /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/collections/__init__.pyi collections/__init__.meta.json collections/__init__.data.json
LOG:  Cached module collections has changed interface
LOG:  Writing os.path /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/os/path.pyi os/path.meta.json os/path.data.json
LOG:  Cached module os.path has changed interface
LOG:  Writing importlib /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/importlib/__init__.pyi importlib/__init__.meta.json importlib/__init__.data.json
LOG:  Cached module importlib has changed interface
LOG:  Writing builtins /private/tmp/test_case/.venv/lib/python3.9/site-packages/mypy/typeshed/stdlib/builtins.pyi builtins.meta.json builtins.data.json
LOG:  Cached module builtins has changed interface
LOG:  Processing SCC singleton (test_case) as inherently stale with stale deps (builtins typing)
LOG:  Writing test_case test_case1.py test_case.meta.json test_case.data.json
LOG:  Cached module test_case has changed interface
LOG:  No fresh SCCs left in queue
LOG:  Build finished in 1.096 seconds with 25 modules, and 0 errors
Success: no issues found in 1 source file

@posita posita changed the title No (longer any?) error when passing non-compliant type to overloaded __getitem__ No (longer any?) error when passing non-compliant type to @overload-ed __getitem__ Jul 30, 2021
@posita posita changed the title No (longer any?) error when passing non-compliant type to @overload-ed __getitem__ No (longer any?) error when passing non-compliant type to __getitem__ with typed key: Union[int, slice] parameter Jul 30, 2021
@posita posita changed the title No (longer any?) error when passing non-compliant type to __getitem__ with typed key: Union[int, slice] parameter No (longer any?) error when passing non-compliant type to __getitem__ in function body? Jul 30, 2021
@posita posita changed the title No (longer any?) error when passing non-compliant type to __getitem__ in function body? No (longer any?) error when passing non-compliant type to __getitem__ inside function body, but same statement generates error at module level Jul 30, 2021
@posita posita changed the title No (longer any?) error when passing non-compliant type to __getitem__ inside function body, but same statement generates error at module level Regression: no error when passing non-compliant type to __getitem__ inside function body, but same statement generates error at module level Jul 30, 2021
@posita posita changed the title Regression: no error when passing non-compliant type to __getitem__ inside function body, but same statement generates error at module level Regression(?): no error when passing non-compliant type to __getitem__ inside function body, but same statement generates error at module level Jul 30, 2021
@posita
Copy link
Contributor Author

posita commented Jul 30, 2021

I have now verified this happens with Python versions 3.6.14, 3.7.11, 3.8.2, 3.8.11, and 3.9.6.

@pranavrajpal
Copy link
Contributor

I think this is because of https://mypy.readthedocs.io/en/stable/common_issues.html#no-errors-reported-for-obviously-wrong-code. Does running mypy with --check-untyped-defs fix this?

@posita
Copy link
Contributor Author

posita commented Jul 30, 2021

I think this is because of https://mypy.readthedocs.io/en/stable/common_issues.html#no-errors-reported-for-obviously-wrong-code. Does running mypy with --check-untyped-defs fix this?

--check-untyped-defs "fixes" this (in a sense):

% rm -fr .mypy_cache ; mypy --config=/dev/null --check-untyped-defs test_case3.py
/dev/null: No [mypy] section in config file
test_case3.py:5: error: No overload variant of "__getitem__" of "list" matches argument type "str"
test_case3.py:5: note: Possible overload variants:
test_case3.py:5: note:     def __getitem__(self, SupportsIndex) -> int
test_case3.py:5: note:     def __getitem__(self, slice) -> List[int]
test_case3.py:8: error: No overload variant of "__getitem__" of "list" matches argument type "str"
test_case3.py:8: note: Possible overload variants:
test_case3.py:8: note:     def __getitem__(self, SupportsIndex) -> int
test_case3.py:8: note:     def __getitem__(self, slice) -> List[int]
Found 2 errors in 1 file (checked 1 source file)

I still think this is a bug, for two reasons. First, the fact that it works at the module level and not the function level is just weird. Second, I swear this used to work with classes that defined their own __getitem__ methods. (See my test_case2.py example in the original report.) Also, this still doesn't work without --check-untyped-defs:

# test_case4.py
from typing import List

def func():
    a: List[int] = [0, 1, 2]
    key: str = "asdf"
    a[key]  # <- This doesn't generate an error

func()
b: List[int] = [0, 1, 2]
key: str = "asdf"
b[key]  # <- But this does?!

Runtime:

% rm -fr .mypy_cache ; mypy --config=/dev/null test_case4.py
/dev/null: No [mypy] section in config file
test_case4.py:12: error: No overload variant of "__getitem__" of "list" matches argument type "str"
test_case4.py:12: note: Possible overload variants:
test_case4.py:12: note:     def __getitem__(self, SupportsIndex) -> int
test_case4.py:12: note:     def __getitem__(self, slice) -> List[int]
Found 1 error in 1 file (checked 1 source file)

@posita
Copy link
Contributor Author

posita commented Jul 30, 2021

Ugh, talk about a super sharp edge. This works as expected (since I typed the enclosing function):

# test_case5.py
from typing import List

def func() -> None:
    a: List[int] = [0, 1, 2]
    key: str = "asdf"
    a[key]  # <- *Now* this generates an error

func()
b: List[int] = [0, 1, 2]
key: str = "asdf"
b[key]  # <- So does this

@posita
Copy link
Contributor Author

posita commented Jul 30, 2021

As does this:

# test_case6.py
from typing import List, Union

class GetItemTest:
    def __getitem__(self, key: Union[int, slice]) -> Union[int, List[int]]:
        return list(range(10))[key]

def test_a() -> None:
    a = GetItemTest()
    a["asdf"]  # <- errors

@posita posita changed the title Regression(?): no error when passing non-compliant type to __getitem__ inside function body, but same statement generates error at module level No errors on typed function internals if function itself is untyped; I'd argue that's probably incorrect behavior, but at the very least it's *super* confusing and *very* hard to diagnose when it happens Jul 30, 2021
@erictraut
Copy link

This is yet another example that supports my thinking that --check-untyped-defs should default to on. See discussion here.

@posita posita changed the title No errors on typed function internals if function itself is untyped; I'd argue that's probably incorrect behavior, but at the very least it's *super* confusing and *very* hard to diagnose when it happens No errors on typed function internals if function definition itself is untyped; I'd argue that's probably incorrect behavior, but at the very least it's *super* confusing and *very* hard to diagnose when it happens Jul 30, 2021
@posita
Copy link
Contributor Author

posita commented Jul 30, 2021

This is yet another example that supports my thinking that --check-untyped-defs should default to on. See discussion here.

Maybe as a work-around, but functions seem like pretty arbitrary boundaries to try and walk this particular line, especially where bodies contain explicit type annotations. I can't see how ignoring those can be considered correct under any setting. In other words, yeah, totally make the sane thing the default thing, but I'm pretty sure this is still a bug.

@JelleZijlstra
Copy link
Member

It's documented that mypy doesn't type check unannotated functions (https://mypy.readthedocs.io/en/stable/common_issues.html#no-errors-reported-for-obviously-wrong-code). I don't disagree with you that this is unintuitive, but the other issue is a sufficient place to discuss that.

@posita
Copy link
Contributor Author

posita commented Jul 30, 2021

It's documented that mypy doesn't type check unannotated functions (https://mypy.readthedocs.io/en/stable/common_issues.html#no-errors-reported-for-obviously-wrong-code). I don't disagree with you that this is unintuitive, but the other issue is a sufficient place to discuss that.

Yes, this appears to be a dupe of #3948.

But I don't think the documentation is even accurate. Consider:

# test_case9.py
from typing import Callable
a: Callable[[], int]

def _untyped_init_a():
    global a
    a = lambda: [0, 1, 2]["key"]  # errors

def _untyped_init_b():
    global b
    b = lambda: [0, 1, 2]["key"]  # doesn't error

According to the docs, _untyped_init_a shouldn't error, but it does. Un-annotated function bodies are treated inconsistently where untyped_defs is False.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests

4 participants