agronholm / agronholm/typeguard
Never and NoReturn are not checked when nested inside another annotation, so any value passes
- 主要語言
- Python
- 星號
- 1.8k
- 分支
- 145
- 平均合併
- 8 天 12 小時
- 30 天內合併 PR
- 1
描述
### Things to check first
- [x] I have searched the existing issues and didn't find my bug already reported there
- [x] I have checked that my bug is still present in the latest release
### Typeguard version
4.6.0 (also present on `main` at `123f1abc7e82bbe980c779005de858533e1786c3`)
### Python version
3.12.14
### What happened?
`Never` and `NoReturn` are handled correctly when either one is the entire argument or return annotation. In the example below `bare(3)` raises `TypeCheckError`, which is what I expected.
When either one appears inside another annotation, no check is performed and every value passes. `list[Never]` accepts `[1]`, `dict[str, Never]` accepts `{"a": 1}`, `Optional[Never]` accepts `1`, and `Sequence[NoReturn]` accepts `[1]`. Nothing is raised and nothing is warned. The two forms disagree with each other in the same argument position:
```
def bare(x: Never) rejects 3, correct
def nested(x: list[Never]) accepts [1]
```
The case I found most surprising is `Union[int, Never]`, which accepts `"x"`. A plain `str` is not an `int`, so that union passes on its `Never` member alone.
I expected `check_type([1], list[Never])` to raise `TypeCheckError` the same way `check_type(["a"], list[int])` does.
`docs/features.rst` lists `typing.Never` and `typing.NoReturn` as "Supported in argument and return type annotations", and `list[Never]` is an argument annotation, so I could not work out from the docs which of the two behaviours is intended. If nesting is deliberately out of scope, then narrowing that table entry would have saved me the confusion, because the bare form working is exactly what led me to expect the nested form to work.
This is separate from #579. That one is about PEP 695 aliases; this reproduces with no alias involved, and the bare form here already works.
### How can we reproduce the bug?
Standard library only, no third party packages beyond typeguard itself.
```python
from typing import Never, NoReturn, Optional, Sequence, Union
from typeguard import TypeCheckError, check_type, typechecked
@typechecked
def bare(x: Never) -> int:
return 1
@typechecked
def nested(x: list[Never]) -> int:
return 1
try:
bare(3)
print("bare(3) accepted")
except TypeCheckError:
print("bare(3) TypeCheckError")
try:
nested([1])
print("nested([1]) accepted")
except TypeCheckError:
print("nested([1]) TypeCheckError")
for annotation, value in [
(list[Never], [1]),
(dict[str, Never], {"a": 1}),
(Union[int, Never], "x"),
(Optional[Never], 1),
(tuple[Never, ...], (1, 2)),
(Sequence[NoReturn], [1]),
]:
try:
check_type(value, annotation)
print(f"check_type accepted {value!r:12} against {annotation}")
except TypeCheckError:
print(f"check_type TypeCheckError {value!r} against {annotation}")
```
Output on 4.6.0 under Python 3.12.14, in a container with only typeguard installed:
```
bare(3) TypeCheckError
nested([1]) accepted
check_type accepted [1] against list[typing.Never]
check_type accepted {'a': 1} against dict[str, typing.Never]
check_type accepted 'x' against typing.Union[int, typing.Never]
check_type accepted 1 against typing.Optional[typing.Never]
check_type accepted (1, 2) against tuple[typing.Never, ...]
check_type accepted [1] against typing.Sequence[typing.NoReturn]
```
The first line is the control and is correct. Every line after it is a value passing a check I expected it to fail. The output is byte for byte the same at `main` HEAD `123f1abc`.
貢獻指南
評估
這個 Issue 還沒有評估資料。