Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ bar"""
reveal_type(len(())) # revealed: Literal[0]
reveal_type(len((1,))) # revealed: Literal[1]
reveal_type(len((1, 2))) # revealed: Literal[2]

# TODO: Handle constructor calls
reveal_type(len(tuple())) # revealed: int
reveal_type(len(tuple())) # revealed: Literal[0]

# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[],))) # revealed: Literal[1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,36 @@ specialization of `tuple` we (TODO: should) check that the values passed in matc
defined in the specialization.

```py
# TODO: revealed: tuple[()]
reveal_type(tuple()) # revealed: tuple[Unknown, ...]
# TODO: revealed: tuple[Literal[1]]
from typing_extensions import Iterable, Never

reveal_type(tuple()) # revealed: tuple[()]
reveal_type(tuple[int]((1,))) # revealed: tuple[int]
reveal_type(().__class__()) # revealed: tuple[()]
reveal_type((1, 2).__class__((1, 2))) # revealed: tuple[Literal[1], Literal[2]]

def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never]):
reveal_type(tuple(x)) # revealed: tuple[int, ...]
reveal_type(tuple(y)) # revealed: tuple[str, ...]
reveal_type(tuple(z)) # revealed: tuple[Unknown, ...]

# This is correct as the only inhabitants of `list[Never]` can be empty lists
reveal_type(tuple(aa)) # revealed: tuple[()]

reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]]

# TODO: should be `tuple[Literal[1], ...]`
reveal_type(tuple([1])) # revealed: tuple[Unknown, ...]

# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int]`, found `list[Unknown]`"
reveal_type(tuple[int]([1])) # revealed: tuple[int]
# TODO: error for invalid arguments
reveal_type(tuple[int, str]([1])) # revealed: tuple[int, str]

reveal_type(().__class__()) # revealed: tuple[()]
# TODO: error for invalid arguments
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int, str]`, found `tuple[Literal[1]]`"
reveal_type(tuple[int, str]((1,))) # revealed: tuple[int, str]

# error: [missing-argument] "No argument provided for required parameter `iterable`"
reveal_type((1,).__class__()) # revealed: tuple[Literal[1]]
# TODO: error for invalid arguments

# error: [missing-argument] "No argument provided for required parameter `iterable`"
reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]]
```

Expand Down
50 changes: 44 additions & 6 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,13 @@ impl<'db> Type<'db> {
matches!(self, Type::ClassLiteral(..))
}

pub(crate) const fn into_tuple(self) -> Option<TupleType<'db>> {
match self {
Type::Tuple(tuple_type) => Some(tuple_type),
_ => None,
}
}

/// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`.
/// Since a `ClassType` must be specialized, apply the default specialization to any
/// unspecialized generic class literal.
Expand Down Expand Up @@ -4237,6 +4244,27 @@ impl<'db> Type<'db> {
.into()
}

Some(KnownClass::Tuple) => {
let object = Type::object(db);

CallableBinding::from_overloads(
self,
[
Signature::new(Parameters::empty(), Some(TupleType::empty(db))),
Signature::new(
Parameters::new([Parameter::positional_only(Some(
Name::new_static("iterable"),
))
.with_annotated_type(
KnownClass::Iterable.to_specialized_instance(db, [object]),
)]),
Some(TupleType::homogeneous(db, object)),
),
],
)
.into()
}

// Most class literal constructor calls are handled by `try_call_constructor` and
// not via getting the signature here. This signature can still be used in some
// cases (e.g. evaluating callable subtyping). TODO improve this definition
Expand Down Expand Up @@ -4276,14 +4304,24 @@ impl<'db> Type<'db> {
.into()
}

Type::GenericAlias(_) => {
Type::GenericAlias(alias) => {
let instantiated = Type::instance(db, ClassType::from(alias));

let parameters = if alias.origin(db).is_known(db, KnownClass::Tuple) {
Copy link
Member

Choose a reason for hiding this comment

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

I think I follow the constructors here, but can you put a comment with the Python syntax of the signature you're trying to construct here?

let spec = alias.specialization(db).tuple(db);
let mut parameter =
Parameter::positional_only(Some(Name::new_static("iterable")))
.with_annotated_type(instantiated);
Copy link
Member

Choose a reason for hiding this comment

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

With the Tuple::resize method that #18948 introduces, we can make this work for any tuple, not just one with exactly the same size as the type being constructed. I also have plans to have try_iterator return a tuple spec describing the return values of the iterator, which would then give us what we need for this to work for any iterable type.

Copy link
Member Author

Choose a reason for hiding this comment

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

With the Tuple::resize method that #18948 introduces, we can make this work for any tuple, not just one with exactly the same size as the type being constructed.

Hmm, not sure I totally follow this. Can you give an example of something that should be covered in this PR but isn't? :-)

Copy link
Member

@dcreager dcreager Jun 27, 2025

Choose a reason for hiding this comment

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

Sorry, wasn't requesting any changes here with this comment, just leaving me/others a breadcrumb for a future improvement

if matches!(spec.size_hint().1, Some(0)) {
parameter = parameter.with_default_type(TupleType::empty(db));
}
Parameters::new([parameter])
} else {
Parameters::gradual_form()
};
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
Binding::single(
self,
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
)
.into()
Binding::single(self, Signature::new(parameters, Some(instantiated))).into()
}

Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
Expand Down
22 changes: 22 additions & 0 deletions crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,28 @@ impl<'db> Bindings<'db> {
}
}

Some(KnownClass::Tuple) if overload_index == 1 => {
if let [Some(argument)] = overload.parameter_types() {
let overridden_return =
argument.into_tuple().map(Type::Tuple).unwrap_or_else(|| {
Copy link
Member

Choose a reason for hiding this comment

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

I think this would avoid unwrapping the argument type just to then rewrap it:

Suggested change
argument.into_tuple().map(Type::Tuple).unwrap_or_else(|| {
argument.filter(Type::is_tuple).unwrap_or_else(|| {

(though it depends on an is_tuple method that may not exist yet!)

Copy link
Member Author

Choose a reason for hiding this comment

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

argument is a type rather than an Option here so I think this would have to be

Suggested change
argument.into_tuple().map(Type::Tuple).unwrap_or_else(|| {
argument.into_tuple().filter(Type::is_tuple).unwrap_or_else(|| {

Adding a Type::is_tuple method just for that feels a bit unnecessary to me? No strong opinion though

// Some awkward special handling is required here because of the fact
// that calling `try_iterate()` on `Never` returns `Never`,
// but `tuple[Never, ...]` eagerly simplifies to `tuple[()]`,
// which will cause us to emit false positives if we index into the tuple
let specialization = if argument.is_never() {
Type::unknown()
} else {
argument.try_iterate(db).expect(
"try_iterate() should not fail on a type \
assignable to `Iterable`",
)
};
TupleType::homogeneous(db, specialization)
});
overload.set_return_type(overridden_return);
}
}

_ => {}
},

Expand Down
11 changes: 10 additions & 1 deletion crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2334,6 +2334,7 @@ pub enum KnownClass {
NamedTuple,
NewType,
SupportsIndex,
Iterable,
// Collections
ChainMap,
Counter,
Expand Down Expand Up @@ -2426,6 +2427,7 @@ impl KnownClass {
| Self::Float
| Self::Enum
| Self::ABCMeta
| KnownClass::Iterable
Copy link
Member

Choose a reason for hiding this comment

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

nit: Self::Iterable to be consistent with the other arms

// Empty tuples are AlwaysFalse; non-empty tuples are AlwaysTrue
| Self::NamedTuple
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
Expand Down Expand Up @@ -2513,6 +2515,7 @@ impl KnownClass {
| Self::DefaultDict
| Self::OrderedDict
| Self::NewType
| Self::Iterable
| Self::BaseExceptionGroup => false,
}
}
Expand All @@ -2531,7 +2534,7 @@ impl KnownClass {
/// 2. It's probably more performant.
const fn is_protocol(self) -> bool {
match self {
Self::SupportsIndex => true,
Self::SupportsIndex | Self::Iterable => true,

Self::Any
| Self::Bool
Expand Down Expand Up @@ -2648,6 +2651,7 @@ impl KnownClass {
Self::Enum => "Enum",
Self::ABCMeta => "ABCMeta",
Self::Super => "super",
Self::Iterable => "Iterable",
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
Self::StdlibAlias => "_Alias",
// This is the name the type of `sys.version_info` has in typeshed,
Expand Down Expand Up @@ -2882,6 +2886,7 @@ impl KnownClass {
| Self::TypeVar
| Self::NamedTuple
| Self::StdlibAlias
| Self::Iterable
| Self::SupportsIndex => KnownModule::Typing,
Self::TypeAliasType
| Self::TypeVarTuple
Expand Down Expand Up @@ -2984,6 +2989,7 @@ impl KnownClass {
| Self::NewType
| Self::Field
| Self::KwOnly
| Self::Iterable
| Self::NamedTupleFallback => false,
}
}
Expand Down Expand Up @@ -3052,6 +3058,7 @@ impl KnownClass {
| Self::NewType
| Self::Field
| Self::KwOnly
| Self::Iterable
| Self::NamedTupleFallback => false,
}
}
Expand Down Expand Up @@ -3101,6 +3108,7 @@ impl KnownClass {
"NewType" => Self::NewType,
"TypeAliasType" => Self::TypeAliasType,
"TypeVar" => Self::TypeVar,
"Iterable" => Self::Iterable,
"ParamSpec" => Self::ParamSpec,
"ParamSpecArgs" => Self::ParamSpecArgs,
"ParamSpecKwargs" => Self::ParamSpecKwargs,
Expand Down Expand Up @@ -3197,6 +3205,7 @@ impl KnownClass {
| Self::ParamSpecKwargs
| Self::TypeVarTuple
| Self::NamedTuple
| Self::Iterable
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5343,6 +5343,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| KnownClass::TypeVar
| KnownClass::NamedTuple
| KnownClass::TypeAliasType
| KnownClass::Tuple
)
)
// temporary special-casing for all subclasses of `enum.Enum`
Expand Down
Loading