[ty] Synthesize an empty __slots__ for named tuples (#22573)

## Summary

Closes https://github.com/astral-sh/ty/issues/2490.
This commit is contained in:
Charlie Marsh
2026-01-14 13:22:27 -05:00
committed by GitHub
parent 3e0299488e
commit eb96456e1e
2 changed files with 15 additions and 2 deletions

View File

@@ -845,6 +845,7 @@ class Person(NamedTuple):
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]]
reveal_type(Person.__slots__) # revealed: tuple[()]
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Person
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: (self: Self, *, name: str = ..., age: int | None = ...) -> Self
@@ -887,6 +888,8 @@ Person = namedtuple("Person", ["id", "name", "age"], defaults=[None])
alice = Person(1, "Alice", 42)
bob = Person(2, "Bob")
reveal_type(Person.__slots__) # revealed: tuple[()]
```
## `collections.namedtuple` with tuple variable field names

View File

@@ -3244,7 +3244,10 @@ impl<'db> StaticClassLiteral<'db> {
)
})
}
(CodeGeneratorKind::NamedTuple, "__new__" | "_replace" | "__replace__" | "_fields") => {
(
CodeGeneratorKind::NamedTuple,
"__new__" | "_replace" | "__replace__" | "_fields" | "__slots__",
) => {
let fields = self.fields(db, specialization, field_policy);
let fields_iter = fields.iter().map(|(name, field)| {
let default_ty = match &field.kind {
@@ -5212,6 +5215,10 @@ fn synthesize_namedtuple_class_member<'db>(
fields.map(|(field_name, _, _)| Type::string_literal(db, &field_name));
Some(Type::heterogeneous_tuple(db, field_types))
}
"__slots__" => {
// __slots__: tuple[()] - always empty for namedtuples
Some(Type::empty_tuple(db))
}
"_replace" | "__replace__" => {
if name == "__replace__" && Program::get(db).python_version(db) < PythonVersion::PY313 {
return None;
@@ -5536,7 +5543,10 @@ impl<'db> DynamicNamedTupleLiteral<'db> {
// For fallback members from NamedTupleFallback, apply type mapping to handle
// `Self` types. The explicitly synthesized members (__new__, _fields, _replace,
// __replace__) don't need this mapping.
if matches!(name, "__new__" | "_fields" | "_replace" | "__replace__") {
if matches!(
name,
"__new__" | "_fields" | "_replace" | "__replace__" | "__slots__"
) {
result
} else {
result.map(|ty| {