Preserve str subclass type returned by __str__ / __repr__ by changjoon-park · Pull Request #7701 · RustPython/RustPython · GitHub
Skip to content

Preserve str subclass type returned by __str__ / __repr__ - #7701

Merged
youknowone merged 1 commit into
RustPython:mainfrom
changjoon-park:fix-str-subclass-preservation
Apr 28, 2026
Merged

Preserve str subclass type returned by __str__ / __repr__#7701
youknowone merged 1 commit into
RustPython:mainfrom
changjoon-park:fix-str-subclass-preservation

Conversation

@changjoon-park

@changjoon-park changjoon-park commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Closes #7450.

Background

CPython's unicode_new_impl (Objects/unicodeobject.c#L15575-L15596) only invokes unicode_subtype_new when the requested type is a str subclass. When type == &PyUnicode_Type, it returns the result of PyObject_Str(x) as-is, preserving any str subclass type that __str__ or __repr__ returned.

RustPython's PyStr::Constructor::py_new extracted the raw bytes via Self::from(s.as_wtf8().to_owned()), then slot_new re-materialized through into_ref_with_type(vm, cls). This stripped the subclass type even when cls is str.

Repro

class MyStr(str): pass
class Foo:
    def __repr__(self): return MyStr('hello')

print(type(str(Foo())))
# CPython 3.14:    <class '__main__.MyStr'>
# RustPython:      <class 'str'>     (before this PR)

Fix

Add a branch in slot_new that returns input.str(vm)? directly when cls is str_type (no subtype conversion, no encoding). Subtype construction (StrSubclass(...)) and the str(bytes, encoding) decoding path are unchanged.

Tests unmasked

  • test_str.StrTest.test_conversion (11 assertTypedEqual cases covering str(WithStr/StrWithStr/WithRepr(...)) × StrSubclass / OtherStrSubclass permutations)

Verification

  • CPython 3.14.4 byte-identical for 8 probed cases (plain str(), str('hi'), str(MyStr('x')), __str__/__repr__ returning subclass, MyStr('z'), OtherStr(MyStr('w')), str(b'abc', 'utf-8'))
  • No regressions across 14 modules (~3,349 tests): test_str, test_descr, test_class, test_typing, test_userstring, test_format, test_pickle, test_io, test_collections, test_dict, test_set, test_exceptions, test_fstring, test_genericalias

Summary by CodeRabbit

  • Bug Fixes
    • Improved string constructor to correctly preserve behavior when using user-defined string subclasses.

Closes RustPython#7450.

CPython's unicode_new_impl returns the PyObject_Str result as-is when
type == &PyUnicode_Type, only invoking unicode_subtype_new for actual
str subclasses. RustPython's PyStr::Constructor stripped the result via
Self::from(s.as_wtf8().to_owned()) and re-materialized through
into_ref_with_type, dropping the subclass type even when cls is exactly
str.

Add a slot_new branch that returns input.str(vm)? directly when cls is
str_type with no encoding. Subtype construction and the bytes-decoding
path are unchanged.

Unmasks test_str.StrTest.test_conversion (11 assertTypedEqual cases).
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

@github-actions

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

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

[x] test: cpython/Lib/test/test_str.py (TODO: 14)
[x] test: cpython/Lib/test/test_fstring.py (TODO: 19)
[x] test: cpython/Lib/test/test_string_literals.py (TODO: 4)

dependencies:

dependent tests: (no tests depend on str)

Legend:

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

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

Great!
ty for finding these:)

@changjoon-park

Copy link
Copy Markdown
Contributor Author

@youknowone
youknowone merged commit 6c498fc into RustPython:main Apr 28, 2026
21 checks passed
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.

str() discards str subclass type returned by __repr__

3 participants