Skip to content

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

@jamie-marshmallow

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions