python / python/mypy

A decorator implementing the descriptor protocol is incorrectly regarded read-only

Open
#16,709 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
20.6k
Forks
3.3k
PR merge metrics
PR metrics pending

Description

Bug Report

A class-based decorator that implements the data descriptor protocol is incorrectly regarded as read-only, and does not match against itself in a Protocol. This bug was discovered when trying to implement protocols that match against SQLAlchemy's declared_attr decorator.

To Reproduce

Pattern in typical use:

from sqlalchemy.orm import Mapped, declared_attr

class MyModel:
    @declared_attr
    @classmethod
    def foo(cls) -> Mapped[int]: ...

    reveal_type(foo)  # declared_attr[int]
    reveal_type(foo.__set__)  # exists, so this is a data descriptor
    reveal_type(foo.__get__)  # Typed as `-> int | InstrumentedAttribute[int]`, unlike `property.__get__`

class MyProtocol:
    foo: declared_attr[int]

o: MyProtocol = MyModel()  # Error: MyProtocol.foo expected settable variable, got read-only attribute

Here is an isolated code sample without external dependencies. This bug does not appear when the method decorator is replaced with a function call after method definition, and appears to be a side-effect of how Mypy processes decorators.

https://mypy-play.net/?gist=5eb1e9262cd880a77fd3b9fb5bb7dca0

Marking the attribute as read-only in the Protocol using @property works when the attribute is accessed via the instance, but not when accessed via the class (as o.__class__.foo), because it gets typed as property.fget (Mypy) or as an error (Pyright), instead of the expected declared_attr.__get__ return type.

This problem exists in all versions of Mypy available at mypy-play, including the latest release 1.8.0. Pyright 1.1.338 does not have this problem.

Expected Behavior

A decorated method that is turned into a descriptor should not be regarded as read-only when matching against a protocol.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the isolated mypy-play sample and the decorator-processing and Protocol attribute-matching paths it exercises. Reproduce the read-only diagnostic, then trace how the decorated method's descriptor methods are typed; done means a data descriptor matches the Protocol as settable and class access preserves the expected get return type.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, sqlalchemy
Domain
compilers, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.