Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ty] Diagnose zero-step slices on lists#25966

Merged
charliermarsh merged 3 commits into
mainfrom
charlie/fix-list-zero-slice-step
Jun 14, 2026
Merged

[ty] Diagnose zero-step slices on lists#25966
charliermarsh merged 3 commits into
mainfrom
charlie/fix-list-zero-slice-step

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Jun 13, 2026

Copy link
Copy Markdown
Member

Summary

A slice with a step size of zero always fails for an exact list, but we currently only report zero-stepsize-in-slice for string, bytes, and tuple literals:

values = list(range(10))
result = values[1:10:0]  # error: [zero-stepsize-in-slice]
reveal_type(result)  # list[int]

We now report the diagnostic when the receiver type is list or another known built-in while preserving the inferred result type for error recovery.

(A hypothetical subclass could override __getitem__ to accept a zero step, but the list type includes exact list instances that are known to reject it. Subclasses with their own nominal type and custom sequence types continue through normal subscript dispatch without this diagnostic.)

Closes astral-sh/ty#3736.

@astral-sh-bot

astral-sh-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 94.36%. The percentage of expected errors that received a diagnostic held steady at 88.91%. The number of fully passing files held steady at 93/134.

@charliermarsh charliermarsh force-pushed the charlie/fix-list-zero-slice-step branch from c83b6c6 to 4545d0a Compare June 13, 2026 22:17
@astral-sh-bot

astral-sh-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
flake8 33.97MB 33.97MB -0.01% (3.36kB) ⬇️
sphinx 194.03MB 194.03MB -0.00% (4.83kB) ⬇️
trio 82.96MB 82.95MB -0.02% (15.54kB) ⬇️
prefect 524.56MB 524.53MB -0.01% (30.38kB) ⬇️

Significant changes

Click to expand detailed breakdown

flake8

Name Old New Diff Outcome
StaticClassLiteral<'db>::try_mro_ 288.92kB 287.32kB -0.55% (1.60kB) ⬇️
StaticClassLiteral<'db>::try_mro_::interned_arguments 72.14kB 71.79kB -0.49% (360.00B) ⬇️
Specialization 170.36kB 170.03kB -0.19% (336.00B) ⬇️
infer_definition_types 1.35MB 1.35MB -0.02% (276.00B) ⬇️
is_typed_dict_inner 17.95kB 17.73kB -1.24% (228.00B) ⬇️
GenericAlias 74.18kB 73.97kB -0.28% (216.00B) ⬇️
enum_metadata 55.87kB 55.69kB -0.31% (180.00B) ⬇️
infer_expression_types_impl 956.05kB 955.98kB -0.01% (72.00B) ⬇️
infer_deferred_types 445.72kB 445.68kB -0.01% (48.00B) ⬇️
Type<'db>::class_member_with_policy_ 340.72kB 340.69kB -0.01% (36.00B) ⬇️
code_generator_of_static_class 21.57kB 21.56kB -0.05% (12.00B) ⬇️
FunctionType<'db>::signature_ 272.11kB 272.10kB -0.00% (12.00B) ⬇️
lookup_dunder_new_inner 13.78kB 13.77kB -0.09% (12.00B) ⬇️
member_lookup_with_policy_inner 423.00kB 422.99kB -0.00% (12.00B) ⬇️

sphinx

Name Old New Diff Outcome
infer_definition_types 18.50MB 18.50MB -0.02% (2.87kB) ⬇️
infer_expression_types_impl 20.02MB 20.02MB -0.00% (828.00B) ⬇️
StaticClassLiteral<'db>::try_mro_ 2.21MB 2.21MB -0.02% (352.00B) ⬇️
is_typed_dict_inner 120.57kB 120.34kB -0.18% (228.00B) ⬇️
enum_metadata 568.23kB 568.05kB -0.03% (180.00B) ⬇️
infer_deferred_types 4.03MB 4.03MB -0.00% (84.00B) ⬇️
StaticClassLiteral<'db>::try_mro_::interned_arguments 550.76kB 550.69kB -0.01% (72.00B) ⬇️
Type<'db>::class_member_with_policy_ 4.96MB 4.96MB -0.00% (72.00B) ⬇️
FunctionType<'db>::signature_ 1.74MB 1.74MB -0.00% (48.00B) ⬇️
member_lookup_with_policy_inner 5.87MB 5.87MB -0.00% (48.00B) ⬇️
infer_scope_types_impl 10.53MB 10.53MB -0.00% (36.00B) ⬇️
code_generator_of_static_class 175.72kB 175.71kB -0.01% (12.00B) ⬇️
function_known_decorators 929.63kB 929.62kB -0.00% (12.00B) ⬇️
StaticClassLiteral<'db>::implicit_attribute_inner_ 779.60kB 779.59kB -0.00% (12.00B) ⬇️
lookup_dunder_new_inner 81.51kB 81.50kB -0.01% (12.00B) ⬇️
... 1 more

trio

Name Old New Diff Outcome
infer_definition_types 5.56MB 5.56MB -0.10% (5.77kB) ⬇️
infer_expression_types_impl 6.45MB 6.45MB -0.07% (4.88kB) ⬇️
Type<'db>::apply_specialization_inner_ 509.46kB 508.41kB -0.21% (1.05kB) ⬇️
Type<'db>::class_member_with_policy_ 1.23MB 1.23MB -0.05% (620.00B) ⬇️
infer_scope_types_impl 3.19MB 3.19MB -0.02% (600.00B) ⬇️
is_redundant_with_impl 173.24kB 172.71kB -0.30% (540.00B) ⬇️
loop_header_reachability 124.46kB 124.03kB -0.35% (444.00B) ⬇️
enum_metadata 204.98kB 204.59kB -0.19% (396.00B) ⬇️
infer_deferred_types 1.80MB 1.80MB -0.02% (384.00B) ⬇️
is_typed_dict_inner 42.89kB 42.66kB -0.52% (228.00B) ⬇️
member_lookup_with_policy_inner 1.48MB 1.48MB -0.01% (200.00B) ⬇️
all_narrowing_constraints_for_expression 621.85kB 621.66kB -0.03% (192.00B) ⬇️
FunctionType<'db>::signature_ 724.35kB 724.23kB -0.02% (120.00B) ⬇️
member_lookup_with_policy_inner::interned_arguments 761.60kB 761.48kB -0.02% (120.00B) ⬇️
Type<'db>::class_member_with_policy_::interned_arguments 612.42kB 612.32kB -0.02% (104.00B) ⬇️
... 3 more

prefect

Name Old New Diff Outcome
infer_definition_types 69.67MB 69.65MB -0.03% (22.59kB) ⬇️
infer_expression_types_impl 53.67MB 53.67MB -0.01% (6.09kB) ⬇️
StaticClassLiteral<'db>::try_mro_ 4.29MB 4.29MB -0.01% (352.00B) ⬇️
all_narrowing_constraints_for_expression 6.84MB 6.84MB -0.00% (276.00B) ⬇️
is_typed_dict_inner 379.77kB 379.55kB -0.06% (228.00B) ⬇️
infer_scope_types_impl 38.92MB 38.92MB -0.00% (192.00B) ⬇️
enum_metadata 2.27MB 2.27MB -0.01% (180.00B) ⬇️
Type<'db>::class_member_with_policy_ 10.87MB 10.87MB -0.00% (120.00B) ⬇️
infer_deferred_types 8.84MB 8.84MB -0.00% (84.00B) ⬇️
StaticClassLiteral<'db>::try_mro_::interned_arguments 1.14MB 1.14MB -0.01% (72.00B) ⬇️
FunctionType<'db>::signature_ 2.85MB 2.85MB -0.00% (60.00B) ⬇️
member_lookup_with_policy_inner 14.14MB 14.14MB -0.00% (48.00B) ⬇️
infer_unpack_types 970.12kB 970.09kB -0.00% (36.00B) ⬇️
infer_statement_types_impl 859.96kB 859.92kB -0.00% (36.00B) ⬇️
code_generator_of_static_class 590.27kB 590.25kB -0.00% (12.00B) ⬇️
... 3 more

@astral-sh-bot

astral-sh-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Full report with detailed diff (timing results)

@AlexWaygood

AlexWaygood commented Jun 14, 2026

Copy link
Copy Markdown
Member

