ruff/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md
justin 08fcf7e106
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
[ty] initial support for slots=True in dataclasses (#20278)
2025-09-07 18:25:35 +01:00

6.1 KiB

Tests for ty's instance-layout-conflict error code

__slots__: not specified or 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

__slots__: incompatible tuples

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

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

class C(  # error: [instance-layout-conflict]
    A,
    B,
): ...

__slots__ are the same value

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

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

class C(  # error: [instance-layout-conflict]
    A,
    B,
): ...

__slots__ is a string

class A:
    __slots__ = "abc"

class B:
    __slots__ = ("abc",)

class AB(  # error: [instance-layout-conflict]
    A,
    B,
): ...

Synthesized __slots__ from dataclasses

from dataclasses import dataclass

@dataclass(slots=True)
class F: ...

@dataclass(slots=True)
class G: ...

class H(F, G): ...  # fine because both classes have empty `__slots__`

@dataclass(slots=True)
class I:
    x: int

@dataclass(slots=True)
class J:
    y: int

class K(I, J): ...  # error: [instance-layout-conflict]

Invalid __slots__ definitions

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

Inherited __slots__

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

class B(A): ...

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

class D(C): ...
class E(  # error: [instance-layout-conflict]
    B,
    D,
): ...

A single "disjoint 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 to __slots__

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

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

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

# TODO: ideally this would trigger `[instance-layout-conflict]`
# (but it's also not high-priority)
class C(A, B): ...

Explicitly annotated __slots__

We do not emit false positives on classes with empty __slots__ definitions, even if the __slots__ definitions are annotated with variadic tuples:

class Foo:
    __slots__: tuple[str, ...] = ()

class Bar:
    __slots__: tuple[str, ...] = ()

class Baz(Foo, Bar): ...  # fine

Built-ins with implicit layouts

Certain classes implemented in C extensions also have an extended instance memory layout, in the same way as classes that define non-empty __slots__. CPython internally calls all such classes with a unique instance memory layout "solid bases", but PEP 800 calls these classes "disjoint bases", and this is the term we generally use. The @disjoint_base decorator introduced by this PEP provides a generalised way for type checkers to identify such classes.

from typing_extensions import disjoint_base

# fmt: off

class A(  # error: [instance-layout-conflict]
    int,
    str
): ...

class B:
    __slots__ = ("b",)

class C(  # error: [instance-layout-conflict]
    int,
    B,
): ...
class D(int): ...

class E(  # error: [instance-layout-conflict]
    D,
    str
): ...

class F(int, str, bytes, bytearray): ...  # error: [instance-layout-conflict]

@disjoint_base
class G: ...

@disjoint_base
class H: ...

class I(  # error: [instance-layout-conflict]
    G,
    H
): ...

# fmt: on

We avoid emitting an instance-layout-conflict diagnostic for this class definition, because range is @final, so we'll complain about the class statement anyway:

class Foo(range, str): ...  # error: [subclass-of-final-class]

Multiple "disjoint bases" where one is a subclass of the other

A class is permitted to multiple-inherit from multiple disjoint bases if one is a subclass of the other:

class A:
    __slots__ = ("a",)

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

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

The same principle, but a more complex example:

class AA:
    __slots__ = ("a",)

class BB(AA):
    __slots__ = ("b",)

class CC(BB): ...
class DD(AA): ...
class FF(CC, DD): ...  # fine

False negatives

Possibly unbound __slots__

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 __slots__ 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-tuple __slots__ definitions

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

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

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

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