Skip to content

[ty] Emit diagnostic for NamedTuple and TypedDict decorated with dataclass#22672

Merged
charliermarsh merged 7 commits intomainfrom
charlie/data-decorator
Jan 18, 2026
Merged

[ty] Emit diagnostic for NamedTuple and TypedDict decorated with dataclass#22672
charliermarsh merged 7 commits intomainfrom
charlie/data-decorator

Conversation

@charliermarsh
Copy link
Member

Summary

Closes astral-sh/ty#2515.

Closes astral-sh/ty#2527.

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Jan 18, 2026
@charliermarsh charliermarsh force-pushed the charlie/data-decorator branch from e3555d1 to e2b3a1c Compare January 18, 2026 01:21
@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 18, 2026

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 18, 2026

mypy_primer results

Changes were detected when running on open source projects
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 46 diagnostics
+ Found 47 diagnostics

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/index.py:580:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[TVContainer_co@loc, TVDtype@Index]`, found `InterGetItemLocReduces[Any | Bottom[Series[Any, Any]], TVDtype@Index]`
+ static_frame/core/index.py:580:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[TVContainer_co@loc, TVDtype@Index]`, found `InterGetItemLocReduces[Bottom[Series[Any, Any]] | Any, TVDtype@Index]`
- 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]`

No memory usage changes detected ✅

@charliermarsh charliermarsh force-pushed the charlie/data-decorator branch from e2b3a1c to 47015ed Compare January 18, 2026 01:28
@charliermarsh charliermarsh marked this pull request as ready for review January 18, 2026 01:45
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Nice. I wonder if we should also flag enums and protocols decorated with @dataclass. Those also indicate that the user is probably very confused. And the enum docs explicitly lay out that adding @dataclass to an enum class is not supported


# TODO: This should emit `invalid-named-tuple` but currently emits `no-matching-overload`
# due to overload resolution not matching `DynamicNamedTuple` against `type[_T]`.
# error: [no-matching-overload]
Copy link
Member Author

Choose a reason for hiding this comment

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

This is turning out to be fairly annoying to fix.

Copy link
Member

Choose a reason for hiding this comment

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

Looks like it's a pre-existing issue, though? I think fine to defer it for this PR?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 Yeah that was my plan.

@charliermarsh charliermarsh force-pushed the charlie/data-decorator branch from 0615efb to d7ece7e Compare January 18, 2026 14:09
@charliermarsh charliermarsh marked this pull request as draft January 18, 2026 14:09
@charliermarsh charliermarsh force-pushed the charlie/data-decorator branch from d7ece7e to b450573 Compare January 18, 2026 14:23
@charliermarsh charliermarsh marked this pull request as ready for review January 18, 2026 14:34
/// Applying `@dataclass` to a class that inherits from `NamedTuple`, `TypedDict`,
/// `Enum`, or `Protocol` is invalid:
///
/// - `NamedTuple` and `TypedDict` classes will raise an exception at runtime when
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// - `NamedTuple` and `TypedDict` classes will raise an exception at runtime when
/// - `NamedTuple` and `TypedDict` classes will often raise an exception at runtime when

since you can avoid the TypedDict exception if you're very careful and use keyword arguments to instantiate the class:

>>> from typing import TypedDict
>>> from dataclasses import dataclass
>>> @dataclass
... class Foo(TypedDict):
...     x: int
...     
>>> Foo(x=42)
{'x': 42}

class.name(self.db()),
));
diagnostic.info(
"An exception will be raised when instantiating the class at runtime",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"An exception will be raised when instantiating the class at runtime",
"An exception may be raised when instantiating the class at runtime",

@charliermarsh charliermarsh force-pushed the charlie/data-decorator branch from a6579bc to 3fd1211 Compare January 18, 2026 15:31

### Protocol classes

Applying `@dataclass` to a protocol class is invalid because protocols define interfaces and cannot
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Applying `@dataclass` to a protocol class is invalid because protocols define interfaces and cannot
Applying `@dataclass` to a protocol class is invalid because protocols define abstract interfaces and cannot

y: str
```

The same error occurs with `dataclasses.dataclass` used as a function call:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
The same error occurs with `dataclasses.dataclass` used as a function call:
The same error occurs with `dataclasses.dataclass` used with parentheses:

Comment on lines 716 to 717
}
if class.is_typed_dict(self.db()) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
}
if class.is_typed_dict(self.db()) {
} else if class.is_typed_dict(self.db()) {

@charliermarsh charliermarsh enabled auto-merge (squash) January 18, 2026 17:14
@charliermarsh charliermarsh merged commit 57c98a1 into main Jan 18, 2026
48 checks passed
@charliermarsh charliermarsh deleted the charlie/data-decorator branch January 18, 2026 17:20
charliermarsh added a commit that referenced this pull request Jan 19, 2026
## Summary

Fixes some TODOs introduced in #22672 around cases like the following:


```python
from collections import namedtuple
from dataclasses import dataclass

NT = namedtuple("NT", "x y")

# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass(NT)
```

On main, `dataclass(NT)` emits `# error: [no-matching-overload]`
instead, which is wrong -- the overload does match! On main, the logic
proceeds as follows:

1. `dataclass` has two overloads:
  - `dataclass(cls: type[_T], ...) -> type[_T]`
  - `dataclass(cls: None = None, ...) -> Callable[[type[_T]], type[_T]]`
2. When `dataclass(NT)` is called:
- Arity check: Both overloads accept one positional argument, so both
pass.
- Type checking on first overload: `NT` matches `type[_T]`... but then
`invalid_dataclass_target()` runs and adds `InvalidDataclassApplication`
error
- Type checking on second overload: `NT` doesn't match `None`, so we
have a type error.
3. After type checking, both overloads have errors.
4. `matching_overload_index()` filters by
`overload.as_result().is_ok()`, which checks if `errors.is_empty()`.
Since both overloads have errors, neither matches...
5. We emit the "No overload matches arguments" error.

Instead, we now differentiate between non-matching errors, and errors
that occur when we _do_ match, but the call has some other semantic
failure.
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.

Emit a diagnostic on TypedDict classes decorated with @dataclass Emit a diagnostic on NamedTuple classes decorated with @dataclass

2 participants