#4034·Nuitka

`TypeError: keywords must be strings` on `**a, **{...}` when a key is a `str` subclass

Author: thiggerCreated Sep 9, 2026Updated Sep 11, 2026
Labelsbugexcellent_report

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 the quoted_name str-subclass key we used; the bug is reproducible without SQLAlchemy using any str subclass 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

python
# 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
python
# 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.key on a mapped Table column returns a quoted_name, and insert(table).values(**values, **{pk_col.key: entity_id, ...}) throws the same TypeError. The SSCCE above uses a bare dict(**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:

bash
python -m nuitka \
    --module \
    --include-package=reprex_pkg \
    --python-flag=no_docstrings \
    --output-dir=/site \
    reprex_pkg

Then 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 reproduction

Earlier 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) OK

Note 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.