Hmm, to me it seems strange to special-case list here, but not other builtin sequences such as bytearray, memoryview, range, bytes, or str:

>>> bytearray(b"foo")[0:2:0]
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    bytearray(b"foo")[0:2:0]
    ~~~~~~~~~~~~~~~~~^^^^^^^
ValueError: slice step cannot be zero
>>> memoryview(b"foo")[0:2:0]
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    memoryview(b"foo")[0:2:0]
    ~~~~~~~~~~~~~~~~~~^^^^^^^
ValueError: slice step cannot be zero
>>> range(42)[0:2:0]
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    range(42)[0:2:0]
    ~~~~~~~~~^^^^^^^
ValueError: slice step cannot be zero

we don't emit errors for any of these on your branch, yet they will all fail at runtime for at least some inhabitants of the type:

bytearray(b"foo")[0:2:0]
memoryview(b"foo")[0:2:0]
range(42)[0:2:0]

def f(x: str, y: bytes):
    x[1:1:0]
    y[1:1:0]

The existing places where we emit zero-stepsize-in-slice are all places where we already have heavy special casing for slicing for those sequences, and so it's "impossible to ignore" it when a user passes a stepsize of 0 to the slice. I don't mind extending the lint rule to other builtin types, but extending it only to list feels oddly specific to me. And we should probably more clearly explain in the documentation that this rule does not attempt to be exhaustive (and could not even if it wanted to).

@charliermarsh

Copy link
Copy Markdown
Member Author

I broadened it.

Comment on lines +526 to +536
fn builtin_sequence_rejects_zero_step<'db>(
db: &'db dyn Db,
instance: NominalInstanceType<'db>,
) -> bool {
let class = instance.class_literal(db);
matches!(
class.name(db).as_str(),
"list" | "bytearray" | "memoryview" | "range" | "bytes" | "str"
) && file_to_module(db, class.file(db))
.is_some_and(|module| module.is_known(db, KnownModule::Builtins))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

sort-of the whole point of having the KnownClass enum is so that that we don't have to do ad-hoc matches on strings everywhere like this. So I'd just add KnownClass::Range and KnownClass::Memoryview variants -- then this can be

diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs
index 1e47fa6980..6ddc18f1d0 100644
--- a/crates/ty_python_semantic/src/types/subscript.rs
+++ b/crates/ty_python_semantic/src/types/subscript.rs
@@ -523,18 +523,6 @@ fn typed_dict_subscript<'db>(
     )
 }

-fn builtin_sequence_rejects_zero_step<'db>(
-    db: &'db dyn Db,
-    instance: NominalInstanceType<'db>,
-) -> bool {
-    let class = instance.class_literal(db);
-    matches!(
-        class.name(db).as_str(),
-        "list" | "bytearray" | "memoryview" | "range" | "bytes" | "str"
-    ) && file_to_module(db, class.file(db))
-        .is_some_and(|module| module.is_known(db, KnownModule::Builtins))
-}
-
 impl<'db> Type<'db> {
     pub(super) fn subscript(
         self,
@@ -599,7 +587,18 @@ impl<'db> Type<'db> {
             (
                 Type::NominalInstance(maybe_sequence_nominal),
                 Type::NominalInstance(maybe_slice_nominal),
-            ) if builtin_sequence_rejects_zero_step(db, maybe_sequence_nominal)
+            ) if matches!(
+                maybe_sequence_nominal.known_class(db),
+                Some(
+                    KnownClass::List
+                    | KnownClass::Tuple
+                    | KnownClass::Str
+                    | KnownClass::Bytes
+                    | KnownClass::Bytearray
+                    | KnownClass::Range
+                    | KnownClass::Memoryview
+                )
+            )
                 && maybe_slice_nominal
                     .slice_literal(db)
                     .is_some_and(|slice| slice.step == Some(0)) =>

@charliermarsh charliermarsh enabled auto-merge (squash) June 14, 2026 23:05
@charliermarsh charliermarsh merged commit 09781e4 into main Jun 14, 2026
58 checks passed
@charliermarsh charliermarsh deleted the charlie/fix-list-zero-slice-step branch June 14, 2026 23:06
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.

zero-stepsize-in-slice - false negative in the example from the docs

3 participants