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
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:…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:109compares the requested deserialization structure againstUnion[str, bytes]usingis:In CPython 3.14,
typing.Unionandtypes.UnionTypewere merged, and old-syntaxUnion[...]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, notis.").As a result:
The generated SDK methods pass
Union[str, bytes]asstructure, so theisbranch in_returnnever matches on 3.14 — the SDK falls through tojson.loads(value)on plain text and raisesDeserializeError.The
# type: ignorecomment 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
looker_sdk==25.20.0(and earlier 25.x) on Python 3.14looker_sdk==26.6.1(latest release as of 2026-04-28) on Python 3.14mainHEAD on Python 3.14The 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
Or without any HTTP call at all:
Suggested fix
One-line change at
python/looker_sdk/rtl/api_methods.py:109— pre-cache the union and compare with==: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._returnat import time is straightforward — substitutestrforUnion[str, bytes]before delegating to the original implementation, so the SDK's existingstructure is strbranch fires:Happy to send a PR with the one-line fix + a regression test if that helps.
References
typing.Unionandtypes.UnionType==to compare unions for equality, notis"Union[SomeNonStrType, bytes]deserialization