Python Unicode Storage: ASCII vs UCS-1, UCS-2, UCS-4

Python optimizes in-memory string management through the Flexible String Representation introduced in PEP 393. This article examines the architectural differences between compact ASCII strings and UCS-1, UCS-2, and UCS-4 Unicode representations in CPython, explaining how character ranges, memory footprints, and underlying C structures distinguish each format.

The Flexible String Representation (PEP 393)

Prior to Python 3.3, CPython relied on either a "narrow" build using 2-byte units (UCS-2) or a "wide" build using 4-byte units (UCS-4). This approach either wasted vast amounts of memory for basic Latin text or failed to support characters outside the Basic Multilingual Plane (BMP) without complex surrogate pairs.

PEP 393 solved this by selecting the minimal required byte-width for a string based on the maximum Unicode code point it contains. All strings are stored as fixed-width arrays, preserving \(O(1)\) random character access by index.

Compact ASCII Strings

A string is classified as compact ASCII when every character falls strictly within the 7-bit ASCII range (\(U+0000\) to \(U+007F\)).

UCS-1 (Latin-1) Representation

A string uses the UCS-1 representation when its maximum code point is between \(U+0080\) and \(U+00FF\).

UCS-2 Representation

Python escalates a string to UCS-2 when at least one code point exceeds \(U+00FF\), but no code points exceed \(U+FFFF\).

UCS-4 Representation

When a string contains even a single code point greater than \(U+FFFF\), Python allocates it using UCS-4.

Key Distinctions Summary

Feature Compact ASCII UCS-1 UCS-2 UCS-4
Max Code Point \(U+007F\) \(U+00FF\) \(U+FFFF\) \(U+10FFFF\)
Bytes / Char 1 byte 1 byte 2 bytes 4 bytes
C Structure PyASCIIObject PyCompactUnicodeObject PyCompactUnicodeObject PyCompactUnicodeObject
Base Header (64-bit) 48 bytes 72 bytes 72 bytes 72 bytes
Native UTF-8 Reuse Yes (shared buffer) No (requires encoding) No (requires encoding) No (requires encoding)