Skip to content

Enforce __slots__ validation for attribute writes#2695

Closed
Adist319 wants to merge 2 commits intofacebook:mainfrom
Adist319:feature/slots-validation
Closed

Enforce __slots__ validation for attribute writes#2695
Adist319 wants to merge 2 commits intofacebook:mainfrom
Adist319:feature/slots-validation

Conversation

@Adist319
Copy link
Copy Markdown
Contributor

@Adist319 Adist319 commented Mar 6, 2026

Summary

Adds type checking enforcement for Python's __slots__ mechanism. When a class defines __slots__, writing to attributes not declared in the slots now produces a missing-attribute error.

Fixes #630.

Supported __slots__ forms:

  • Tuple literals: __slots__ = ("x", "y")
  • List literals: __slots__ = ["x", "y"]
  • Single string: __slots__ = "x"
  • @dataclass(slots=True)

Enforcement is correctly suppressed when:

  • Any ancestor in the MRO is unslotted (inherits __dict__)
  • "__dict__" appears in slots
  • The class or an ancestor defines a custom __setattr__
  • The attribute is accessed through a property setter or descriptor __set__

The implementation checks three code paths where attribute writes occur:

  1. Instance attribute creation (self.y = 2 in methods) — caught during class field validation in class_field.rs
  2. External writes to missing attrs (c.y = 3 where y is unknown) — caught in attr.rs via the GetAttr/not-found paths
  3. External writes to class-level attrs not in slots (c.y = 3 where y: int is a bare annotation) — caught in attr.rs via the ClassAttribute path

Test Plan

  • 17 targeted test cases covering enforcement, suppression, and edge cases
  • cargo test -p pyrefly test_slots — all 17 pass
  • cargo test -p pyrefly -- attributes:: — all 175 pass, no regressions
  • cargo test -p pyrefly -- dataclasses:: — all 106 pass, no regressions

Adds type checking enforcement for Python's `__slots__` mechanism. When a class
defines `__slots__`, writing to attributes not declared in the slots now produces
a `missing-attribute` error.

Handles tuple, list, and single-string literal forms of `__slots__`, as well as
`@dataclass(slots=True)`. Enforcement is correctly suppressed when any ancestor
is unslotted, `__dict__` appears in slots, or the class defines a custom
`__setattr__`. Properties and descriptors are also exempted since they operate
at the class level rather than instance storage.
@meta-cla meta-cla bot added the cla signed label Mar 6, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 6, 2026

Diff from mypy_primer, showing the effect of this PR on open source code:

