I am considering using UTF-8 as Unicode’s internal representation. The potential benefits are:
- PEP 393’s Flexible String Representation (FSR) is an implementation detail and is not part of the Stable ABI. Some extension modules use the fastest available APIs for performance reasons, which makes moving to the Stable ABI harder. Eliminating the overhead of creating a UTF-8 cache could help encourage Stable ABI adoption.
- Python is a glue language. C, C++, and Rust have many libraries that process UTF-8 efficiently, but supporting FSR requires hand-written code.
- UTF-8 can use less memory for large strings when only some characters require the 4-byte kind.
Reference implementation
Specification
- Ordinary non-ASCII strings retain a UTF-8 representation. The PEP 393 fixed-width representation (FSR) is created and retained only when needed.
- Lone surrogates are preserved internally using
surrogatepass, without changing Python-level semantics such as string length and indexing. - Strings under construction with
PyUnicode_New()continue to use a writable FSR. - Many paths, including string operations,
UnicodeWriter, codecs, and JSON, have been changed to process UTF-8 directly or read characters sequentially. PyUnicode_KIND()returns the width of the fixed-width representation.PyUnicode_DATA()and the various*_DATA()functions create the FSR lazily, so they may returnNULLif memory is exhausted.PyUnicode_READ_CHAR()andPyUnicode_ReadChar()normally create an FSR cache and read from it. If memory is exhausted, they fall back to scanning the UTF-8 representation.PyUnicode_READ(), which takes a pointer, has no such fallback.- Writes are limited to construction buffers created by
PyUnicode_New(). CallingPyUnicode_WriteChar(),PyUnicode_Fill(), orPyUnicode_CopyCharacters()on a string whose primary representation is UTF-8 callsabort(). A UTF-8 representation is generated for construction-buffer strings when they are read. PyUnicode_New()and the write APIs will be deprecated. When these APIs are removed in the future, we plan to maintain O(1) random access using a more efficient position table, similar to PyPy’s.
Open questions
PyUnicode_DATA()currently cannot fail. In this implementation, it can returnNULL. Extension modules that do not check forNULLwould likely crash immediately, for example with a segmentation fault. Is there a better migration path?- The current implementation hashes the UTF-8 representation directly, so
hash(s) == hash(s.encode()). Should we adjust the string hash to distinguish it from the bytes hash, given that a dictionary may contain bothstrandbyteskeys?