Thanks to visit codestin.com
Credit goes to github.com

Skip to content

disallow A().__class__ = B #12997

Closed
@jorenham

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:

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