Toolkit Labs

Why len() disagrees with your database, your front end and your user

Because "how long is this string" has four correct answers, and your language, your database, your front end and your user are not all giving the same one. On a corpus of 300 strings built to contain the awkward cases, the byte count differs from the codepoint count on 294 of them, and the UTF-16 count differs on 60. All four numbers agree on 6.

The four lengths

A piece of text can be counted as grapheme clusters (what a person would call characters - one of these can be several codepoints), as codepoints (Unicode scalar values), as UTF-16 code units (where anything above U+FFFF costs two), or as UTF-8 bytes (one to four per codepoint). For plain ASCII all four are the same number, which is why this never shows up in testing and always shows up in production.

An accented letter written as a base letter plus a combining mark is one grapheme, two codepoints, two UTF-16 units and three bytes. An emoji with a skin tone and a joiner is one grapheme and can be seven codepoints and twenty bytes: 3 cases in this corpus tie for the worst ratio at 20 bytes over 7 codepoints, the first of them u300-0033.

Which layer counts which unit

wherewhat it counts
Python len(s)codepoints
JavaScript s.length, Java String.length(), C# .LengthUTF-16 code units
Go len(s), Rust String::len()UTF-8 bytes
PostgreSQL char_length()codepoints
PostgreSQL octet_length(), index size limitsbytes
MySQL VARCHAR(n) on utf8mb4characters to declare, bytes for row and index limits
JSON Schema maxLengthcodepoints
HTTP headers, URLs, log linesbytes
a text field a person is filling ingrapheme clusters

Read that table as a list of places where two limits that look identical are not. A form that allows twenty characters in the browser is counting UTF-16 units; the Python service behind it is counting codepoints; the column it lands in may be counting bytes. Three numbers, one field.

What that costs in practice

What to do instead

None of this is exotic input. It is a name with an accent, a message with an emoji, and text pasted out of a word processor.

Where these numbers come from

They are computed with Python's unicodedata on UCD 15.0.0 over UNICODE-300, a corpus of 300 labelled strings built to break naive text handling - 10 categories, 30 cases each, every case carrying its codepoint, UTF-8 and UTF-16 lengths, its normalisation flags and a one-line description of the trap. Twenty cases and the checker are free and public domain; the full corpus is a paid download. The corpus and the free sample are here.

Where these 300 strings come from

Every number on this page was computed from UNICODE-300: 300 labelled strings, each property measured with unicodedata rather than asserted. The full corpus is EUR 19, one-time; a 20-case slice and the checker are free on the product page, public domain, no account.