rich (https://github.com/Textualize/rich)
+ ERROR rich/text.py:1325:13-23: Object of class `Text` has no attribute `plain` (not declared in `__slots__`) [missing-attribute]

dd-trace-py (https://github.com/DataDog/dd-trace-py)
+ ERROR ddtrace/_trace/span.py:231:14-25: Object of class `Span` has no attribute `duration_ns` (not declared in `__slots__`) [missing-attribute]
+ ERROR ddtrace/_trace/span.py:298:18-25: Object of class `Span` has no attribute `service` (not declared in `__slots__`) [missing-attribute]
+ ERROR ddtrace/_trace/span.py:490:14-19: Object of class `Span` has no attribute `error` (not declared in `__slots__`) [missing-attribute]

core (https://github.com/home-assistant/core)
+ ERROR homeassistant/components/bluetooth/manager.py:93:14-20: Object of class `HomeAssistantBluetoothManager` has no attribute `_debug` (not declared in `__slots__`) [missing-attribute]
+ ERROR homeassistant/components/bluetooth/manager.py:182:14-26: Object of class `HomeAssistantBluetoothManager` has no attribute `_all_history` (not declared in `__slots__`) [missing-attribute]
+ ERROR homeassistant/components/bluetooth/manager.py:182:33-53: Object of class `HomeAssistantBluetoothManager` has no attribute `_connectable_history` (not declared in `__slots__`) [missing-attribute]

pip (https://github.com/pypa/pip)
+ ERROR src/pip/_vendor/rich/text.py:1323:13-23: Object of class `Text` has no attribute `plain` (not declared in `__slots__`) [missing-attribute]

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 6, 2026

Primer Diff Classification

❌ 3 regression(s) | ✅ 1 improvement(s) | 4 project(s) total

3 regression(s) across rich, core, pip. error kinds: missing-attribute. caused by effective_slots_for_instance_write(). 1 improvement(s) across dd-trace-py.

Project Verdict Changes Error Kinds Root Cause
rich ❌ Regression +1 missing-attribute effective_slots_for_instance_write()
dd-trace-py ✅ Improvement +3 missing-attribute effective_slots_for_instance_write()
core ❌ Regression +3 missing-attribute effective_slots_for_instance_write()
pip ❌ Regression +1 missing-attribute effective_slots_for_instance_write()
Detailed analysis

❌ Regression (3)

rich (+1)

This is a REGRESSION. While the code does have a genuine slots violation (line 1325 assigns to plain which is not in the declared slots), this represents pyrefly being stricter than the established ecosystem standard. Mypy and pyright do not enforce slots constraints during static analysis - they treat it as a runtime concern. The typing spec does not require type checkers to validate slots compliance. The PR intentionally added this stricter behavior, but it creates false positive noise compared to other type checkers. The rich library is a well-tested project, and this pattern (assigning to plain via the property setter on line 1325) works correctly at runtime despite the slots violation because plain is implemented as a property with custom getter/setter logic (lines 403-418).
Attribution: The change to effective_slots_for_instance_write() in pyrefly/lib/alt/attr.rs and the new enforcement logic in class_field.rs added slots validation. The PR explicitly states this adds 'type checking enforcement for Python's slots mechanism' with enforcement in three code paths including instance attribute creation.

core (+3)

These are new slots enforcement errors that pyrefly now reports. While the PR description states this implements spec compliance, slots validation is not actually required by the typing specification - it's a runtime Python feature for memory optimization. The typing spec focuses on static type compatibility, not runtime attribute access restrictions.

Looking at the specific errors:

  1. Line 93: self._debug = ... - _debug is not in the class's slots tuple
  2. Line 182: self._all_history, self._connectable_history = ... - neither attribute is declared in slots

These would indeed cause AttributeError at runtime due to Python's slots mechanism, but mypy and pyright do not flag such patterns because they treat slots as a runtime optimization detail rather than a type checking concern. The ecosystem standard is to allow these patterns in static analysis.

The HomeAssistant codebase is a mature, well-tested project. These attribute assignments work at runtime, likely because the parent class BluetoothManager (from the habluetooth library) either doesn't use slots or includes these attributes in its slots. The type checker is failing to properly resolve the complete slot inheritance chain.

This represents pyrefly being stricter than the established ecosystem standard, creating false positive noise without practical type safety value.

Attribution: The changes to effective_slots_for_instance_write() in pyrefly/lib/alt/attr.rs and check_class_field() in pyrefly/lib/alt/class/class_field.rs added new slots enforcement logic that validates attribute writes against declared slots.

pip (+1)

This is a REGRESSION. While the code has a genuine runtime bug (line 1323 assigns to line.plain but plain is not in the Text class's __slots__ list on lines 132-142), pyrefly is enforcing slots validation which is not a typing spec requirement and not enforced by mypy/pyright. The typing ecosystem treats slots as a runtime optimization mechanism, not a static type checking concern. The PR description acknowledges this is new enforcement ("Adds type checking enforcement for Python's __slots__ mechanism"), making pyrefly stricter than established tools. Even though this catches a real runtime bug, it creates false positive noise since the typing spec doesn't require this check.
Attribution: The change to effective_slots_for_instance_write() in pyrefly/lib/alt/attr.rs added slots enforcement logic that checks attribute writes against declared slots. The error occurs because the code tries to assign to line.plain where plain is not declared in the Text class's __slots__.

✅ Improvement (1)

dd-trace-py (+3)

These are genuine bugs being caught by pyrefly's new __slots__ enforcement. Looking at the Span class definition on lines 103-120, the __slots__ list contains attributes like _meta, context, _metrics, etc., but notably missing are duration_ns, service, and error. The code attempts to write to these undeclared attributes: (1) Line 231: self.duration_ns = finish_time_ns - ... - duration_ns is not in __slots__, (2) Line 298: self.service = value - service is not in __slots__, (3) Line 490: self.error = 1 - error is not in __slots__. At runtime, these assignments would raise AttributeError because Python's __slots__ mechanism prevents setting attributes not declared in the slots list. While mypy/pyright don't traditionally enforce __slots__ (focusing on type checking rather than runtime behavior), pyrefly is correctly identifying real runtime bugs where the code violates Python's __slots__ restrictions.
Attribution: The changes to pyrefly/lib/alt/attr.rs and pyrefly/lib/alt/class/class_field.rs added comprehensive __slots__ enforcement. The new effective_slots_for_instance_write() method checks if attributes are declared in __slots__ and reports missing-attribute errors when they're not. This enforcement applies to instance attribute writes in methods (class_field.rs) and external attribute assignments (attr.rs).

Suggested Fix

Summary: New slots enforcement is stricter than ecosystem standard, causing regressions in 3 projects despite catching genuine runtime bugs in others.

1. In effective_slots_for_instance_write() in pyrefly/lib/alt/attr.rs, add a configuration flag or make slots enforcement opt-in rather than default behavior. The current implementation enforces slots validation unconditionally, but the typing ecosystem (mypy/pyright) treats slots as a runtime optimization detail rather than a static type checking concern.

Files: pyrefly/lib/alt/attr.rs
Confidence: high
Affected projects: rich, core, pip
Fixes: missing-attribute
The slots enforcement logic was added in three places in attr.rs (lines ~946, ~983, ~1025) and is causing false positive noise. Making this configurable eliminates 5 missing-attribute errors across rich, core, and pip while preserving the ability to catch genuine bugs when users opt-in to stricter validation.

2. In check_class_field() in pyrefly/lib/alt/class/class_field.rs, guard the slots enforcement with the same configuration flag to ensure consistent behavior between instance attribute writes in methods and external attribute assignments.

Files: pyrefly/lib/alt/class/class_field.rs
Confidence: high
Affected projects: rich, core, pip
Fixes: missing-attribute
The class_field.rs changes also enforce slots validation for instance attribute writes within methods. This needs to be consistent with the attr.rs behavior to avoid confusing users with inconsistent enforcement. Expected outcome: eliminates remaining slots-related errors while maintaining the option for strict validation.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (4 LLM)

@migeed-z
Copy link
Copy Markdown
Contributor

migeed-z commented Mar 7, 2026

The mypy primer error delta looks a lot more reasonable this time! @rchen152 this looks reasonable to me. I tagged you for a quick look as well :)

@meta-codesync

This comment was marked as outdated.

migeed-z pushed a commit to migeed-z/pyrefly that referenced this pull request Mar 10, 2026
Summary:
Adds type checking enforcement for Python's `__slots__` mechanism. When a class defines `__slots__`, writing to attributes not declared in the slots now produces a `missing-attribute` error.

Fixes facebook#630.

Supported `__slots__` forms:
- Tuple literals: `__slots__ = ("x", "y")`
- List literals: `__slots__ = ["x", "y"]`
- Single string: `__slots__ = "x"`
- `dataclass(slots=True)`

Enforcement is correctly suppressed when:
- Any ancestor in the MRO is unslotted (inherits `__dict__`)
- `"__dict__"` appears in slots
- The class or an ancestor defines a custom `__setattr__`
- The attribute is accessed through a property setter or descriptor `__set__`

The implementation checks three code paths where attribute writes occur:
1. **Instance attribute creation** (`self.y = 2` in methods) — caught during class field validation in `class_field.rs`
2. **External writes to missing attrs** (`c.y = 3` where `y` is unknown) — caught in `attr.rs` via the `GetAttr`/not-found paths
3. **External writes to class-level attrs not in slots** (`c.y = 3` where `y: int` is a bare annotation) — caught in `attr.rs` via the `ClassAttribute` path


Test Plan:
- 17 targeted test cases covering enforcement, suppression, and edge cases
- `cargo test -p pyrefly test_slots` — all 17 pass
- `cargo test -p pyrefly -- attributes::` — all 175 pass, no regressions
- `cargo test -p pyrefly -- dataclasses::` — all 106 pass, no regressions

Differential Revision: D95826013

Pulled By: migeed-z
Copy link
Copy Markdown
Contributor

@rchen152 rchen152 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay! Agree that this approach looks reasonable. The one piece of big-picture feedback I'd give is that it looks like we have two different methods of slots extraction (extracting from the type and extracting from the expression). Other than dataclass(slots=True), are there any cases we want to support in which the slots can't be extracted from the expression? If not, IMO it would be simpler to just always do that, rather than having it as a fallback.

Copy link
Copy Markdown
Contributor

@yangdanny97 yangdanny97 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review automatically exported from Phabricator review in Meta.

Copy link
Copy Markdown
Contributor

@rchen152 rchen152 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review automatically exported from Phabricator review in Meta.

@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync bot commented Mar 11, 2026

@migeed-z merged this pull request in 70cb58d.

oopscompiled pushed a commit to oopscompiled/pyrefly that referenced this pull request Mar 13, 2026
Summary:
Adds type checking enforcement for Python's `__slots__` mechanism. When a class defines `__slots__`, writing to attributes not declared in the slots now produces a `missing-attribute` error.

Fixes facebook#630.

Supported `__slots__` forms:
- Tuple literals: `__slots__ = ("x", "y")`
- List literals: `__slots__ = ["x", "y"]`
- Single string: `__slots__ = "x"`
- `dataclass(slots=True)`

Enforcement is correctly suppressed when:
- Any ancestor in the MRO is unslotted (inherits `__dict__`)
- `"__dict__"` appears in slots
- The class or an ancestor defines a custom `__setattr__`
- The attribute is accessed through a property setter or descriptor `__set__`

The implementation checks three code paths where attribute writes occur:
1. **Instance attribute creation** (`self.y = 2` in methods) — caught during class field validation in `class_field.rs`
2. **External writes to missing attrs** (`c.y = 3` where `y` is unknown) — caught in `attr.rs` via the `GetAttr`/not-found paths
3. **External writes to class-level attrs not in slots** (`c.y = 3` where `y: int` is a bare annotation) — caught in `attr.rs` via the `ClassAttribute` path

Pull Request resolved: facebook#2695

Test Plan:
- 17 targeted test cases covering enforcement, suppression, and edge cases
- `cargo test -p pyrefly test_slots` — all 17 pass
- `cargo test -p pyrefly -- attributes::` — all 175 pass, no regressions
- `cargo test -p pyrefly -- dataclasses::` — all 106 pass, no regressions

Reviewed By: rchen152, yangdanny97

Differential Revision: D95907248

Pulled By: migeed-z

fbshipit-source-id: 17912ffadd2a7559129ec1c9c41b2e6257cd325d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: support __slots__

5 participants