`TypeError: keywords must be strings` on `**a, **{...}` when a key is a `str` subclass
Bug Description
Under Nuitka module compilation, a call that uses two **-unpacks in the same
call — where the second unpack is a dict literal whose key is a str SUBCLASS (not a
plain str) — fails at runtime with TypeError: keywords must be strings. The same
code runs fine under ordinary CPython, and runs fine under Nuitka when the key is a
plain str.
In our case the key is SQLAlchemy's quoted_name (sqlalchemy.sql.quoted_name), a
str subclass used for auto-quoted column identifiers. insert(table).values(**values, **{pk_col.key: ..., ...})
throws TypeError: keywords must be strings. Isolating the unpack into a plain dict
builtin (dict(**a, **{...})) or a plain **kwargs function reproduces it too — it is
NOT specific to SQLAlchemy. A single **merged unpack with the same key works fine.
️ Environment
1. Nuitka Version, Python Version, OS, and Platform
4.2.1
Update status: up to date with stable release '4.2.1' (cached, 7 minutes old).
Commercial: None
Python: 3.12.13 (main, Aug 5 2026, 01:11:46) [GCC 12.2.0]
Flavor: Unknown
Executable: /usr/local/bin/python
OS: Linux
Arch: x86_64
Distribution: Debian 12
Version C compiler: gcc (gcc 12).(Output of python -m nuitka --version run inside the reprex container. The bug
reproduces on both 4.2 — the version we were using when we noticed — and the latest stable 4.2.1.)
2. How Nuitka and Python were Installed
- Nuitka installed via pip inside a Docker build stage (image
python:3.12-slim-bookworm):pip install --no-cache-dir "nuitka==4.2.1". - Build toolchain:
build-essential,python3-dev,ccache(apt). - Compiled with
--module --include-package=<pkg> --python-flag=no_docstrings.
3. Relevant PyPI Packages and Versions
sqlalchemy==2.0.52(provides thequoted_namestr-subclass key we used; the bug is reproducible without SQLAlchemy using anystrsubclass as the unpack key).greenlet==3.5.5,typing_extensions==4.16.0(SQLAlchemy deps).
️ To Reproduce
2. Short, Self-Contained, Correct, Eligible (SSCCE) Example
# reprex_pkg/bug.py
from sqlalchemy.sql import quoted_name
class _StrSub(str):
"""A minimal str SUBCLASS (NOT a plain str)."""
class Bug:
async def _create_entity(self, values, pk_col_key, entity_id, now):
# THE BUGGY LINE: two **-unpacks in one call; the second literal's key is a
# str subclass -- not a plain str.
return dict(**values, **{pk_col_key: entity_id, "created_dtm": now, "updated_dtm": now})
async def _create_entity_fixed(self, values, pk_col_key, entity_id, now):
# WORKAROUND: merge once, single **-unpack.
merged = dict(values)
merged[pk_col_key] = entity_id
merged["created_dtm"] = now
merged["updated_dtm"] = now
return merged# run.py
import asyncio
from reprex_pkg.bug import Bug, _StrSub
from sqlalchemy.sql import quoted_name
PK_QN = quoted_name("user_id", True) # SQLAlchemy's quoted_name — a str subclass
PK_PLAIN = _StrSub("user_id") # a plain str subclass, no SQLAlchemy
async def main():
values = {"external_id": "x", "display_name": "y", "active": True}
bug = Bug()
for name, coro in (
("buggy double-** (quoted_name key)", bug._create_entity(values, PK_QN, "abc", "NOW")),
("buggy double-** (plain str-sub )", bug._create_entity(values, PK_PLAIN, "abc", "NOW")),
("fixed single-** merged ", bug._create_entity_fixed(values, PK_QN, "abc", "NOW")),
):
try:
await coro
print(f"OK {name}")
except Exception as exc:
print(f"ERR {name}: {type(exc).__name__}: {exc}")
if __name__ == "__main__":
asyncio.run(main())The actual project triggers this through SQLAlchemy:
pk_col.keyon a mappedTablecolumn returns aquoted_name, andinsert(table).values(**values, **{pk_col.key: entity_id, ...})throws the sameTypeError. The SSCCE above uses a baredict(**a, **{...})to prove the bug is in the**-unpack codegen itself, not an SQLAlchemy interaction.
3. Nuitka Command Line Options
Compile the package to a module (one .so), which is how we ship our packages:
python -m nuitka \
--module \
--include-package=reprex_pkg \
--python-flag=no_docstrings \
--output-dir=/site \
reprex_pkgThen run run.py from a directory that does not contain the original .py source
(so the compiled .so is not shadowed by source).
Expected Behavior
All variants should print OK. The double-**-unpack with a str-subclass key
should behave identically to the single-unpack form (both forms work in CPython).
Actual Behavior & Output
Under the Nuitka-compiled .so (reproduced on both 4.2 and 4.2.1):
ERR buggy double-** (quoted_name key): TypeError: keywords must be strings
ERR buggy double-** (plain str-sub ): TypeError: keywords must be strings
OK fixed single-** merged
RESULT: reproduced (a variant raised)Under ordinary CPython (same source, interpreted), all print OK:
OK buggy double-** (quoted_name key): INSERT INTO auth_users ...
OK buggy double-** (plain str-sub ): INSERT INTO auth_users ...
OK fixed single-** merged: INSERT INTO auth_users ...
RESULT: no reproductionEarlier tests from the our project build (pre-workaround), where
pk_col.key is a quoted_name:
DIAG merged keys: ['str', 'str', 'str', 'quoted_name', 'str', 'str']
DIAG plain dict(**a, **{...}) FAILED: keywords must be strings
DIAG plain func(**a, **{...}) FAILED: keywords must be strings
DIAG control merged .values(**merged) OKNote the 4th key is quoted_name (a str subclass). dict(**a, **{...}) and a plain
**kwargs function both fail; a single **merged succeeds. With a plain str key the
double-**-unpack does NOT fail.
↩️ Regression (if applicable)
Not bisected (found the first time we used Nuitka). Confirmed present on both Nuitka 4.2 and the latest stable 4.2.1.
Additional context
AI (Zoo code, DS4Flash-0731) did most of the heavy lifting in producing the reprex above so it's written the report; I have reviewed and modified it.
Source: Nuitka/Nuitka