(🐞) TypeGuard behaves inconsistently on instance methods.
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- Python
- Star
- 20.6k
- Fork
- 3.3k
- Chỉ số merge pull request
- Chỉ số pull request đang chờ
Mô tả
Bug Report
When type guards are implemented as methods, Mypy's type narrowing behaviour is inconsistent:
- It depends on whether the method only takes the implicit
selfargument or instead takes additional arguments. - It further depends on whether the second argument has a default value, regardless of whether an explicit value is passed to it in the call expression.
This does not appear to be in compliance with appears to fall through the cracks of PEP 647, whose stated behaviour does not depend on any arguments further than the first explicit argument. Some relevant quotes from the PEP follow:
Type checkers should assume that type narrowing should be applied to the expression that is passed as the first positional argument to a user-defined type guard. If the type guard function accepts more than one argument, no type narrowing is applied to those additional argument expressions.
If the type guard function is implemented as an instance or class method, an implicit self or cls argument will also be passed to the function.
If a type guard function is implemented as an instance method or class method, the first positional argument maps to the second parameter (after
selforcls).
A concern was raised that there may be cases where it is desired to apply the narrowing logic on
selfandcls. [...] It was [...] decided that no special provision would be made for it. If narrowing ofselforclsis required, the value can be passed as an explicit argument to a type guard function.
Expected Behavior
In Issue #16146, it was clarified that the intent of PEP 647 is for positionality to depend on the call expression: the first positional argument is c in the call expression a.f(c), but it is a in the call expression A.f(a, c).
Now, consider the following pattern where a type guard is implemented by an instance method, with arbitrary arguments:
class A:
def f(self, ...) -> TypeGuard[B]:
# ^^^ your choice of arguments here
return isinstance(self, B)
class B(A): ...
The My expectation here is that call expressions in the form A.f(a, ...)—where ... here stands for arbitary values passed to other arguments in such a way as to make the call legal—will always result in type narrowing of a from a: A to a: B. This behaviour should be independent of the number of additional arguments and/or whether additional arguments are given default values (mentioned here because default values increase the number of legal call expressions).
Actual Behavior
In the three code snippets below, Mypy fails to apply this logic to the case where methods only take the implicit argument self, so that A.f(a) does not result in type narrowing for a while A.f(a, c) does. Furthermore, A.f(a, c) results in type narrowing behaviour for a when c is not given a default value, but fails to do so when a default value for c is specified, regardless of whether the call expression is A.f(a, c) (explicit value passed for c) or A.f(a) (default value used for c).
If the method f is implemented as a classmethod, A.f(a) results in type narrowing for a, as expected.
Pylance behaves as expected I expect in all cases.
To Reproduce
❌ Case 1: only self argument.
from __future__ import annotations
from typing import Any, TypeGuard, reveal_type
class A:
def f(self) -> TypeGuard[B]:
return isinstance(self, B)
class B(A): ...
a = A()
assert A.f(a)
reveal_type(a)
# Pylance: Type of "a" is "B"
# Mypy: Revealed type is "A"
b: B = a
# Mypy: Incompatible types in assignment (expression has type "A", variable has type "B")
# Pylance: OK
✅ Case 2: self argument plus one explicit argument.
from __future__ import annotations
from typing import TypeGuard, reveal_type
class A:
def f(self, c: str) -> TypeGuard[B]:
print(c)
return isinstance(self, B)
class B(A): ...
a = A()
assert A.f(a, "Hi!")
reveal_type(a)
# Mypy: Revealed type is "B"
# Pylance: Type of "a" is "B"
b: B = a # No error
❌ Case 3: self argument plus one explicit argument with default value. Second argument passed explicitly in call expression.
from __future__ import annotations
from typing import Any, TypeGuard, reveal_type
class A:
def f(self, c: str = "") -> TypeGuard[B]:
print(c)
return isinstance(self, B)
class B(A): ...
a = A()
assert A.f(a, "Hi!")
reveal_type(a)
# Mypy: Revealed type is "A"
# Pylance: Type of "a" is "B"
b: B = a
# Mypy: Incompatible types in assignment (expression has type "A", variable has type "B")
# Pylance: OK
❌ Case 4: self argument plus one explicit argument with default value. Default value for second argument used implicitly in call expression.
from __future__ import annotations
from typing import Any, TypeGuard, reveal_type
class A:
def f(self, c: str = "") -> TypeGuard[B]:
print(c)
return isinstance(self, B)
class B(A): ...
a = A()
assert A.f(a)
reveal_type(a)
# Mypy: Revealed type is "A"
# Pylance: Type of "a" is "B"
b: B = a
# Mypy: Incompatible types in assignment (expression has type "A", variable has type "B")
# Pylance: OK
✅ Case 5: self argument plus two explicit arguments, only one of which has a default value. (Behaviour does not depend on whether the third argument is passed explicitly or not.)
from __future__ import annotations
from typing import Any, TypeGuard, reveal_type
class A:
def f(self, c: str, d: int = 0) -> TypeGuard[B]:
print(c)
return isinstance(self, B)
class B(A): ...
a = A()
assert A.f(a, "Hi")
reveal_type(a)
# Mypy: Revealed type is "B"
# Pylance: Type of "a" is "B"
b: B = a # No error
✅ Case 6: like Case 1, but using a class method instead of an instance method.
from __future__ import annotations
from typing import Any, Self, TypeGuard, reveal_type
class A:
@classmethod
def f(cls, a: Self) -> TypeGuard[B]:
return isinstance(a, B)
class B(A): ...
a = A()
assert A.f(a)
reveal_type(a)
# Mypy: Revealed type is "B"
# Pylance: Type of "a" is "B"
b: B = a # No error
Your Environment
- Mypy version used:
mypy 1.7.1 (compiled: yes) - Mypy command-line flags:
--strict - Mypy configuration options from
mypy.ini(and other config files): None - Python version used:
Python 3.12.0
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Bắt đầu bằng cách chạy sáu trường hợp tái hiện từ issue với mypy 1.7.1 và so sánh việc thu hẹp kiểu được báo cáo với các kết quả mong đợi. Truy vết việc xử lý TypeGuard cho các lời gọi phương thức thể hiện, bao gồm self ngầm định và các đối số mặc định; được xem là hoàn tất khi các trường hợp phương thức thể hiện thu hẹp kiểu một cách nhất quán với hành vi PEP 647 đã nêu mà không làm hồi quy các trường hợp đang vượt qua.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- python
- Lĩnh vực
- compilers, devtools
- Loại issue
- Lỗi
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức độ hoạt động
- Đình trệ
- Độ rõ ràng
- Khá rõ ràng
- Mức phù hợp với người mới
- 38/100