5 ms·
The Unicode model is complex and overengineered. Like many Python features, it requires permanent marketing until people believe it is good. It provides a stead
by zgrxat 4y ago
The Unicode model is complex and overengineered. Like many Python features, it requires permanent marketing until people believe it is good. It provides a steady income stream for those paid to "work" on Python.
The right way would have been utf-8, which is the only reasonable representation.
Indexing could have been broken early in Python 3 or outsourced to a special type.
- StefanKarpinski 4y agoAbsolutely right. Deprecating direct string indexing would have been the right move. Require writing `str.chars()` to get something that lets you slice by Unicode characters (i.e. code points); provide `str.graphemes()` and `str.grapheme_clusters()` to get something that lets you slice by graphemes and grapheme clusters, respectively. Cache an index structure that lets you do that kind of indexing efficiently once you've asked for it the first time. Provide an API to clear the caches. Not allowing strings to represent invalid Unicode is also a huge mistake (and essentially forced by the representation strategy that they adopted). It forces any programmer who wants to robustly handle potentially invalid string data to use byte vectors instead. Which is exactly what they did with OS paths, but that's far from the only place you can get invalid strings. You can get invalid strings almost anywhere! Worse, since it's incredibly inconvenient to work with byte vectors when you want to do stringlike stuff, no one does it unless forced to, so this design choice effectively guarantees that all Python code that works with strings will blow up if it encounters anything invalid—which is a very common occurrence. If only there was a type that behaves like a string and supports all the handy string operations but which handles invalid data gracefully. Then you could write robust string code conveniently. But at that point, you should just make that the standard string type! This isn't hypothetical, it's exactly how Burnt Sushi's bstr type [1] works in Rust and how the standard String type works in Julia. [1] https://github.com/BurntSushi/bstr https://github.com/BurntSushi/bstr
- Jasper_ 4y agoIt's worth noting that Python str's are sequences of code points, not scalar values. This was a truly horrendous mistake made mostly out of ignorance, but now they rely upon it in surrogateescape to hide "invalid" data, so... I have ranted for long hours go friends about the insanity of Python 3's text model before. It's mostly the blind leading the blind.
- 323 4y agoI've seen this happen multiple times in Python, where they implement a feature without a thorough survey of what other languages are doing. Then when it turns out the new feature has a major broken part, they fix that, but again, without looking how it's done in other places. asyncio is a major example.
- Spivak 4y ago> It forces any programmer who wants to robustly handle potentially invalid string data to use byte vectors instead. Is this not the only thing you can really do? If strings could hold invalid unicode then they effectively become bytes and you now have to be wary of every possible string. I would rather Python just do away with strings entirely in the integration points with the OS and make you either keep them as opaque bytes or decode them. > behaves like a string and supports all the handy string operations but which handles invalid data gracefully Unless Guido was feeling particularly practical I can't imagine this ever making it into the stdlib because the choice of "gracefully" is somewhat arbitrary and application dependent.
- wk_end 4y agoDeprecating direct string indexing might have been the "right" move in some sense but it's hard to imagine it ever happening in Python; it's such a natural and frequently-used operation in the language that you would've broken an enormous amount of code. Because indexing strings is syntactically identical to indexing lists, and because there's no* real static typing, there'd be no* good and robust way to automate the conversion. Like, the changes Python 3 made were honestly pretty subtle, and nearly 15 years later there's still people reluctant to upgrade. If they broke string indexing I'm pretty sure Python 3 adoption would make Perl 6 uptake look impressive.
- Animats 4y agoUnicode string indexing should have been made lazy, rather than deprecated. Random access to strings is rare. Mostly, operations are moving forward linearly or using saved positions. So, only build the index for random access if needed. Optimize "advance one glyph" and "back up one glyph" expressed as indexing, and you'll get most of the frequently used cases. Have the "index" functions that return a string index return an opaque type that's a byte index. Attempting to convert that to an integer forces creation of the string index. This preserves the user visible semantics but keeps performance. PyPy does something like this.
- burntsushi 4y agoAnd note that bstr's design was based on Go's 'string' type, which has the same property: it is conventionally UTF-8.
- mistrial9 4y ago> requires permanent marketing LOL this is great, so true
- froh 4y agoutf-8 needs 24bit per glyph for the logographic unicode scripts, while utf-16 needs 16 bits. So if you're into selling silicon or don't care about these languages, or you only care about heavily mixed content (like html), then yes, utf-8 would be The Right Way (TM).
- zokier 4y agoUtf8 would not help with the issue in the article in any way.
- StefanKarpinski 4y agoIt's not at all obvious how it helps, but it does. First, why is Python unable to represent invalid path names as strings? Because internally it converts strings from UTF-8, UTF-16, or any other encoding, to a fixed-with array of decoded Unicode code points. The width of integer used to represent code points is determined by the largest code point in the string: if the string is ASCII, it can use a byte (uint8) per character; if the string is non-ASCII but all BMP, then it can use a uint16 per character; otherwise it has to use uint32 per character. Why does Python do all this? So that you can have O(1) character indexing. If you gave up on that, you wouldn't need to convert the string at all, you could just leave it as (potentially invalid) UTF-8 data. Suppose you get an invalid path on UNIX where paths are UTF-8 by convention? What does Python do with this string? It can't convert it to an array of code points because invalid UTF-8 doesn't correspond to a code point (well, it can if it's just illegal, not malformed, but in general, we have to consider completely malformed strings that don't even follow the basic UTF-8 format). So Python is stuck: it can only replace the invalid data with something like the Unicode replacement character. But then you can't do anything useful with that because it's not the correct name of the path you're trying to work with. How does using UTF-8 to represent strings help? Because you can represent invalid strings: just leave them as-is and don't try to decode them unless you have to. Sure, you can't decode them as code points, but that's actually a pretty unusual thing to do. If someone asks for decoding, _then_ you can give an error. What about Windows where paths are UTF-16 by convention? You can convert them to WTF-8 and everything works out. (Described in way more detail here: https://news.ycombinator.com/item?id=33984308 https://news.ycombinator.com/item?id=33984308).
- masklinn 4y ago> How does using UTF-8 to represent strings help? Because you can represent invalid strings: just leave them as-is and don't try to decode them unless you have to. That’s not UTF8. That’s a bag’o bytes which might be UTF8. Very different thing. > Sure, you can't decode them as code points, but that's actually a pretty unusual thing to do. It’s not, any unicode-aware text processing does it implicitly. This means any such processing has to either perform its own validation that the input is valid, or it may fly off the rails entirely if fed nonsense. This also increases risks if security issues, either outright UBs, or the ability to smuggle payloads through overlong encoding.
- jerf 4y agoDo you mean the right way would be for Python to use utf-8 for file names, or for the filesystem? I'm not sure how "utf-8" is the solution in this case for the problem. The problem here is that file names aren't any kind of Unicode. You can have a utf-8 filename right next to a utf-16 file name right next to a Shift-JIS filename right next to one that's just binary gibberish that isn't a legal string in any encoding (that has any restrictions at all). They're just bytes with no indication of what coding scheme they are at all. You can't solve this problem with any Unicode solution. (Even the one that tries to wrap bad bytes in a recoverable way, whose name escapes me and I can't recall if it's actually a standard or not, would still make me nervous. You really need code that just treats the filenames as opaque bytes, and only later at display time tries to figure out if they're UTF-8 or not.) It doesn't matter what Unicode model Python used. That may solve Python's other problems, sure. But filenames would always be some sort of problem. Even if you do just treat them as bytes, all that really does is make your potential errors deterministic, which is still better than a crash probably, but the filesystem's lack of encoding standards is a fundamental problem that can't be solved by higher code levels.
- StefanKarpinski 4y agoOn UNIX, paths are UTF-8 by convention, but not forced to be valid. Treating paths as UTF-8 works very well as long as you hadn't also make the mistake of requiring your UTF-8 strings to be valid (which Python did, unfortunately). On Windows, paths are UTF-16 by convention, but also not forced to be valid. However, invalid UTF-16 can be faithfully converted to WTF-8 and converted back losslessly, so you can translate Windows paths to WTF-16 and everything Just Works™ [1]. There aren't any operating systems I'm aware of where paths are actually Shift-JIS by convention, so that seems like a non-issue. Using "UTF-8 by convention" strings works on all modern OSes. [1] Ok, here's why the WTF-8 thing works so well. If we write WTF-16 for potentially invalid UTF-16 (just arbitrary sequences of 16-bit code units), then the mapping between WTF-16 and WTF-8 space is a bijection because it's losslessly round-trippable. But more importantly, this WTF-8/16 bijection is also a homomorphism with respect to pretty much any string operation you can think of. For example `utf16_concat(a, b) == utf8_concat(wtf8(a), wtf8(b))` for arbitrary UTF-16 strings a and b. Similar identities hold for other string operations like searching for substrings or splitting on specific strings.