Descriptors misbehave with `NoReturn`/`Never`, with `@overload`s, etc.
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 20.6k
- Forks
- 3.3k
- PR merge metrics
- PR metrics pending
Description
Bug Report
To Reproduce
Gists:
mypy-play.net
pyright-play.net (pyright doesn't handle this well, issuing no warnings at all; it does respond to reveal_type() in the descriptors-used-as-intended case, suggesting it doesn't understand that directive is unreachable)
from __future__ import annotations
from typing_extensions import (
NoReturn,
Optional,
overload,
Self,
Type,
)
# Note: This is a simplified example.
#
# - obviously, it would be more expedient to simply omit the definition
# of '__get__()' for this trivial case
#
# - similar problems affect implicitly calling '__set__()' through a
# descriptor
class WriteOnlyDescr:
'''some kind of write-only descriptor (to demonstrate the general issue)
'''
@overload # descriptor accessible through class ('SomeClass')
def __get__(self, owner: None, objtype: Type[object]) -> Self: ...
@overload
def __get__( # access on an instance of 'SomeClass' (forbidden)
self, owner: object, objtype: Optional[Type[object]] = None
) -> NoReturn: ...
def __get__(
self, owner: Optional[object],
objtype: Optional[Type[object]] = None,
) -> Self:
# allow access to this descriptor via the class:
if owner is None:
return self
raise AttributeError
# '__set__()' omitted for brevity
class SomeClass:
'''a class with a write-only descriptor
'''
descr = WriteOnlyDescr()
descr = SomeClass.descr
inst = SomeClass()
if bool():
# First case:
#
# First, we'll "spell out" the descriptor mechanics; this should
# give the same results as using the descriptor in a natural way
# (the second case)
# EXPECTED for the second case:
# ──────────────────────────────────────
get = descr.__get__(inst) # ← mypy: [var-annotated]
# "Need type annotation for 'get'"
reveal_type(get) # ← (no mypy output; this is unreachable)
_ = True # ← mypy: [unreachable]
else: # ↑ mypy output _should_ be identical ↑
# ↓ above and below this point ↓
# Second case:
#
# This _should_ give the same output as above, since the first
# lines of the two cases are virtually synonymous, while the
# remaining lines are identical.
# ACTUAL for the second case:
# ──────────────────────────────────────
get = inst.descr # ← (no mypy output)
reveal_type(get) # ← mypy: "Revealed type is 'Never'"
_ = True # ← (no mypy output)
Expected Behavior
Descriptors should behave the same way when used as intended and when the mechanics are spelled out (explicit calls to __get__(), etc.).
Actual Behavior
This isn't the case, especially when an argument has type Never or a function is marked NoReturn. (This can come up when @overloads are in use.)
Your Environment
- Mypy version used: 1.8.0
- Mypy command-line flags:
--warn-unreachableis informative - Mypy configuration options from
mypy.ini(and other config files): none - Python version used: 3.8, 3.12
Further context
...follows in a comment.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the simplified example in the linked mypy-play.net case with mypy 1.8.0 and --warn-unreachable, comparing explicit get calls with natural descriptor access. Trace the descriptor and overload handling involved in NoReturn/Never, then add regression coverage so both forms produce equivalent diagnostics and revealed types.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100