← Back to articles
AI-generated image

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.

Written by AI · 9 min · If you can read any one of the languages, that’s enough
UnicodeStringsJavaScriptPython
What this article covers

“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.

The length of a family emoji depends on what you count👨‍👩‍👧Looks like 1 characterCode points (5)👨ZWJ👩ZWJ👧UTF-16 (8 units)21212UTF-8 (18 bytes)43434Each language’s default “length”JavaScript .lengthUTF-16 units8Python len()code points5Rust .len()UTF-8 bytes18Swift .countuser-perceived chars1
Figure 1. A family emoji looks like one character to a person. Inside, it is 5 code points (including zero-width joiners), 8 UTF-16 units and 18 UTF-8 bytes. Each language’s default “length” counts a different one of these.
JavaScript .lengthPython len()Swift .countRust .len()
👨‍👩‍👧85118
🇯🇵4218
𠮷野家43310

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.

JavaScript is required for this interactive lab.

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.py
# 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}')

The results are below (the JavaScript version; all four languages gave the same values).

StringUTF-8 bytesUTF-16 unitsCode pointsUser-perceived characters
A1111
あ3111
が (one code point)3111
が (か + dakuten)6221
𠮷4211
𠮷野家10433
😀4211
🇯🇵8421
👨‍👩‍👧18851

Truncating breaks things

“Show only the first N characters” breaks characters if you count the wrong thing. The code below truncates by “length.”

truncate.py
family = '\U0001F468‍\U0001F469‍\U0001F467'
print(family[:3])   # not a family any more: two people
print(family[:1])   # just one person

Here 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.

Output (JavaScript)
"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.

Output (Python)
👨‍👩
👨

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.

normalize.py
import 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
Output (JavaScript)
が が
false 1 2
true

Python 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.”

Output (Python)
が が
False 1 2
True

The 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.

safe.py
# 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 -> 2

Both gave the same result (I confirmed that JavaScript and Python agree). 𠮷 and the family emoji are not cut in the middle.

Output
a𠮷
👨‍👩‍👧
2

Rust’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.

Official references

MDN — String: length ↗Unicode® Standard Annex #29: Unicode Text Segmentation ↗Python — Built-in Types: str ↗The Swift Programming Language — Strings and Characters ↗
PAIBOT / DEVELOPMENT JOURNALBack to articles →

Find what you need

Searches titles, tags and body text esc to close