Skip to content

Comments

[ty] diagnostic on overridden __setattr__ and __delattr__ in frozen dataclasses#21430

Merged
carljm merged 3 commits intoastral-sh:mainfrom
thejchap:thejchap/frozen-setattr
Jan 17, 2026
Merged

[ty] diagnostic on overridden __setattr__ and __delattr__ in frozen dataclasses#21430
carljm merged 3 commits intoastral-sh:mainfrom
thejchap:thejchap/frozen-setattr

Conversation

@thejchap
Copy link
Contributor

@thejchap thejchap commented Nov 13, 2025

Summary

astral-sh/ty#111

this pr adds an invalid-dataclass-override diagnostic when a custom __setattr__ or __delattr__ is defined on a dataclass where frozen=True (docs)

Runtime exception

Traceback (most recent call last):
  File "/Users/justinchapman/src/ty-playground/main.py", line 4, in <module>
    @dataclass(frozen=True)
     ~~~~~~~~~^^^^^^^^^^^^^
  File "/Users/justinchapman/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13/dataclasses.py", line 1295, in wrap
    return _process_class(cls, init, repr, eq, order, unsafe_hash,
                          frozen, match_args, kw_only, slots,
                          weakref_slot)
  File "/Users/justinchapman/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13/dataclasses.py", line 1157, in _process_class
    func_builder.add_fns_to_class(cls)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
  File "/Users/justinchapman/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13/dataclasses.py", line 516, in add_fns_to_class
    raise TypeError(error_msg)
TypeError: Cannot overwrite attribute __setattr__ in class A

Diagnostic

error[invalid-dataclass-override]: Cannot overwrite attribute __setattr__ in class A
 --> /Users/justinchapman/src/ty-playground/main.py:6:5
  |
4 | @dataclass(frozen=True)
5 | class A:
6 |     def __setattr__(self, name: str, value: object) -> None: ...
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
info: __setattr__
info: rule `invalid-dataclass-override` is enabled by default

Found 1 diagnostic

Test Plan

  • new mdtests
  • e2e
  • the attrs mypy primer diff looks to be a true positive - the other results have been unpredictable and have changed every time i pushed new code, even if the diagnostic logic didn't change...

@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 13, 2025

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 13, 2025

mypy_primer results

Changes were detected when running on open source projects
attrs (https://github.com/python-attrs/attrs)
+ tests/test_setattr.py:374:17: error[invalid-dataclass-override] Cannot overwrite attribute `__setattr__` in class `CustomSetAttr`
- Found 627 diagnostics
+ Found 628 diagnostics

prefect (https://github.com/PrefectHQ/prefect)
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any]` is not assignable to `dict[str, Any]`
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `T@resolve_variables | str | int | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `T@resolve_variables | dict[str, Any]` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any]` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `T@resolve_variables | str | int | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `T@resolve_variables` is not assignable to `dict[str, Any]`
- src/prefect/deployments/runner.py:795:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | ((...) -> Any)`
+ src/prefect/deployments/runner.py:795:70: warning[possibly-missing-attribute] Attribute `__name__` may be missing on object of type `Unknown | (((...) -> Any) & ((*args: object, **kwargs: object) -> object))`
- src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements`
+ src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any]`
- src/prefect/flow_engine.py:812:32: error[invalid-await] `Unknown | R@FlowRunEngine | Coroutine[Any, Any, R@FlowRunEngine]` is not awaitable
- src/prefect/flow_engine.py:1401:24: error[invalid-await] `Unknown | R@AsyncFlowRunEngine | Coroutine[Any, Any, R@AsyncFlowRunEngine]` is not awaitable
- src/prefect/flow_engine.py:1482:43: error[invalid-argument-type] Argument to function `next` is incorrect: Expected `SupportsNext[Unknown]`, found `Unknown | R@run_generator_flow_sync`
- src/prefect/flow_engine.py:1490:21: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_sync`
- src/prefect/flow_engine.py:1524:44: warning[possibly-missing-attribute] Attribute `__anext__` may be missing on object of type `Unknown | R@run_generator_flow_async`
- src/prefect/flow_engine.py:1531:25: warning[possibly-missing-attribute] Attribute `throw` may be missing on object of type `Unknown | R@run_generator_flow_async`
- src/prefect/flows.py:286:34: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
+ src/prefect/flows.py:286:34: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
- src/prefect/flows.py:404:68: error[unresolved-attribute] Object of type `(**P@Flow) -> R@Flow` has no attribute `__name__`
+ src/prefect/flows.py:404:68: error[unresolved-attribute] Object of type `((**P@Flow) -> R@Flow) & ((*args: object, **kwargs: object) -> object)` has no attribute `__name__`
+ src/prefect/flows.py:1750:53: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` on object of type `dict[str, Any]`
+ src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | dict[str, Any]` on object of type `dict[str, Any]`
- src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[T@resolve_block_document_references | dict[str, Any] | str | ... omitted 5 union elements]`
+ src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[T@resolve_block_document_references | dict[str, Any] | Unknown]`
- src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, T@resolve_variables | str | int | ... omitted 5 union elements]`
+ src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, T@resolve_variables | Unknown]`
- src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[T@resolve_variables | str | int | ... omitted 5 union elements]`
+ src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[T@resolve_variables | Unknown]`
- src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements`
+ src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any]`
- src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `T@resolve_variables | str | int | ... omitted 4 union elements`
+ src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `T@resolve_variables`
- Found 5411 diagnostics
+ Found 5406 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/build/wheel.py:99:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 47 diagnostics
+ Found 46 diagnostics

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/bus.py:671:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[Bus[Any], object_]`, found `InterGetItemLocReduces[Bus[Any] | Bottom[Series[Any, Any]] | ndarray[Never, Never] | ... omitted 6 union elements, object_]`
+ static_frame/core/bus.py:671:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[Bus[Any], object_]`, found `InterGetItemLocReduces[Bus[Any] | Bottom[Index[Any]] | TypeBlocks | ... omitted 6 union elements, object_]`
- static_frame/core/node_selector.py:526:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[TVContainer_co@InterfaceSelectQuartet, Any]`, found `InterGetItemLocReduces[Bottom[Series[Any, Any]] | Unknown, Any]`
+ static_frame/core/node_selector.py:526:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[TVContainer_co@InterfaceSelectQuartet, Any]`, found `InterGetItemLocReduces[Unknown | Bottom[Series[Any, Any]], Any]`
- static_frame/core/series.py:772:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Series[Any, Any], TVDtype@Series]`, found `InterGetItemILocReduces[Series[Any, Any] | Bottom[Index[Any]] | TypeBlocks | ... omitted 6 union elements, TVDtype@Series]`
+ static_frame/core/series.py:772:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Series[Any, Any], TVDtype@Series]`, found `InterGetItemILocReduces[Series[Any, Any] | ndarray[Never, Never] | TypeBlocks | ... omitted 6 union elements, TVDtype@Series]`
- static_frame/core/yarn.py:418:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Yarn[Any], object_]`, found `InterGetItemILocReduces[Yarn[Any] | Bottom[Index[Any]] | TypeBlocks | ... omitted 6 union elements, object_]`
+ static_frame/core/yarn.py:418:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Yarn[Any], object_]`, found `InterGetItemILocReduces[Yarn[Any] | ndarray[Never, Never] | TypeBlocks | ... omitted 6 union elements, object_]`

rotki (https://github.com/rotki/rotki)
- rotkehlchen/chain/decoding/tools.py:96:44: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- rotkehlchen/chain/decoding/tools.py:99:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `Sequence[A@BaseDecoderTools]`, found `Unknown | tuple[BTCAddress, ...] | tuple[ChecksumAddress, ...] | tuple[SubstrateAddress, ...] | tuple[SolanaAddress, ...]`
- rotkehlchen/chain/decoding/tools.py:100:62: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ rotkehlchen/chain/decoding/tools.py:97:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `BTCAddress | ChecksumAddress | SubstrateAddress | SolanaAddress`, found `A@BaseDecoderTools`
+ rotkehlchen/chain/decoding/tools.py:98:13: error[invalid-argument-type] Argument to function `decode_transfer_direction` is incorrect: Expected `BTCAddress | ChecksumAddress | SubstrateAddress | SolanaAddress | None`, found `A@BaseDecoderTools | None`
- Found 2057 diagnostics
+ Found 2056 diagnostics

core (https://github.com/home-assistant/core)
- homeassistant/util/variance.py:47:12: error[invalid-return-type] Return type does not match returned value: expected `(**_P@ignore_variance) -> _R@ignore_variance`, found `_Wrapped[_P@ignore_variance, _R@ignore_variance | int | float | datetime, _P@ignore_variance, _R@ignore_variance | int | float | datetime]`
- Found 14492 diagnostics
+ Found 14491 diagnostics

No memory usage changes detected ✅

@thejchap thejchap force-pushed the thejchap/frozen-setattr branch from dad0985 to 5c3f14a Compare November 15, 2025 05:58
@thejchap thejchap force-pushed the thejchap/frozen-setattr branch from 5c3f14a to 3272ab1 Compare November 30, 2025 02:24
@thejchap thejchap force-pushed the thejchap/frozen-setattr branch 10 times, most recently from b7e81c1 to bcf0226 Compare December 8, 2025 00:35
@thejchap thejchap marked this pull request as ready for review December 8, 2025 00:39
@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Dec 8, 2025
@sharkdp
Copy link
Contributor

sharkdp commented Dec 9, 2025

Thank you for your contribution. I did not have time to review this in detail yet. One question upfront: would it be reasonable to use the existing invalid-override diagnostic here instead?

@thejchap
Copy link
Contributor Author

thejchap commented Dec 9, 2025

@sharkdp i am not sure - one thing that seems like it could be nice over time is to have better diagnostic info for this specific case (for example highlighting the frozen argument similar to the runtime error) which wouldn't be applicable to the more generic LSP violation that invalid-method-override is referring to

i will defer to you though - does seem reasonable!

@AlexWaygood
Copy link
Member

I think it's better to have a different error code here. invalid-method-override is specifically about Liskov violations, which this isn't. I've been wondering about renaming it to unsound-method-override, since there are obviously lots of other ways in which method overrides can be invalid.

@AlexWaygood
Copy link
Member

I think it would be good to give the error code a name that makes it clear it's specifically about dataclasses, though, which the documentation of the rule makes clear

@carljm
Copy link
Contributor

carljm commented Dec 9, 2025

We may add similar rules in future for things you're not allowed to override on a tuple subclass. Would we want those to use the same rule as the dataclass case, or a distinct rule for tuples? That may influence how we name this rule.

@carljm
Copy link
Contributor

carljm commented Dec 9, 2025

(Having said that, I think maybe a different rule, since the tuple case won't be things that raise at runtime, just things that could cause unsoundness. So it would be reasonable to want to disable the one rule but not the other; and they should maybe have different severities. So I think I'd support naming this rule in a dataclass-specific way.)

@thejchap thejchap force-pushed the thejchap/frozen-setattr branch from bcf0226 to d76d6ca Compare December 11, 2025 02:44
@thejchap thejchap marked this pull request as draft December 11, 2025 02:44
@thejchap thejchap force-pushed the thejchap/frozen-setattr branch 4 times, most recently from a576b6e to 7611878 Compare December 11, 2025 03:23
@thejchap
Copy link
Contributor Author

@AlexWaygood took another stab at naming based on how i interpreted your and Carl's comments

cc @carljm - i wasn't sure the best way to pull these comments over from #21820. i did refactor has_dataclass_param to be a method on ClassLiteral instead of a closure in own_synthesized_member, but i didn't change any of the actual logic - if you want to elaborate a bit more on the refactor you had in mind i can either try to implement it here, or just add a comment

@thejchap thejchap marked this pull request as ready for review December 11, 2025 03:23
@thejchap thejchap force-pushed the thejchap/frozen-setattr branch from 7611878 to a1cdc0c Compare December 18, 2025 11:05
@AlexWaygood
Copy link
Member

Thank you! Sorry for the back-and-forth on naming... maybe invalid-dataclass-override might be good?

  • The name clearly indicates that it's dataclass-related
  • But it's a bit shorter than unsound-dataclass-method-override
  • And I'm not sure "unsound" is the best adjective to use here, because to me it implies that this is detecting sort-of an abstract theoretical issue rather than something that would raise TypeError at runtime. That's why I want to rename Liskov violations to unsound-method-override -- because the error code that detects Liskov violations is kinda abstract and theoretical. But this error code is different to the Liskov error code -- it's not really abstract and theoretical, it's detecting something that's going to cause TypeError to always be raised at runtime. So the stronger adjective invalid seems more fitting here.

@thejchap thejchap force-pushed the thejchap/frozen-setattr branch from a1cdc0c to c6148aa Compare December 22, 2025 06:26
@thejchap
Copy link
Contributor Author

@AlexWaygood ah! thanks for clarifying. sounds good - done

@carljm
Copy link
Contributor

carljm commented Dec 24, 2025

@thejchap sorry for the slow response time! We've been getting swamped since the beta and this got lost in my notifications. I would say don't worry about those comments for this PR, let's just get the functionality right, better to separate things when separable. Those comments are mostly about making sure we handle various kinds of dataclass-transforms correctly, and that's kind of orthogonal to this PR anyway.

@thejchap
Copy link
Contributor Author

Sounds good - I think this is ready for re-review then - let me know if I'm missing anything!

@MichaReiser
Copy link
Member

Thank you for your work on feature. Almost the entire team is out this week. It may take a few days before someone finds time to review your PR. Happy holidays.

@thejchap
Copy link
Contributor Author

@MichaReiser thanks! Happy holidays

@thejchap thejchap force-pushed the thejchap/frozen-setattr branch 2 times, most recently from 6064263 to 6036d5a Compare January 7, 2026 02:38
@thejchap
Copy link
Contributor Author

@AlexWaygood @MichaReiser hi! Just wanted to check on this one - I had been interested in continuing down this road on the diagnostics for overridden comparison dunder methods on order=True dataclasses as well so was curious on any feedback on the direction in this PR

@AlexWaygood
Copy link
Member

Sorry, we still have a bit of a PR backlog from the Christmas holidays :-(

I haven't forgotten about this PR and I promise we will get to it. I've been trying to munch through review of as many PRs as possible this week.

@thejchap
Copy link
Contributor Author

Ah no worries! Thanks for the update

@carljm carljm force-pushed the thejchap/frozen-setattr branch from 6036d5a to 9e23d3d Compare January 17, 2026 02:07
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Looks great! Sorry again for the really slow review, we definitely aim to do better than this. This PR just hit the exact wrong time frame with beta release and then holidays :)

@carljm carljm enabled auto-merge (squash) January 17, 2026 02:23
@carljm carljm merged commit 938c1c5 into astral-sh:main Jan 17, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants