ruff/crates/ty_python_semantic/resources/mdtest/slots.md
2025-05-03 19:49:15 +02:00

3.6 KiB

__slots__

Not specified and empty

class A: ...

class B:
    __slots__ = ()

class C:
    __slots__ = ("lorem", "ipsum")

class AB(A, B): ...  # fine
class AC(A, C): ...  # fine
class BC(B, C): ...  # fine
class ABC(A, B, C): ...  # fine

Incompatible tuples

class A:
    __slots__ = ("a", "b")

class B:
    __slots__ = ("c", "d")

class C(
    A,  # error: [incompatible-slots]
    B,  # error: [incompatible-slots]
): ...

Same value

class A:
    __slots__ = ("a", "b")

class B:
    __slots__ = ("a", "b")

class C(
    A,  # error: [incompatible-slots]
    B,  # error: [incompatible-slots]
): ...

Strings

class A:
    __slots__ = "abc"

class B:
    __slots__ = ("abc",)

class AB(
    A,  # error: [incompatible-slots]
    B,  # error: [incompatible-slots]
): ...

Invalid

TODO: Emit diagnostics

class NonString1:
    __slots__ = 42

class NonString2:
    __slots__ = b"ar"

class NonIdentifier1:
    __slots__ = "42"

class NonIdentifier2:
    __slots__ = ("lorem", "42")

class NonIdentifier3:
    __slots__ = (e for e in ("lorem", "42"))

Inheritance

class A:
    __slots__ = ("a", "b")

class B(A): ...

class C:
    __slots__ = ("c", "d")

class D(C): ...
class E(
    B,  # error: [incompatible-slots]
    D,  # error: [incompatible-slots]
): ...

Single solid base

class A:
    __slots__ = ("a", "b")

class B(A): ...
class C(A): ...
class D(B, A): ...  # fine
class E(B, C, A): ...  # fine

Post-hoc modifications

class A:
    __slots__ = ()
    __slots__ += ("a", "b")

reveal_type(A.__slots__)  # revealed: tuple[Literal["a"], Literal["b"]]

class B:
    __slots__ = ("c", "d")

class C(
    A,  # error: [incompatible-slots]
    B,  # error: [incompatible-slots]
): ...

False negatives

Possibly unbound

def _(flag: bool):
    class A:
        if flag:
            __slots__ = ("a", "b")

    class B:
        __slots__ = ("c", "d")

    # Might or might not be fine at runtime
    class C(A, B): ...

Bound but with different types

def _(flag: bool):
    class A:
        if flag:
            __slots__ = ("a", "b")
        else:
            __slots__ = ()

    class B:
        __slots__ = ("c", "d")

    # Might or might not be fine at runtime
    class C(A, B): ...

Non-tuples

class A:
    __slots__ = ["a", "b"]  # This is treated as "dynamic"

class B:
    __slots__ = ("c", "d")

# False negative: [incompatible-slots]
class C(A, B): ...

Built-ins with implicit layouts

# False negative: [incompatible-slots]
class A(int, str): ...

Diagnostic if __slots__ is externally modified

We special-case type inference for __slots__ and return the pure inferred type, even if the symbol is not declared — a case in which we union with Unknown for other public symbols. The reason for this is that __slots__ has a special handling in the runtime. Modifying it externally is actually allowed, but those changes do not take effect. If you have a class C with __slots__ = ("foo",) and externally set C.__slots__ = ("bar",), you still can't access C.bar. And you can still access C.foo. We therefore issue a diagnostic for such assignments:

class A:
    __slots__ = ("a",)

    # Modifying `__slots__` from within the class body is fine:
    __slots__ = ("a", "b")

# No `Unknown` here:
reveal_type(A.__slots__)  # revealed: tuple[Literal["a"], Literal["b"]]

# But modifying it externally is not:

# error: [invalid-assignment]
A.__slots__ = ("a",)

# error: [invalid-assignment]
A.__slots__ = ("a", "b_new")

# error: [invalid-assignment]
A.__slots__ = ("a", "b", "c")