Skip to content

Keep Literal after indexing? #19152

Open
Open
@ego-thales

Description

@ego-thales

EDIT: MWE

from typing import Literal

foo = 0
bar: tuple[Literal[1], ...] = (1,)[foo:]  # error: Incompatible types in assignment (expression has type "tuple[int, ...]", variable has type "tuple[Literal[1], ...]")  [assignment]
baz: tuple[Literal[1], ...] = (1,)[0:]    # OK

bar_fix: tuple[Literal[1], ...] = (1,)    # In two steps...
bar_fix = bar_fix[foo:]                   # ...works fine

Original message

Hi there,

As always, I'm not sure whether I should post on mypy or python typing page. The problem I encounter is the following.

aligns: tuple[Literal["center", "right"], ...] = ("center", "right", "center")[no_extra:]
error: Incompatible types in assignment (expression has type "tuple[str, ...]", variable has type "tuple[Literal['center', 'right'], ...]")  [assignment]
            aligns: tuple[Literal["center", "right"], ...] = ("center", "right", "center")[no_extra:]

where no_extra is a bool. Essentially, I either keep or not the first element with my_tuple[no_extra:], and this makes it into a tuple[str, ...].

Is it normal? If so, is there a recommended practice around it, or should this be a new feature?

Thanks in advance!

All the best.
Élie

Activity

ego-thales

ego-thales commented on May 26, 2025

@ego-thales
Author

Just for info, I circumvent this by proceeding in two steps:

aligns: tuple[Literal["center", "right"], ...] = ("center", "right", "center")
aligns_no_extra = aligns[:no_extra]  # Still `tuple[Literal["center", "right"], ...]`

It would still be nice to have the possibility to proceed directly.

A5rocks

A5rocks commented on May 26, 2025

@A5rocks
Collaborator

I don't see why this isn't possible, unless we are using a heuristic on whether to use type context here (in which case this is just a sad consequence of that).

ego-thales

ego-thales commented on May 27, 2025

@ego-thales
Author

I don't see why this isn't possible

I don't understand what you mean. I'm not sure whether you are saying that it's a possible feature to implement or saying that this should not raise the error. Just to be sure, I provide this MWE:

from typing import Literal

foo = 0
bar: tuple[Literal[1], ...] = (1,)[foo:]  # error: Incompatible types in assignment (expression has type "tuple[int, ...]", variable has type "tuple[Literal[1], ...]")  [assignment]
baz: tuple[Literal[1], ...] = (1,)[0:]    # OK

bar_fix: tuple[Literal[1], ...] = (1,)    # In two steps...
bar_fix = bar_fix[foo:]                   # ...works fine
sterliakov

sterliakov commented on May 28, 2025

@sterliakov
Collaborator

This is trivially fixable, but I suspect this was intentionally not supported to prevent horror like this:

reveal_type((1, 2, 3, 4, 5, 'foo', 'bar')[foo:])  # N: Revealed type is "builtins.tuple[Union[Literal[1]?, Literal[2]?, Literal[3]?, Literal[4]?, Literal[5]?, Literal['foo']?, Literal['bar']?], ...]"

...which is exactly what happens if we decide to retain literal values in tuples, and is bad enough IMO to not fix this corner case.

If anyone has better ideas that retain literals only to some extent, here's what I did to receive that giant union:

diff --git a/mypy/checker.py b/mypy/checker.py
index aceb02919..cb153a865 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -7261,14 +7261,15 @@ class TypeChecker(NodeVisitor[None], TypeCheckerSharedApi):
         any_type = AnyType(TypeOfAny.from_omitted_generics)
         return Instance(node, [any_type] * len(node.defn.type_vars))
 
-    def named_generic_type(self, name: str, args: list[Type]) -> Instance:
+    def named_generic_type(self, name: str, args: list[Type], *, keep_last_known_values: bool = False) -> Instance:
         """Return an instance with the given name and type arguments.
 
         Assume that the number of arguments is correct.  Assume that
         the name refers to a compatible generic type.
         """
         info = self.lookup_typeinfo(name)
-        args = [remove_instance_last_known_values(arg) for arg in args]
+        if not keep_last_known_values:
+            args = [remove_instance_last_known_values(arg) for arg in args]
         # TODO: assert len(args) == len(info.defn.type_vars)
         return Instance(info, args)
 
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index ec64669c1..4faad23fc 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -4617,7 +4617,7 @@ class ExpressionChecker(ExpressionVisitor[Type], ExpressionCheckerSharedApi):
         # We could return the return type from above, but unions are often better than the join
         union = self.union_tuple_fallback_item(left_type)
         if isinstance(index, SliceExpr):
-            return self.chk.named_generic_type("builtins.tuple", [union])
+            return self.chk.named_generic_type("builtins.tuple", [union], keep_last_known_values=True)
         return union
 
     def union_tuple_fallback_item(self, left_type: TupleType) -> Type:
diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test
index 3424d053f..a224ad7b0 100644
--- a/test-data/unit/check-tuples.test
+++ b/test-data/unit/check-tuples.test
@@ -1438,6 +1438,15 @@ reveal_type(t[x:])  # N: Revealed type is "builtins.tuple[Union[builtins.int, bu
 t[y:]  # E: Slice index must be an integer, SupportsIndex or None
 [builtins fixtures/tuple.pyi]
 
+[case testNonLiteralSlicePreservesLiteralElements]
+from typing import Literal
+
+foo = 0
+bar: tuple[Literal[1], ...] = (1,)[foo:]
+baz: tuple[Literal[1], ...] = (1,)[0:]
+reveal_type((1,2,3,4,5,'foo','bar')[foo:])
+[builtins fixtures/tuple.pyi]
+
 [case testTupleSliceStepZeroNoCrash]
 # This was crashing: https://github.com/python/mypy/issues/18062
 # TODO: emit better error when 0 is used for step
A5rocks

A5rocks commented on May 28, 2025

@A5rocks
Collaborator

If anyone has better ideas that retain literals only to some extent, here's what I did to receive that giant union:

Maybe return the type context (self.type_context[-1]) if left_type is a subtype? (or if union is a subtype?)

ego-thales

ego-thales commented on May 28, 2025

@ego-thales
Author

Thanks for the details, it's interesting for me as mypy noob. I'm not sure this makes sense, but couldn't the behaviour be different between typed and nontyped statements?

What I mean is that your example indeed provides ugly revealed type, but maybe it should not unless the assignment explicitly types with Literals? Again, I'm not sure I'm making sense here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @JelleZijlstra@A5rocks@sterliakov@ego-thales

        Issue actions

          Keep `Literal` after indexing? · Issue #19152 · python/mypy