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

Open Beginner friendly
#1,705 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
84/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
python
Domain
api

Research direction

Start in python/looker_sdk/rtl/api_methods.py at APIMethods._return and reproduce the issue with the minimal run_inline_query example on Python 3.14. Add a regression test for a Union[str, bytes] plain-text response, then verify that it returns the raw value without DeserializeError on supported Python versions.

Written by the indexing model from the issue text.

Description

need triage p3

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

Dominant language
TypeScript
Stars
272
Forks
203
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from looker-open-source/sdk-codegen

All issues in looker-open-source/sdk-codegen

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.