-
Notifications
You must be signed in to change notification settings - Fork 144
Description
What are you really trying to do?
I am running a workflow that passes data containing a dictionary. The dictionary's type hint uses typing.Literal for its keys. The workflow fails during data conversion when attempting to convert this dictionary.
Describe the bug
When temporalio.converter.value_to_type attempts to convert a dictionary that has a typing.Literal as its key_type, it incorrectly uses isinstance(key, key_type) for validation.
In Python 3.13, using isinstance() with subscripted generics like typing.Literal is no longer allowed and raises a TypeError: Subscripted generics cannot be used with class and instance checks.
The value_to_type function catches this TypeError and re-raises its own TypeError (e.g., "Failed converting key 'AA' to type..."), which hides the root cause.
This bug appears to be in temporalio/converter.py around line 1601 (in v1.18.1) inside the "mapping" logic of converter.py::value_to_type:
is_newtype = getattr(key_type, "__supertype__", None)
if is_newtype or not isinstance(key, key_type): # <--- THIS IS THE BUG
key = value_to_type(key_type, key, custom_converters)Stacktrace:
Failing workflow task run_id=019a2143-d61f-76f5-95c8-c4b9e79ecd51 failure=Failure { failure: Some(Failure { message: "Failed decoding arguments", source: "", stack_trace: " File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/worker/_workflow_instance.py\", line 413, in activate\n self._apply(job)\n ~~~~~~~~~~~^^^^^\n\n File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/worker/_workflow_instance.py\", line 512, in _apply\n self._apply_resolve_activity(job.resolve_activity)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^\n\n File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/worker/_workflow_instance.py\", line 761, in _apply_resolve_activity\n ret_vals = self._convert_payloads(\n [job.result.completed.result],\n ret_types,\n )\n\n File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/worker/_workflow_instance.py\", line 2000, in _convert_payloads\n raise RuntimeError(\"Failed decoding arguments\") from err\n", encoded_attributes: None, cause: Some(Failure { message: "Failed converting field stats on dataclass <class 'core.app.domain.models.experiments.RunSummary'>", source: "", stack_trace: " File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/worker/_workflow_instance.py\", line 1990, in _convert_payloads\n return self._payload_converter.from_payloads(\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^\n payloads,\n ^^^^^^^^^\n type_hints=types,\n ^^^^^^^^^^^^^^^^^\n )\n ^\n\n File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/converter.py\", line 311, in from_payloads\n values.append(converter.from_payload(payload, type_hint))\n ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/converter.py\", line 594, in from_payload\n obj = value_to_type(type_hint, obj, self._custom_type_converters)\n\n File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/converter.py\", line 1645, in value_to_type\n raise TypeError(\n f\"Failed converting field {field.name} on dataclass {hint}\"\n ) from err\n", encoded_attributes: None, cause: Some(Failure { message: "Failed converting key 'AA' to type typing.Literal['Clone', 'Chain', 'Plate', 'Well', 'DNA', 'AA', 'Clonaltype Count'] in mapping dict[typing.Literal['Clone', 'Chain', 'Plate', 'Well', 'DNA', 'AA', 'Clonaltype Count'], dict[typing.Literal['Mean', 'Median', 'Range', 'Count of Missing Values', 'Count of Unique Values', 'Data Type', 'Total Records'], str | int | float | None]]", source: "", stack_trace: " File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/converter.py\", line 1641, in value_to_type\n field_values[field.name] = value_to_type(\n ~~~~~~~~~~~~~^\n field_hints[field.name], field_value, custom_converters\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/converter.py\", line 1604, in value_to_type\n raise TypeError(\n f\"Failed converting key {repr(key)} to type {key_type} in mapping {hint}\"\n ) from err\n", encoded_attributes: None, cause: Some(Failure { message: "**Subscripted generics cannot be used with class and instance checks**", source: "", stack_trace: " File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/converter.py\", line 1601, in value_to_type\n if is_newtype or not isinstance(key, key_type):\n ~~~~~~~~~~^^^^^^^^^^^^^^^\n\n File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/worker/workflow_sandbox/_importer.py\", line 497, in __call__\n return self.current(*args, **kwargs)\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^\n\n File \"<my_repo>/.venv/lib/python3.13/site-packages/temporalio/worker/workflow_sandbox/_importer.py\", line 120, in unwrap_second_param\n return orig(a, b)\n\n File \"/opt/homebrew/Cellar/python@3.13/3.13.2/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py\", line 1375, in __instancecheck__\n return self.__subclasscheck__(type(obj))\n ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n File \"/opt/homebrew/Cellar/python@3.13/3.13.2/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py\", line 1378, in __subclasscheck__\n raise TypeError(\"**Subscripted generics cannot be used with\"\n \" class and instance checks**\")\n", encoded_attributes: None, cause: None, failure_info: Some(ApplicationFailureInfo(ApplicationFailureInfo { r#type: "TypeError", non_retryable: false, details: None, next_retry_delay: None, category: Unspecified })) }), failure_info: Some(ApplicationFailureInfo(ApplicationFailureInfo { r#type: "TypeError", non_retryable: false, details: None, next_retry_delay: None, category: Unspecified })) }), failure_info: Some(ApplicationFailureInfo(ApplicationFailureInfo { r#type: "TypeError", non_retryable: false, details: None, next_retry_delay: None, category: Unspecified })) }), failure_info: Some(ApplicationFailureInfo(ApplicationFailureInfo { r#type: "RuntimeError", non_retryable: false, details: None, next_retry_delay: None, category: Unspecified })) }), force_cause: Unspecified }
Minimal Reproduction
The following pytest unit test reliably reproduces the bug by isolating the value_to_type function. This test expects the "Failed converting key" TypeError, confirming the bug's presence.
import pytest
import typing
from temporalio.converter import value_to_type
def test_value_to_type_literal_key_bug():
"""
Reproduces the bug where value_to_type fails on a dict
with a typing.Literal key on Python 3.13+.
"""
# 1. Define the types
KeyHint = typing.Literal["Key1", "Key2"]
InnerKeyHint = typing.Literal["Inner1", "Inner2"]
InnerValueHint = str | int | float | None
ValueHint = dict[InnerKeyHint, InnerValueHint]
# 2. Define the hint and value that cause the crash
hint_with_bug = dict[KeyHint, ValueHint]
value_to_convert = {"Key1": {"Inner1": 123.45, "Inner2": 10}}
custom_converters = []
# 3. Assert that the function raises the specific TypeError
# This test PASSES if the bug is present.
# It will FAIL if the bug is fixed (as the error won't be raised).
with pytest.raises(TypeError, match="Failed converting key 'Key1' to type"):
value_to_type(hint_with_bug, value_to_convert, custom_converters)Environment/Versions
- OS and processor: macOS (Apple Silicon, based on /opt/homebrew/)
- Python Version: 3.13.2
- Temporal Version: 1.18.1
- Using Docker, K8s, and building from source
Additional context
The fix is to add a check to see if key_type is a "real" class (like int or str) before calling isinstance(), as isinstance(typing.Literal[...], type) returns False.
Proposed Fix:
Change this line:
if is_newtype or not isinstance(key, key_type):
To this:
if (
is_newtype
or not isinstance(key_type, type) # Check if key_type is a class
or not isinstance(key, key_type)
):This short-circuits the logic and avoids calling isinstance(key, key_type) when key_type is a generic, correctly falling through to the value_to_type call which handles Literal properly.