Summary
Unpacking a dict whose keys contain lone surrogates via ** raises TypeError: keywords must be strings, but the keys are str instances. CPython accepts any str key through the ** call path.
def collect(**kw):
return kw
collect(**{'\udc81': 2}) # TypeError: keywords must be strings
dict(**{'\udc81': 1}) # TypeError: keywords must be strings
CPython 3.14.2:
>>> collect(**{'\udc81': 2})
{'\udc81': 2}
'\udc81' is a real str (e.g. produced by os.fsdecode of undecodable bytes, surrogateescape-decoded data, etc.), so this rejects valid programs. Verified on main @ 7044fdc.
Cause
ExecutingFrame::collect_ex_args (crates/vm/src/frame.rs:6539) validates kwargs keys with
key.downcast_ref::<PyUtf8Str>()
.ok_or_else(|| vm.new_type_error("keywords must be strings"))?;
A str containing a lone surrogate is a PyStr whose payload is Wtf8, not PyUtf8Str, so the downcast fails even though the key is a str. The same pattern exists at frame.rs:6521, crates/vm/src/stdlib/_functools.rs (partial), and crates/capi/src/abstract_.rs.
The deeper constraint is that KwArgs/FuncArgs keyword names are Rust String (UTF-8), so a surrogate-carrying key currently has no representation in the calling convention; fixing this properly means keyword names need to be Wtf8Buf (or PyStrRef) end-to-end. The error message check should be downcast_ref::<PyStr>() (matching CPython, which only rejects non-str keys) once the storage can hold it.
Found while running synthetic benchmark/compat suites; researched and reported by Claude on behalf of @youknowone.
Summary
Unpacking a dict whose keys contain lone surrogates via
**raisesTypeError: keywords must be strings, but the keys arestrinstances. CPython accepts anystrkey through the**call path.CPython 3.14.2:
'\udc81'is a realstr(e.g. produced byos.fsdecodeof undecodable bytes,surrogateescape-decoded data, etc.), so this rejects valid programs. Verified onmain@ 7044fdc.Cause
ExecutingFrame::collect_ex_args(crates/vm/src/frame.rs:6539) validates kwargs keys withA str containing a lone surrogate is a
PyStrwhose payload is Wtf8, notPyUtf8Str, so the downcast fails even though the key is astr. The same pattern exists atframe.rs:6521,crates/vm/src/stdlib/_functools.rs(partial), andcrates/capi/src/abstract_.rs.The deeper constraint is that
KwArgs/FuncArgskeyword names are RustString(UTF-8), so a surrogate-carrying key currently has no representation in the calling convention; fixing this properly means keyword names need to beWtf8Buf(orPyStrRef) end-to-end. The error message check should bedowncast_ref::<PyStr>()(matching CPython, which only rejects non-strkeys) once the storage can hold it.Found while running synthetic benchmark/compat suites; researched and reported by Claude on behalf of @youknowone.