Skip to content

[ty] Allow subtypes of LiteralString to be narrowed using equality checks#23794

Merged
AlexWaygood merged 2 commits intomainfrom
alex/literalstring-narrow
Mar 9, 2026
Merged

[ty] Allow subtypes of LiteralString to be narrowed using equality checks#23794
AlexWaygood merged 2 commits intomainfrom
alex/literalstring-narrow

Conversation

@AlexWaygood
Copy link
Member

Summary

Our current narrowing machinery has some very specific special casing for LiteralString exactly, which produces odd results in if/elif chains:

from typing import LiteralString, reveal_type

def parse_status2(status: LiteralString) -> None:
    if status == "MALFORMED":
        reveal_type(status)  # revealed: Literal["MALFORMED"]
    elif status == "ABORTED":
        reveal_type(status)  # revealed: LiteralString & ~Literal["MALFORMED"]

The second reveal_type there should clearly be Literal["ABORTED"].

This PR extends our narrowing behaviour for LiteralString to all subtypes of LiteralString.

Test Plan

mdtests extended

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Mar 7, 2026
@astral-sh-bot
Copy link

astral-sh-bot bot commented Mar 7, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 85.05%. The percentage of expected errors that received a diagnostic held steady at 78.05%. The number of fully passing files held steady at 63/132.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Mar 7, 2026

mypy_primer results

