Skip to content

disallow A().__class__ = B #12997

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jorenham opened this issue Nov 11, 2024 · 7 comments · Fixed by #13021
Closed

disallow A().__class__ = B #12997

jorenham opened this issue Nov 11, 2024 · 7 comments · Fixed by #13021

Comments

@jorenham
Copy link
Contributor

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:

@Daverball
Copy link
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.

@jorenham
Copy link
Contributor Author

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) 🤔

@srittau
Copy link
Collaborator

srittau commented Nov 16, 2024

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

@jorenham
Copy link
Contributor Author

the mypy primer seems to like it

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Nov 16, 2024

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

@jorenham
Copy link
Contributor Author

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?

@Daverball
Copy link
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
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants