
The length of "👨👩👧" is 8 in JavaScript, 5 in Python, 18 in Rust and 1 in Swift
The same string has a different “length” in each language. I counted 𠮷野家, a flag and a family emoji in four languages, to see why, and how to handle them without breaking anything.
“Number of characters” can be counted four ways: bytes, UTF-16 units, code points, and what a person sees as one character. Each language’s default “length” counts a different one. I checked it in four languages.
The results first
The emoji 👨👩👧 looks like one character. Count its length with each language’s default method and the answers all differ.
| JavaScript .length | Python len() | Swift .count | Rust .len() | |
|---|---|---|---|---|
| 👨👩👧 | 8 | 5 | 1 | 18 |
| 🇯🇵 | 4 | 2 | 1 | 8 |
| 𠮷野家 | 4 | 3 | 3 | 10 |
None of them is wrong. They just count different things. Under the same name, “length,” each language returns something else. This article is the result of actually running the code in four languages.
Try your own text
Type anything: a name, an emoji, a flag. The four counts change, and you can watch the text split into characters. With the slider you can also compare what breaks and what doesn’t when you take the first few characters. Everything is computed in your own browser.
Four ways to count
“Number of characters” changes depending on which of these four you count.
- UTF-8 bytes: the size when written to a file or sent over a network. In my runs, Rust’s .len() returned this
- UTF-16 units: what JavaScript’s length counts. MDN describes it as the length “in UTF-16 code units,” and notes that one Unicode character can be one or two units, so it may not match the actual number of characters
- Code points: the number assigned to each Unicode character. A Python str is an “immutable sequence of Unicode code points,” and len() is its count (official documentation)
- User-perceived characters (grapheme clusters): what a person feels is one character. Unicode’s UAX #29 gives the example of “G” plus a grave accent: users think of it as a single character, yet it is represented by two code points. In my runs, Swift’s count returned this
Checking it in code
I counted the same nine strings four ways in four languages (I ran them on Node.js 26.1.0, Python 3.11.5, Swift 6.1 and Rust 1.86.0). For the counts they share, the four languages agreed exactly. Rust has three, because its standard library can’t count graphemes. Counting graphemes in Python uses the regex library, so run pip install regex first.
// Count the same strings in four ways (JavaScript)
const samples = [
['A', 'A'],
['あ', 'あ'],
['が (one code point)', 'が'],
['が (か + dakuten)', 'が'],
['𠮷', '\u{20BB7}'],
['𠮷野家', '\u{20BB7}野家'],
['😀', '\u{1F600}'],
['🇯🇵', '\u{1F1EF}\u{1F1F5}'],
['👨👩👧', '\u{1F468}\u{1F469}\u{1F467}'],
];
const segmenter = new Intl.Segmenter('ja', { granularity: 'grapheme' });
for (const [name, s] of samples) {
const utf8 = Buffer.byteLength(s, 'utf8');
const utf16 = s.length; // JavaScript's length is the number of UTF-16 code units
const codePoints = [...s].length; // spreading gives code points
const graphemes = [...segmenter.segment(s)].length;
console.log(`${name}\tutf8=${utf8}\tutf16=${utf16}\tcodepoints=${codePoints}\tgraphemes=${graphemes}`);
}# Count the same strings in four ways (Python). Counting graphemes is not in the standard library, so we use regex
import regex # pip install regex
samples = [
('A', 'A'),
('あ', 'あ'),
('が (one code point)', 'が'),
('が (か + dakuten)', 'が'),
('𠮷', '\U00020BB7'),
('𠮷野家', '\U00020BB7野家'),
('😀', '\U0001F600'),
('🇯🇵', '\U0001F1EF\U0001F1F5'),
('👨👩👧', '\U0001F468\U0001F469\U0001F467'),
]
for name, s in samples:
utf8 = len(s.encode('utf-8'))
utf16 = len(s.encode('utf-16-le')) // 2 # number of UTF-16 code units
codepoints = len(s) # Python's len is the number of code points
graphemes = len(regex.findall(r'\X', s)) # \X matches one user-perceived character
print(f'{name}\tutf8={utf8}\tutf16={utf16}\tcodepoints={codepoints}\tgraphemes={graphemes}')// Count the same strings in four ways (Swift)
let samples: [(String, String)] = [
("A", "A"),
("あ", "あ"),
("が (one code point)", "\u{304C}"),
("が (か + dakuten)", "\u{304B}\u{3099}"),
("𠮷", "\u{20BB7}"),
("𠮷野家", "\u{20BB7}野家"),
("😀", "\u{1F600}"),
("🇯🇵", "\u{1F1EF}\u{1F1F5}"),
("👨👩👧", "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}"),
]
for (name, s) in samples {
// Swift's count is the number of user-perceived characters (grapheme clusters)
print("\(name)\tutf8=\(s.utf8.count)\tutf16=\(s.utf16.count)\tcodepoints=\(s.unicodeScalars.count)\tgraphemes=\(s.count)")
}// Count the same strings in three ways (Rust). Graphemes aren't in the standard library, so we don't count them here
fn main() {
let samples = [
("A", "A".to_string()),
("あ", "あ".to_string()),
("が (one code point)", "\u{304C}".to_string()),
("が (か + dakuten)", "\u{304B}\u{3099}".to_string()),
("𠮷", "\u{20BB7}".to_string()),
("𠮷野家", "\u{20BB7}野家".to_string()),
("😀", "\u{1F600}".to_string()),
("🇯🇵", "\u{1F1EF}\u{1F1F5}".to_string()),
("👨👩👧", "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}".to_string()),
];
for (name, s) in samples.iter() {
// len() is the number of UTF-8 bytes, chars().count() is the number of code points
println!("{}\tutf8={}\tutf16={}\tcodepoints={}", name, s.len(), s.encode_utf16().count(), s.chars().count());
}
}The results are below (the JavaScript version; all four languages gave the same values).
| String | UTF-8 bytes | UTF-16 units | Code points | User-perceived characters |
|---|---|---|---|---|
| A | 1 | 1 | 1 | 1 |
| あ | 3 | 1 | 1 | 1 |
| が (one code point) | 3 | 1 | 1 | 1 |
| が (か + dakuten) | 6 | 2 | 2 | 1 |
| 𠮷 | 4 | 2 | 1 | 1 |
| 𠮷野家 | 10 | 4 | 3 | 3 |
| 😀 | 4 | 2 | 1 | 1 |
| 🇯🇵 | 8 | 4 | 2 | 1 |
| 👨👩👧 | 18 | 8 | 5 | 1 |
Truncating breaks things
“Show only the first N characters” breaks characters if you count the wrong thing. The code below truncates by “length.”
const s = 'a\u{20BB7}b'; // a𠮷b
const cut = s.slice(0, 2); // take the first 2 code units
console.log(JSON.stringify(cut)); // cut in the middle of 𠮷: a broken character
console.log(cut.isWellFormed()); // false
const family = '\u{1F468}\u{1F469}\u{1F467}';
console.log([...family].slice(0, 3).join('')); // not a family any more: two peoplefamily = '\U0001F468\U0001F469\U0001F467'
print(family[:3]) # not a family any more: two people
print(family[:1]) # just one personHere is the JavaScript output. Taking the first 2 units of a𠮷b cuts 𠮷 in the middle and leaves a broken value (a lone surrogate), and isWellFormed() returned false. Taking three code points of the family emoji gives an emoji of two people, not a family.
"a\ud842"
false
👨👩Python cuts by code point, so it never breaks 𠮷 in the middle. But the family emoji still turns into two people or one.
👨👩
👨The same “が” can be written two ways
“が” can be written as one code point, or as two: “か” plus a dakuten (voiced mark). They look the same but differ inside, and comparing them says they are not equal.
const a = 'が'; // が (one code point)
const b = 'が'; // か + dakuten (two code points)
console.log(a, b); // they look the same
console.log(a === b, a.length, b.length);
console.log(a === b.normalize('NFC')); // equal once normalizedimport unicodedata
a = 'が' # が (one code point)
b = 'が' # か + dakuten (two code points)
print(a, b) # they look the same
print(a == b, len(a), len(b))
print(a == unicodedata.normalize('NFC', b)) # equal once normalizedが が
false 1 2
truePython gave the same result. Once normalized (NFC), they become equal. This difference can be the cause when search or duplicate checks say “they look the same but don’t match.”
が が
False 1 2
TrueThe fix: work in user-perceived characters
To count or cut by user-perceived character, you need to split into graphemes. JavaScript can do it with the built-in Intl.Segmenter. Python’s standard library can’t, so use \X from the regex library. In Swift, the built-in count is already user-perceived characters.
// Take the first n user-perceived characters (graphemes)
const segmenter = new Intl.Segmenter('ja', { granularity: 'grapheme' });
const graphemes = s => [...segmenter.segment(s)].map(x => x.segment);
const truncate = (s, n) => graphemes(s).slice(0, n).join('');
console.log(truncate('a\u{20BB7}b家', 2)); // a𠮷
console.log(truncate('\u{1F468}\u{1F469}\u{1F467}と\u{1F1EF}\u{1F1F5}', 1)); // the family emoji stays whole
console.log(graphemes('\u{1F1EF}\u{1F1F5}\u{1F1FA}\u{1F1F8}').length); // two flags -> 2# Take the first n user-perceived characters (graphemes)
import regex # pip install regex
def graphemes(s):
return regex.findall(r'\X', s)
def truncate(s, n):
return ''.join(graphemes(s)[:n])
print(truncate('a\U00020BB7b家', 2)) # a𠮷
print(truncate('\U0001F468\U0001F469\U0001F467と\U0001F1EF\U0001F1F5', 1)) # the family emoji stays whole
print(len(graphemes('\U0001F1EF\U0001F1F5\U0001F1FA\U0001F1F8'))) # two flags -> 2Both gave the same result (I confirmed that JavaScript and Python agree). 𠮷 and the family emoji are not cut in the middle.
a𠮷
👨👩👧
2Rust’s standard library has no grapheme splitting, so you need an external crate. I did not try graphemes in Rust for this article. How graphemes are split can change with the Unicode version and the implementation.
Checklist
Things to check when handling strings.
- When you set a “character” limit, you decided the unit (bytes, UTF-16, code points, user-perceived characters)
- You truncate by user-perceived character (not by length or len())
- You normalize (NFC and so on) before comparing or searching
- You know that an emoji or a flag that looks like one character is several code points inside
- You checked the database’s own unit for its character limits (I did not test this)
Not tested: other languages (Java, Go, C# and so on), database character limits, and how each web service counts characters. None of these are covered by the results here.