Changes were detected when running on open source projects
pydantic (https://github.com/pydantic/pydantic)
+ pydantic/_migration.py:295:28: error[invalid-argument-type] Method `__getitem__` of type `bound method dict[LiteralString, LiteralString].__getitem__(key: LiteralString, /) -> LiteralString` cannot be called with key of type `str` on object of type `dict[LiteralString, LiteralString]`
+ pydantic/_migration.py:302:34: error[invalid-argument-type] Method `__getitem__` of type `bound method dict[LiteralString, LiteralString].__getitem__(key: LiteralString, /) -> LiteralString` cannot be called with key of type `str` on object of type `dict[LiteralString, LiteralString]`
- Found 3192 diagnostics
+ Found 3194 diagnostics

meson (https://github.com/mesonbuild/meson)
+ mesonbuild/compilers/mixins/apple.py:46:16: error[unsupported-operator] Operator `+` is not supported between objects of type `ImmutableListProtocol[str]` and `list[LiteralString]`
- Found 2362 diagnostics
+ Found 2363 diagnostics

dd-trace-py (https://github.com/DataDog/dd-trace-py)
- ddtrace/propagation/_database_monitoring.py:136:13: error[invalid-assignment] Invalid subscript assignment with key of type `Literal["ddsh"]` and value of type `str` on object of type `dict[Literal["ddps", "dde", "ddpv", "dddbs"], Unknown | str]`
+ ddtrace/propagation/_database_monitoring.py:136:13: error[invalid-assignment] Invalid subscript assignment with key of type `Literal["ddsh"]` and value of type `LiteralString` on object of type `dict[Literal["ddps", "dde", "ddpv", "dddbs"], Unknown | str]`

altair (https://github.com/vega/altair)
- altair/utils/deprecation.py:40:12: error[invalid-return-type] Return type does not match returned value: expected `LiteralString`, found `str`
- Found 1073 diagnostics
+ Found 1072 diagnostics

hydpy (https://github.com/hydpy-dev/hydpy)
+ hydpy/exe/xmltools.py:2771:21: error[invalid-argument-type] Argument to bound method `extend` is incorrect: Expected `Iterable[LiteralString]`, found `list[str]`
- Found 1066 diagnostics
+ Found 1067 diagnostics

prefect (https://github.com/PrefectHQ/prefect)
- src/integrations/prefect-docker/tests/test_containers.py:27:47: error[invalid-argument-type] Argument is incorrect: Expected `str`, found `str | bool | dict[str, int] | None`
- src/integrations/prefect-docker/tests/test_containers.py:27:47: error[invalid-argument-type] Argument is incorrect: Expected `str | list[str] | None`, found `str | bool | dict[str, int] | None`
- src/integrations/prefect-docker/tests/test_containers.py:27:47: error[invalid-argument-type] Argument is incorrect: Expected `str | None`, found `str | bool | dict[str, int] | None`
- src/integrations/prefect-docker/tests/test_containers.py:27:47: error[invalid-argument-type] Argument is incorrect: Expected `bool | None`, found `str | bool | dict[str, int] | None`
- src/integrations/prefect-docker/tests/test_containers.py:27:47: error[invalid-argument-type] Argument is incorrect: Expected `str | list[str] | None`, found `str | bool | dict[str, int] | None`
- src/integrations/prefect-docker/tests/test_containers.py:27:47: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, str] | list[str] | None`, found `str | bool | dict[str, int] | None`
- src/integrations/prefect-docker/tests/test_containers.py:27:47: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `str | bool | dict[str, int] | None`
- src/integrations/prefect-docker/tests/test_containers.py:42:47: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `str`
+ src/integrations/prefect-docker/tests/test_containers.py:26:31: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-docker/tests/test_containers.py:55:47: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `str`
- src/integrations/prefect-docker/tests/test_containers.py:68:47: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `str`
- src/integrations/prefect-docker/tests/test_containers.py:81:47: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `str`
+ src/integrations/prefect-docker/tests/test_containers.py:41:26: error[invalid-await] `object` is not awaitable
+ src/integrations/prefect-docker/tests/test_containers.py:54:31: error[invalid-await] `object` is not awaitable
+ src/integrations/prefect-docker/tests/test_containers.py:67:31: error[invalid-await] `object` is not awaitable
+ src/integrations/prefect-docker/tests/test_containers.py:80:31: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-docker/tests/test_images.py:16:44: error[invalid-argument-type] Argument is incorrect: Expected `str`, found `str | bool`
+ src/integrations/prefect-docker/tests/test_images.py:16:23: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-docker/tests/test_images.py:16:44: error[invalid-argument-type] Argument is incorrect: Expected `str | None`, found `str | bool`
- src/integrations/prefect-docker/tests/test_images.py:16:44: error[invalid-argument-type] Argument is incorrect: Expected `str | None`, found `str | bool`
- src/integrations/prefect-docker/tests/test_images.py:16:44: error[invalid-argument-type] Argument is incorrect: Expected `bool`, found `str | bool`
- src/integrations/prefect-docker/tests/test_images.py:16:44: error[invalid-argument-type] Argument is incorrect: Expected `DockerHost | None`, found `str | bool`
- src/integrations/prefect-docker/tests/test_images.py:16:44: error[invalid-argument-type] Argument is incorrect: Expected `DockerRegistryCredentials | None`, found `str | bool`
- src/integrations/prefect-docker/tests/test_images.py:16:44: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `str | bool`
- src/integrations/prefect-docker/tests/test_images.py:29:47: error[invalid-argument-type] Argument is incorrect: Expected `bool`, found `str`
+ src/integrations/prefect-docker/tests/test_images.py:28:27: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-docker/tests/test_images.py:29:47: error[invalid-argument-type] Argument is incorrect: Expected `DockerRegistryCredentials | None`, found `str`
- src/integrations/prefect-docker/tests/test_images.py:29:47: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `str`
- src/integrations/prefect-docker/tests/test_images.py:31:16: error[unresolved-attribute] Attribute `id` is not defined on `list[Unknown]` in union `Unknown | list[Unknown]`
- src/integrations/prefect-docker/tests/test_images.py:51:17: error[invalid-argument-type] Argument is incorrect: Expected `bool`, found `str`
+ src/integrations/prefect-docker/tests/test_images.py:48:27: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-docker/tests/test_images.py:51:17: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `str`
- src/integrations/prefect-docker/tests/test_images.py:53:16: error[unresolved-attribute] Attribute `id` is not defined on `list[Unknown]` in union `Unknown | list[Unknown]`
- src/integrations/prefect-docker/tests/test_images.py:64:47: error[invalid-argument-type] Argument is incorrect: Expected `str`, found `str | bool`
+ src/integrations/prefect-docker/tests/test_images.py:63:28: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-docker/tests/test_images.py:64:47: error[invalid-argument-type] Argument is incorrect: Expected `str | None`, found `str | bool`
- src/integrations/prefect-docker/tests/test_images.py:64:47: error[invalid-argument-type] Argument is incorrect: Expected `str | None`, found `str | bool`
- src/integrations/prefect-docker/tests/test_images.py:64:47: error[invalid-argument-type] Argument is incorrect: Expected `bool`, found `str | bool`
- src/integrations/prefect-docker/tests/test_images.py:64:47: error[invalid-argument-type] Argument is incorrect: Expected `DockerRegistryCredentials | None`, found `str | bool`
- src/integrations/prefect-docker/tests/test_images.py:64:47: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `str | bool`
- src/integrations/prefect-kubernetes/prefect_kubernetes/jobs.py:428:17: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `str`
+ src/integrations/prefect-kubernetes/prefect_kubernetes/jobs.py:425:33: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_custom_objects.py:20:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `None`
- src/integrations/prefect-kubernetes/tests/test_custom_objects.py:29:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `None`
- src/integrations/prefect-kubernetes/tests/test_custom_objects.py:38:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `None`
- src/integrations/prefect-kubernetes/tests/test_custom_objects.py:57:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
- src/integrations/prefect-kubernetes/tests/test_custom_objects.py:103:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_custom_objects.py:16:15: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_custom_objects.py:149:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
- src/integrations/prefect-kubernetes/tests/test_custom_objects.py:195:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
- src/integrations/prefect-kubernetes/tests/test_custom_objects.py:240:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_custom_objects.py:24:15: error[invalid-await] `object` is not awaitable
+ src/integrations/prefect-kubernetes/tests/test_custom_objects.py:33:15: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_custom_objects.py:286:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_custom_objects.py:46:11: error[invalid-await] `object` is not awaitable
+ src/integrations/prefect-kubernetes/tests/test_custom_objects.py:98:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_custom_objects.py:344:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_custom_objects.py:144:11: error[invalid-await] `object` is not awaitable
+ src/integrations/prefect-kubernetes/tests/test_custom_objects.py:190:11: error[invalid-await] `object` is not awaitable
+ src/integrations/prefect-kubernetes/tests/test_custom_objects.py:236:11: error[invalid-await] `object` is not awaitable
+ src/integrations/prefect-kubernetes/tests/test_custom_objects.py:274:11: error[invalid-await] `object` is not awaitable
+ src/integrations/prefect-kubernetes/tests/test_custom_objects.py:332:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_deployments.py:18:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_deployments.py:16:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_deployments.py:38:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_deployments.py:34:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_deployments.py:70:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_deployments.py:68:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_deployments.py:92:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_deployments.py:88:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_deployments.py:113:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_deployments.py:110:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_deployments.py:141:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_deployments.py:137:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_jobs.py:36:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_jobs.py:34:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_jobs.py:52:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_jobs.py:50:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_jobs.py:68:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_jobs.py:66:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_jobs.py:87:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_jobs.py:84:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_jobs.py:107:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_jobs.py:104:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_jobs.py:131:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_jobs.py:127:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_jobs.py:159:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_jobs.py:156:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_pods.py:29:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_pods.py:27:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_pods.py:46:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_pods.py:42:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_pods.py:78:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_pods.py:76:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_pods.py:96:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_pods.py:92:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_pods.py:115:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_pods.py:112:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_pods.py:137:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_pods.py:133:11: error[invalid-await] `object` is not awaitable
- src/integrations/prefect-kubernetes/tests/test_pods.py:167:9: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Any]`, found `Literal["test"]`
+ src/integrations/prefect-kubernetes/tests/test_pods.py:163:11: error[invalid-await] `object` is not awaitable
- src/prefect/cache_policies.py:311:25: error[unresolved-attribute] Attribute `__code__` is not defined on `(...) -> Any` in union `Unknown | ((...) -> Any)`
+ src/prefect/cache_policies.py:311:25: error[unresolved-attribute] Attribute `__code__` is not defined on `((...) -> Any) & ((*args: object, **kwargs: object) -> object)` in union `Unknown | (((...) -> Any) & ((*args: object, **kwargs: object) -> object))`
- src/prefect/task_engine.py:1642:28: error[invalid-await] `Unknown | R@AsyncTaskRunEngine | Coroutine[Any, Any, R@AsyncTaskRunEngine]` is not awaitable
- src/prefect/tasks.py:185:9: error[unresolved-attribute] Attribute `__code__` is not defined on `(...) -> Any` in union `Unknown | ((...) -> Any)`
+ src/prefect/tasks.py:185:9: error[unresolved-attribute] Attribute `__code__` is not defined on `((...) -> Any) & ((*args: object, **kwargs: object) -> object)` in union `Unknown | (((...) -> Any) & ((*args: object, **kwargs: object) -> object))`
- src/prefect/tasks.py:795:13: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `(() -> str) | TaskRunNameCallbackWithParameters | str | None`, found `(() -> str) | TaskRunNameCallbackWithParameters | str | ... omitted 3 union elements`
+ src/prefect/tasks.py:795:13: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `(() -> str) | TaskRunNameCallbackWithParameters | str | None`, found `((() -> str) & ~<class 'NotSet'>) | TaskRunNameCallbackWithParameters | str | ... omitted 4 union elements`
- src/prefect/utilities/_engine.py:64:13: error[invalid-argument-type] Argument to bound method `is_callback_with_parameters` is incorrect: Expected `(...) -> str`, found `(Unknown & Top[(...) -> object]) | (str & Top[(...) -> object]) | (() -> str) | TaskRunNameCallbackWithParameters`
+ src/prefect/utilities/_engine.py:64:13: error[invalid-argument-type] Argument to bound method `is_callback_with_parameters` is incorrect: Expected `(...) -> str`, found `(Unknown & Top[(...) -> object]) | (str & Top[(...) -> object]) | ((() -> str) & ((*args: object, **kwargs: object) -> object)) | (TaskRunNameCallbackWithParameters & ((*args: object, **kwargs: object) -> object))`
- src/prefect/utilities/_engine.py:66:48: error[unknown-argument] Argument `parameters` does not match any known parameter
- src/prefect/utilities/_engine.py:69:29: error[missing-argument] No argument provided for required parameter `parameters` of bound method `__call__`
+ src/prefect/utilities/_engine.py:85:12: error[invalid-return-type] Return type does not match returned value: expected `str`, found `object`
- Found 5880 diagnostics
+ Found 5856 diagnostics

@astral-sh-bot
Copy link

astral-sh-bot bot commented Mar 7, 2026

Memory usage report

Memory usage unchanged ✅

@AlexWaygood AlexWaygood force-pushed the alex/literalstring-narrow branch from 10b5429 to 30627b5 Compare March 7, 2026 19:39
@AlexWaygood
Copy link
Member Author

Clearly this is a feature our users are desperate for

@AlexWaygood AlexWaygood marked this pull request as ready for review March 7, 2026 19:39
// literal, in which case (given that it is single-valued), LiteralString
// cannot compare equal to it.
Type::LiteralValue(literal) if literal.is_literal_string() => true,
ty if ty.is_subtype_of(db, Type::literal_string()) => true,
Copy link
Member

Choose a reason for hiding this comment

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

I'm guessing there are other callers of Type::is_literal_string since clippy didn't flag a dead code warning. Are those callers legit? Should they be doing subtype checks as well? (This might also deserve a comment over at the method definition calling out that if you're worried about type relations, you probably want to do this kind of subtype check instead)

Copy link
Member Author

Choose a reason for hiding this comment

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

ooh, v good question

Copy link
Member Author

Choose a reason for hiding this comment

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

You were correct, Type::is_literal_string() was a bug magnet! We now also support this feature (just as crucial as the original one -- our users will be overjoyed):

from typing import LiteralString

def f(literal_a: LiteralString, literal_b: LiteralString):
    if literal_a != "foo":
        reveal_type(literal_a)  # revealed: LiteralString & ~Literal["foo"]

        # on `main`: `str`
        # this branch: `LiteralString`!
        reveal_type(f"{literal_a}")

@AlexWaygood AlexWaygood enabled auto-merge (squash) March 9, 2026 18:09
@AlexWaygood AlexWaygood merged commit 3b62003 into main Mar 9, 2026
50 checks passed
@AlexWaygood AlexWaygood deleted the alex/literalstring-narrow branch March 9, 2026 18:11
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.

2 participants