Skip to content

Python 3.14: APIMethods._return raises DeserializeError for plain text responses (Union singleton identity check) #1705

Description

@jamie-marshmallow

Summary

On Python 3.14, every Looker SDK call whose declared return type is Union[str, bytes] (e.g. run_inline_query(result_format="sql"|"csv"|...), query_for_slug(...) paths that return raw strings, run_query, run_look, run_url_encoded_query, etc.) raises:

looker_sdk.rtl.serialize.DeserializeError: Bad json Expecting value: line 1 column 1 (char 0)

…even though the response body is a perfectly well-formed plain string (e.g. raw SQL, CSV, TSV).

Root cause

python/looker_sdk/rtl/api_methods.py:109 compares the requested deserialization structure against Union[str, bytes] using is:

if structure is Union[str, bytes] or structure is str or value == "":  # type: ignore
    ret = value
else:
    ret = self.deserialize(data=value, structure=structure)  # type: ignore

In CPython 3.14, typing.Union and types.UnionType were merged, and old-syntax Union[...] objects are no longer cached as singletons (see CPython gh-105499 and Python 3.14 What's New, which explicitly states "Use == to compare unions for equality, not is.").

As a result:

>>> from typing import Union
>>> Union[str, bytes] is Union[str, bytes]   # Python 3.14
False
>>> Union[str, bytes] == Union[str, bytes]
True

The generated SDK methods pass Union[str, bytes] as structure, so the is branch in _return never matches on 3.14 — the SDK falls through to json.loads(value) on plain text and raises DeserializeError.

The # type: ignore comment on line 109 suggests the original author already knew the identity check on a generic alias was fragile; CPython 3.14 just made it actually break.

Affected versions

Bug present?
looker_sdk==25.20.0 (and earlier 25.x) on Python 3.14 Yes
looker_sdk==26.6.1 (latest release as of 2026-04-28) on Python 3.14 Yes
main HEAD on Python 3.14 Yes
Any version on Python ≤ 3.13 No (Union singletons still cached)

The Python SDK declares python_requires=">=3.6" with no upper bound, so users on 3.14 install fine and only hit the bug at runtime.

Minimal repro

import looker_sdk
sdk = looker_sdk.init40()
# Any call that returns Union[str, bytes] reproduces:
sql = sdk.run_inline_query(
    result_format="sql",
    body=looker_sdk.models40.WriteQuery(
        model="thelook", view="users", fields=["users.id"], limit="10",
    ),
)
# Python <= 3.13: returns the SQL string.
# Python 3.14:    raises DeserializeError("Bad json Expecting value: line 1 column 1 (char 0)").

Or without any HTTP call at all:

from typing import Union
assert Union[str, bytes] is Union[str, bytes]  # AssertionError on 3.14

Suggested fix

One-line change at python/looker_sdk/rtl/api_methods.py:109 — pre-cache the union and compare with ==:

# At module scope:
_UNION_STR_BYTES = Union[str, bytes]

# In APIMethods._return:
if structure == _UNION_STR_BYTES or structure is str or value == "":
    ret = value
else:
    ret = self.deserialize(data=value, structure=structure)

This is backwards compatible to all currently-supported Python versions (3.6+).

Workaround

For projects that need to ship on 3.14 today, monkey-patching APIMethods._return at import time is straightforward — substitute str for Union[str, bytes] before delegating to the original implementation, so the SDK's existing structure is str branch fires:

from looker_sdk.rtl import api_methods

_UNION_STR_BYTES = str | bytes  # equals Union[str, bytes] at runtime
_original_return = api_methods.APIMethods._return

def _return_patched(self, response, structure):
    if structure == _UNION_STR_BYTES:
        structure = str
    return _original_return(self, response, structure)

api_methods.APIMethods._return = _return_patched

Happy to send a PR with the one-line fix + a regression test if that helps.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions