Skip to content

[Bug] converter.py: TypeError on Python 3.13 when using isinstance with typing.Literal #1188

@BrunodsLilly

Description

@BrunodsLilly

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions