Skip to content

Keep Literal after indexing? #19152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ego-thales opened this issue May 26, 2025 · 6 comments
Open

Keep Literal after indexing? #19152

ego-thales opened this issue May 26, 2025 · 6 comments
Labels
feature topic-literal-types topic-type-context Type context / bidirectional inference

Comments

@ego-thales
Copy link

ego-thales commented May 26, 2025

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

@ego-thales
Copy link
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 added the topic-type-context Type context / bidirectional inference label May 26, 2025
@A5rocks
Copy link
Collaborator

A5rocks commented May 26, 2025

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
Copy link
Author

ego-thales commented May 27, 2025

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
Copy link
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
Copy link
Collaborator

A5rocks commented May 28, 2025

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
Copy link
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
Labels
feature topic-literal-types topic-type-context Type context / bidirectional inference
Projects
None yet
Development

No branches or pull requests

4 participants