python / mypy

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

ctypes.Structure and Iterable #10272

Open giladreti opened 3 years ago

giladreti commented 3 years ago

Bug Report

For some reason ctypes.Structure instances pass mypy's isinstance checks although mypy also claims that they are not iterable. For example, consider the following code:

from ctypes import Structure

from typing import Iterable

class S(Structure):
    pass

s = S()

if isinstance(s, Iterable):
    for _ in s:
        pass

mypy gives the following error:

test.py:13: error: "S" has no attribute "__iter__" (not iterable)
Found 1 error in 1 file (checked 1 source file)

replacing it with iter(s) make mypy happy again.

Your Environment

JulienPalard commented 2 years ago

I don't think a Structure is meant to be iterable, looks like it is not:

$ cat  test.py
from ctypes import Structure

class S(Structure):
    pass

s = S()

iter(s)

$ python test.py
Traceback (most recent call last):
  File "/home/mdk/clones/JulienPalard/oeis/test.py", line 9, in <module>
    iter(s)
TypeError: 'S' object is not iterable

So I think the bug is more than adding iter mutes the error, while it should not?

JelleZijlstra commented 2 years ago

The weird thing then is that mypy thinks the isinstance(S, Iterable) check succeeds. I think that's because ctypes.Structure has a __getattr__ method in typeshed (https://github.com/python/typeshed/blob/5d45e3babc7241001a63e66146a03c3a1d959227/stdlib/ctypes/__init__.pyi#L251).

Here's a simpler repro case: https://mypy-play.net/?mypy=latest&python=3.10&gist=858d8c8589cfdc893dce41f54f08b046

from typing import Iterable, Any

class Structure:
    def __getattr__(self, attr: str) -> Any: ...

class S(Structure):
    pass

s = S()

if isinstance(s, Iterable):
    for _ in s:
        pass

Presumably mypy conceptually looks at the instance for the isinstance() check, so the __getattr__ allows it to think there's an __iter__ method, but when checking for _ in s: it (correctly) looks at the class, which doesn't have an __iter__ method.