Text, characters and encodings
Explain how text is stored as code points and bytes, and why the length of a string depends on what you count.
What you will learn
- Explain strings, indexing and immutability
- Distinguish characters, code points and bytes
- Explain what UTF-8 does and why encodings matter
- Build text with formatting instead of repeated concatenation
On this page
Text feels like the simplest kind of data and is among the trickiest. The word “length” turns out to have three plausible meanings, and a mismatch between them has caused garbled names and broken file reads in nearly every system ever written.
Strings
A string is a sequence of characters treated as one value. You write one with quotes. Strings have an order, so each character has a position, its index. Most languages count from zero.
text: h e l l oindex: 0 1 2 3 4The first character is at index 0 and the last at index length minus one. Asking for an index beyond the end is an error in most languages. Several (Python, for example) also allow negative indexes from the end: -1 is the last character.
Python
text = "hello"print(text[0], text[4], len(text))print(text[-1])h o 5oJavaScript
const text = "hello";console.log(text[0], text[4], text.length);console.log(text.at(-1));h o 5oat(-1) is JavaScript’s way to count from the end; plain text[-1] gives undefined.
Java
public class Indexing { public static void main(String[] args) { String text = "hello"; System.out.println(text.charAt(0) + " " + text.charAt(4) + " " + text.length()); System.out.println(text.charAt(text.length() - 1)); }}h o 5oJava has no negative indexes, so the last character is found with length() - 1.
C
#include <stdio.h>#include <string.h>
int main(void) { const char *text = "hello"; printf("%c %c %zu\n", text[0], text[4], strlen(text)); printf("%c\n", text[strlen(text) - 1]); return 0;}h o 5oC has no negative indexes either. strlen counts characters up to the zero byte that ends every C string.
Immutability
In most languages strings are immutable: you cannot change a character in place. Operations that appear to modify a string actually build a new one. To turn cat into bat, build a new string and rebind the name:
Python
word = "cat"# word[0] = "b" # TypeError: strings do not support item assignmentword = "b" + word[1:]print(word)JavaScript
let word = "cat";word[0] = "x"; // silently ignored in sloppy mode; an error in strict modeword = "b" + word.slice(1);console.log(word);Java
public class Immutable { public static void main(String[] args) { String word = "cat"; word = "b" + word.substring(1); // a new String; the original is untouched System.out.println(word); }}C
#include <stdio.h>
int main(void) { char word[] = "cat"; /* a mutable array of characters */ word[0] = 'b'; printf("%s\n", word); return 0;}C is the exception: a string held in a char array can be changed in place. That freedom comes with the usual dangers of writing past the end.
All four print bat.
Characters, code points and bytes
Here is the heart of the lesson. Three different units are in play:
| Unit | What it is | Example |
|---|---|---|
| Character (as a reader sees it) | A symbol on the screen | é, a, an emoji |
| Code point | A number assigned to a symbol by the Unicode standard | é is U+00E9 (233) |
| Byte | Eight bits, the unit of storage and transmission | é takes 2 bytes in UTF-8 |
Unicode is a catalogue that assigns every symbol, from Latin to Chinese to emoji, a unique code point. It says nothing about bytes. An encoding is the rule for turning code points into bytes.
The dominant encoding is UTF-8. It uses 1 byte for the original ASCII characters (plain English letters, digits, punctuation) and 2 to 4 bytes for everything else. It is compact for English, handles every language, and is the standard for files and the web.
character code point UTF-8 bytes (hex)a U+0061 61é U+00E9 C3 A9€ U+20AC E2 82 AC😀 U+1F600 F0 9F 98 80How languages count
The text “naïve café” has 10 characters, but ï and é each need two bytes in UTF-8, so it occupies 12 bytes. The same visible text yields different numbers depending on how the language defines string length. Each program counts “naïve café” and one emoji. The source files must be saved as UTF-8.
Python
word = "naïve café"print(len(word))print(len(word.encode("utf-8")))print(len("😀"))print(len("😀".encode("utf-8")))101214Python counts code points: the emoji is 1.
JavaScript
const word = "naïve café";console.log(word.length);console.log(new TextEncoder().encode(word).length);console.log("😀".length);console.log(new TextEncoder().encode("😀").length);console.log([..."😀"].length);1012241JavaScript counts UTF-16 units, so an emoji outside the basic range counts as 2. Spreading into an array iterates by code point, which gives 1.
Java
import java.nio.charset.StandardCharsets;
public class TextCounts { public static void main(String[] args) { String word = "naïve café"; String emoji = "😀"; System.out.println(word.length()); System.out.println(word.getBytes(StandardCharsets.UTF_8).length); System.out.println(emoji.length()); System.out.println(emoji.getBytes(StandardCharsets.UTF_8).length); System.out.println(emoji.codePointCount(0, emoji.length())); }}1012241Java also counts UTF-16 units; codePointCount gives the code points.
C
#include <stdio.h>#include <string.h>
/* Count code points by skipping UTF-8 continuation bytes (10xxxxxx). */static int code_points(const char *s) { int count = 0; for (; *s; s++) { if (((unsigned char)*s & 0xC0) != 0x80) { count++; } } return count;}
int main(void) { const char *word = "naïve café"; const char *emoji = "😀"; printf("%d\n", code_points(word)); printf("%zu\n", strlen(word)); printf("%d\n", code_points(emoji)); printf("%zu\n", strlen(emoji)); return 0;}101214strlen counts bytes, so counting code points takes a small helper. C has no notion of characters beyond bytes.
So the emoji is 1 code point, 2 UTF-16 units, and 4 UTF-8 bytes. None of those is wrong; they answer different questions.
One more wrinkle: what a reader sees as one character can be several code points. The accented “é” can be the single code point U+00E9 or an “e” followed by a combining accent U+0301. They look identical, yet compare as unequal. Text from different sources should be normalised to a single form before comparing, which most languages support in their standard library.
Python
import unicodedata
composed = "\u00e9"decomposed = "e\u0301"print(composed == decomposed, len(composed), len(decomposed))print(composed == unicodedata.normalize("NFC", decomposed))False 1 2TrueJavaScript
const composed = "\u00e9";const decomposed = "e\u0301";console.log(composed === decomposed, composed.length, decomposed.length);console.log(composed === decomposed.normalize("NFC"));false 1 2trueJava
import java.text.Normalizer;
public class Normalise { public static void main(String[] args) { String composed = "\u00e9"; String decomposed = "e\u0301"; System.out.println(composed.equals(decomposed) + " " + composed.length() + " " + decomposed.length()); System.out.println(composed.equals(Normalizer.normalize(decomposed, Normalizer.Form.NFC))); }}false 1 2trueC
#include <stdio.h>#include <string.h>
int main(void) { const char *composed = "\xc3\xa9"; /* U+00E9 as UTF-8 */ const char *decomposed = "e\xcc\x81"; /* e + U+0301 as UTF-8 */ printf("%d %zu %zu\n", strcmp(composed, decomposed) == 0, strlen(composed), strlen(decomposed)); return 0;}0 2 3C reports bytes, so the lengths are 2 and 3, but the verdict is the same: not equal. Normalising needs a library such as ICU, because the C standard library has no function for it.
Why encodings matter
A file is just bytes. To read it as text you must know the encoding it was written in. Decode UTF-8 bytes as an older encoding such as Latin-1, and é (the two bytes C3 A9) turns into the two characters é. That garbling is called mojibake. Decode the wrong way round, and you may even hit an outright error because some byte sequences are not valid in the encoding.
The practical rule: use UTF-8 for everything you write, and state the encoding explicitly whenever you read or write a file or network data rather than trusting a default.
Escaping
Some characters cannot be typed directly inside quotes. A backslash begins an escape sequence:
| Sequence | Meaning |
|---|---|
\n |
newline |
\t |
tab |
\" |
a double quote inside double quotes |
\\ |
a literal backslash |
\u00e9 |
the code point U+00E9 (Python, JavaScript, Java) |
Backslashes are also why Windows paths in code surprise people: "C:\new" contains a newline. Double the backslashes or use your language’s raw-string form.
Joining and formatting
Joining strings is called concatenation, usually with +. It works for a few pieces, but it gets clumsy, and in most languages you must convert numbers to strings first. C has no + for text at all.
Formatting inserts values into a template and controls layout. Python has f-strings, JavaScript has template literals plus padding methods, and Java and C share the printf family of templates. Each program builds one receipt line: the name left-aligned in 10 columns, the quantity right-aligned in 3, and two prices with two decimals.
Python
name, qty, price = "Tea", 3, 2.5print(f"{name:<10}{qty:>3} x {price:>6.2f} = {qty * price:>7.2f}"){name:<10} left-aligns in 10 columns; {price:>6.2f} right-aligns a float in 6 columns with two decimals.
JavaScript
const name = "Tea";const qty = 3;const price = 2.5;console.log( `${name.padEnd(10)}${String(qty).padStart(3)} x ${price.toFixed(2).padStart(6)} = ${(qty * price).toFixed(2).padStart(7)}`);JavaScript has no format mini-language, so padding and rounding are separate method calls: padEnd, padStart and toFixed.
Java
public class Receipt { public static void main(String[] args) { String name = "Tea"; int qty = 3; double price = 2.5; System.out.println(String.format("%-10s%3d x %6.2f = %7.2f", name, qty, price, qty * price)); }}In the template, %-10s is a left-aligned string of width 10, %3d an integer of width 3, and %6.2f a float of width 6 with two decimals.
C
#include <stdio.h>
int main(void) { const char *name = "Tea"; int qty = 3; double price = 2.5; printf("%-10s%3d x %6.2f = %7.2f\n", name, qty, price, qty * price); return 0;}C uses the same template as Java, because Java borrowed it from C.
All four print:
Tea 3 x 2.50 = 7.50Prefer formatting once you are inserting more than one value or care about alignment and decimals. If you build a very long string by adding one piece at a time in a loop, collect the pieces and join them once at the end; many languages copy the whole string at every +.
Common pitfalls
- Assuming one character is one byte. True only for ASCII.
- Using byte length to mean display length (or the reverse), for example when truncating a name to fit a database column.
- Not stating the encoding when opening files, so code works on your machine and fails on a colleague’s.
- Comparing visually identical strings that use different code point sequences.
- Treating a string as editable in a language where it is immutable.
- Forgetting to convert numbers before concatenating.
How many characters and how many UTF-8 bytes are in “naïve café”? Explain the difference without running code. Expected: two numbers, a character count and a byte count.
Show solution
Count characters: n, a, ï, v, e, space, c, a, f, é gives 10. Every character is one byte except ï and é, which take two bytes each in UTF-8, so the byte count is 10 + 2 = 12. The text_counts program above confirms both numbers.
Using formatting rather than concatenation, produce a line in which the item name is left-aligned in 10 columns, the quantity right-aligned in 3, and the unit price and total shown with two decimals. Use the inputs Pen, 12 and 0.75. Expected output: Pen 12 x 0.75 = 9.00.
Show solution
Only the values change from the receipt program above.
Python
name, qty, price = "Pen", 12, 0.75print(f"{name:<10}{qty:>3} x {price:>6.2f} = {qty * price:>7.2f}")JavaScript
const name = "Pen";const qty = 12;const price = 0.75;console.log( `${name.padEnd(10)}${String(qty).padStart(3)} x ${price.toFixed(2).padStart(6)} = ${(qty * price).toFixed(2).padStart(7)}`);Java
public class ReceiptPen { public static void main(String[] args) { String name = "Pen"; int qty = 12; double price = 0.75; System.out.println(String.format("%-10s%3d x %6.2f = %7.2f", name, qty, price, qty * price)); }}C
#include <stdio.h>
int main(void) { const char *name = "Pen"; int qty = 12; double price = 0.75; printf("%-10s%3d x %6.2f = %7.2f\n", name, qty, price, qty * price); return 0;}All four print Pen 12 x 0.75 = 9.00. The format specifications give width, alignment and decimals, so no manual padding or number-to-text conversion is needed.
A string s holds "hello". Make it hold "jello" and print it. Expected output: jello. Which of the four languages let you do it by changing the first character in place?
Show solution
Only C allows an in-place change, because its string is a mutable array. In Python, JavaScript and Java the string is immutable, so you build a new string and rebind the name.
Python
s = "hello"s = "j" + s[1:]print(s)JavaScript
let s = "hello";s = "j" + s.slice(1);console.log(s);Java
public class Jello { public static void main(String[] args) { String s = "hello"; s = "j" + s.substring(1); System.out.println(s); }}C
#include <stdio.h>
int main(void) { char s[] = "hello"; s[0] = 'j'; printf("%s\n", s); return 0;}All four print jello.
Summary
- A string is an ordered, usually immutable sequence; indexes normally start at 0.
- Characters, code points and bytes are different units: Unicode assigns code points, and an encoding such as UTF-8 turns them into bytes.
- Always state the encoding when reading or writing, and use formatting to assemble text.
This completes the first module of the shared track. The next module covers control flow and functions.