Skip to content

Warn instead of aborting when a class namespace holds a non-string key - #8581

Open
luantaraschi wants to merge 2 commits into
RustPython:mainfrom
luantaraschi:type-nonstring-key-panic
Open

Warn instead of aborting when a class namespace holds a non-string key#8581
luantaraschi wants to merge 2 commits into
RustPython:mainfrom
luantaraschi:type-nonstring-key-panic

Conversation

@luantaraschi

@luantaraschi luantaraschi commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

A class namespace holding a key that is not a string takes the interpreter down:

>>> type("MyClass", (), {1: 2})
thread 'main' panicked at crates/vm/src/builtins/dict.rs:781:65:
dict has non-string keys: [PyObject PyInt { value: 1 }]

CPython builds the class and warns once:

>>> type("MyClass", (), {1: 2})
<stdin>:1: RuntimeWarning: non-string key in the __dict__ of class MyClass
<class '__main__.MyClass'>

A metaclass that writes into the namespace it is given reaches the same line, so class Z(metaclass=M) aborts too when M.__new__ does ns[42] = "x".

A str subclass hit that panic as well, and it names an attribute as well as a str does:

>>> class S(str): pass
...
>>> type("Sub", (), {S("z"): 9}).z
9

_thread.start_new_thread shares the line through its kwargs dict, so it aborted where an ordinary call with the same mapping already raises:

>>> (lambda **k: k)(**{1: 2})
TypeError: keywords must be strings
>>> import _thread
>>> _thread.start_new_thread(lambda: None, (), {1: 2})
thread 'main' panicked at crates/vm/src/builtins/dict.rs:781:65:

The change

Py<PyDict>::to_attributes called expect() on downcast_exact. PyAttributes is an IndexMap keyed by &'static PyStrInterned, so the two kinds of key now split there:

  • a str subclass is interned like any other name, through downcast_ref::<PyStr>, which is the test collect_ex_args already applies to keyword names;
  • a key that is no kind of string is skipped, and the caller passes a closure that decides what the skip means. type() warns once per class with CPython's wording, _thread raises TypeError: keywords must be strings.

What this does not fix

The key still does not reach the class __dict__, and interning gives up the str subclass:

CPython here
1 in type("K", (), {1: 2}).__dict__ True False
type of the key in {S("z"): 9} S str

Either row would mean PyAttributes giving up its interned-string key, which reaches a lot further than the crash does. test_descr.MiscTests.test_type_lookup_mro_reference rests on the first row, because its lookup only reaches MyKey.__eq__ if the key sits in the class dict, so it stays skipped and its reason now names that instead of the crash.

Tests

Lib/test/test_descr.py::test_gh55664 was skipped on this exact panic, quoting it in the reason, and is now unskipped. Without the change the module takes the runner down with it:

0:00:00 load avg: 2.70 [1/1] test_descr

thread 'main' (4629) panicked at crates/vm/src/builtins/dict.rs:781:65:
dict has non-string keys: [PyObject PyInt { value: 1 }]

extra_tests/snippets/builtin_type.py covers the str subclass key, the single warning a class gets however many bad keys it holds, and that warning turning into an exception under simplefilter("error"). It passes under CPython 3.14 too.

Also run: cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi, cargo test in crates/capi, the extra_tests snippets, cargo fmt --check, the clippy invocation from CI, and -m test over test_descr, test_class, test_type_aliases, test_types, test_thread, test_threading, test_metaclass, test_dict, test_dictviews, test_super, test_abc, test_enum, test_dataclasses, test_functools and test_warnings. The one snippet failure is stdlib_sqlite, which wants a feature that is not in the default set.

Summary by CodeRabbit

  • Bug Fixes

    • Class creation now supports attribute keys that are str subclasses.
    • Non-string class dictionary keys remain available in the class dictionary instead of causing a failure.
    • Non-string keyword arguments passed to thread creation now raise a clear TypeError.
  • Warnings

    • Class creation emits a RuntimeWarning when non-string attribute keys are encountered.
    • Applications configured to treat warnings as errors can handle this condition consistently.

type() took the whole process down when the namespace it was handed had
a key that was not an exact str:

    >>> type("MyClass", (), {1: 2})
    thread 'main' panicked at crates/vm/src/builtins/dict.rs:781:65:
    dict has non-string keys: [PyObject PyInt { value: 1 }]

to_attributes called expect() on downcast_exact, so a str subclass hit
the same line, and that one names an attribute as well as a str does.
PyAttributes is keyed by interned strings and can hold nothing else, so
a str subclass is now interned like any other name, and a key that is no
kind of string is skipped, with the caller deciding what the skip means.
type() warns once per class, the way CPython does, and
_thread.start_new_thread raises the TypeError an ordinary call already
raises for the same mapping.

The key itself still does not land in the class dict, which is what
test_type_lookup_mro_reference needs, so that one stays skipped with the
reason rewritten.

Assisted-by: Claude Code:claude-opus-5
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • Lib/test/test_descr.py is excluded by !Lib/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8571c55a-d17f-4932-adf5-d112a71c434d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: d8b2e5d9-3c58-4b80-8849-d53717acd857

📥 Commits

Reviewing files that changed from the base of the PR and between cc1e55e and ae94047.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_descr.py is excluded by !Lib/**
📒 Files selected for processing (4)
  • crates/vm/src/builtins/dict.rs
  • crates/vm/src/builtins/type.rs
  • crates/vm/src/stdlib/_thread.rs
  • extra_tests/snippets/builtin_type.py

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

PyDict::to_attributes now supports string subclasses, skips non-string keys, and propagates callback errors. Class creation warns for invalid keys. Thread keyword conversion raises TypeError for invalid names. Tests cover these behaviors.

Changes

Dictionary attribute conversion

Layer / File(s) Summary
Attribute conversion contract
crates/vm/src/builtins/dict.rs
to_attributes now returns PyResult<PyAttributes>, preserves str subclasses, skips non-string keys, and invokes a one-shot callback for the first skipped key.
Conversion callers and validation
crates/vm/src/builtins/type.rs, crates/vm/src/stdlib/_thread.rs, extra_tests/snippets/builtin_type.py
Class creation emits RuntimeWarning for non-string keys. Thread keyword conversion raises TypeError. Tests cover string-subclass keys and warning behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to ae940

The change replaces interpreter panics with warning or TypeError behavior for invalid namespace keys while preserving existing supported behavior; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: youknowone

Sequence Diagram(s)

sequenceDiagram
  participant ClassConstruction as type.__new__
  participant AttributeConversion as PyDict::to_attributes
  participant WarningSystem as RuntimeWarning
  participant ClassDictionary as class dictionary
  ClassConstruction->>ClassDictionary: read namespace
  ClassConstruction->>AttributeConversion: convert namespace
  AttributeConversion->>WarningSystem: warn for non-string key
  AttributeConversion-->>ClassConstruction: return PyResult<PyAttributes>
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: warning instead of aborting for non-string keys in class namespaces.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[ ] test: cpython/Lib/test/test_descr.py (TODO: 31)
[ ] test: cpython/Lib/test/test_descrtut.py (TODO: 3)

dependencies:

dependent tests: (no tests depend on descr)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

Comment thread Lib/test/test_descr.py Outdated

class MiscTests(unittest.TestCase):
@unittest.skip("TODO: RUSTPYTHON; rustpython panicked at 'dict has non-string keys: [PyObject PyBaseObject]'")
@unittest.skip("TODO: RUSTPYTHON; a class namespace drops keys that are not strings, so MyKey.__eq__ is never reached and __bases__ stays put")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this no longer crash can we switch this to be expectedFailure instead of skip?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched it. Five runs in a row report it as an expected failure, and it cannot turn into a pass while the class dict drops the key its lookup needs to reach MyKey.__eq__.

It aborted the runner before, which is why it was skipped. Now that it
only fails, the run can carry it: five runs in a row report it as an
expected failure, and it cannot turn into a pass while the class dict
drops the key its lookup needs.

Assisted-by: Claude Code:claude-opus-5

@ShaharNaveh ShaharNaveh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ty:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants