Skip to content

disallow A().__class__ = B #12997

Closed

Description

@jorenham

The object.__class__ getter is annotated to return type[Self], while the setter accepts any argument that's assignable object.

@property
def __class__(self) -> type[Self]: ...
@__class__.setter
def __class__(self, type: type[object], /) -> None: ...

As I also mentioned in microsoft/pyright#9410 (comment), this could lead to type-unsafe situation, e.g.

class A: ...
class B: ...
a = A()
a.__class__ = B
reveal_type(a.__class__)  # Type of "a.__class__" is "type[A]"

Pyright accepts this, as you'd expect by looking at the stubs (playground link).
However, mypy reports an error reports an error at a.__class__ = B (playground link), so I'm guessing it special-cased it.

At runtime the __class__ behaves just like any other attribute, i.e. invariantly, when getting/setting.
So that's why I think that object.__class__ should be more "self-centered", and only accept type[Self] in its setter.


As for a practical use-case; I recently came up with the following trick. But at the moment, this unexpectedly requires adding # type: ignores with both pyright and mypy:

from typing import Protocol, override

class Just[T](Protocol):  # ~T@Just is invariant
    @property  # type: ignore[override]
    @override
    def __class__(self, /) -> type[T]: ...  # pyright: ignore[reportIncompatibleMethodOverride]
    @__class__.setter
    def __class__(self, t: type[T], /) -> None: ...

def only_ints_pls(x: Just[int], /):
    assert type(x) is int

only_ints_pls(42)  # true negative
only_ints_pls("42")  # true positive (sanity check)
only_ints_pls(False)  # true positive (only in mypy at the moment)

playgrounds:

Activity

  1. Daverball commented on Nov 14, 2024

    @Daverball
    Contributor

    The reason mypy reports an error, is because mypy doesn't support asymmetric properties, it will only use the annotation from the getter, so mypy already works the way you want it to by accident.

    I agree however that it seems better to only allow type[Self] for the setter, even if that doesn't reflect what's legal at runtime, since the only runtime requirement is a compatible object layout, it at least means as long as a subclass doesn't violate LSP it should be type safe to assign a subclass.

    If you want to do an unsafe assignment you can still do it, you'll just have to ignore the type error.

  2. jorenham commented on Nov 14, 2024

    @jorenham
    ContributorAuthor

    The reason mypy reports an error, is because mypy doesn't support asymmetric properties, it will only use the annotation from the getter, so mypy already works the way you want it to by accident.

    Perhaps this could also explain why this hasn't been brought up before (as far as I could tell) 🤔

  3. srittau commented on Nov 16, 2024

    @srittau
    Collaborator

    An exploratory PR couldn't hurt. To me, changing the setter types seems fine.

  4. jorenham commented on Nov 16, 2024

    @jorenham
    ContributorAuthor

    the mypy primer seems to like it

  5. hauntsaninja commented on Nov 16, 2024

    @hauntsaninja
    Collaborator

    As Daverball mentions, mypy doesn't model setters of properties correctly, so it already works the way you want it to so primer is a no-op

  6. jorenham commented on Nov 18, 2024

    @jorenham
    ContributorAuthor

    As Daverball mentions, mypy doesn't model setters of properties correctly, so it already works the way you want it to so primer is a no-op

    Does that mypy bug also apply to inheritance, Protocol matching, and other LSP things?

  7. Daverball commented on Nov 18, 2024

    @Daverball
    Contributor

    Yes, also I wouldn't call it a bug per se, it's a difference between mypy's type model and python's object model of properties. I assume this was originally an intentional design choice, so mypyc can always optimize properties a certain way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions