Mojibake: ðŸ / é / ’ instead of real text (UTF-8 vs Windows-1252)

The bytes are fine. Something read them with the wrong codebook, so every multi-byte character exploded into two or three "Latin" letters.

What it looks like

IntendedMojibake you seeWhat happened
é (U+00E9)éUTF-8 C3 A9 read as Windows-1252
' curly apostrophe’UTF-8 E2 80 99 read as CP1252
😀😀4-byte UTF-8 emoji read as CP1252
é againé / é twiceDouble-encoded: UTF-8 of already-mojibaked text

Kid-friendly version of the same bug: Wirebook emoji lesson (scene B).

How to spot it in 30 seconds

Undo a single round of mojibake

If UTF-8 was read as Latin-1/CP1252, each resulting character's code point is the original byte. Re-encode those code points as Latin-1 bytes, then decode as UTF-8:

# Python
fixed = bad.encode("latin-1").decode("utf-8")

# Node (Buffer)
fixed = Buffer.from(bad, "latin1").toString("utf8")

# MySQL (one-shot repair of a column that stored mojibake as characters)
UPDATE t SET col = CONVERT(CAST(CONVERT(col USING latin1) AS BINARY) USING utf8mb4);

Warning: only do this when you have confirmed a single mix-up. Double-encoded text needs the trick applied twice, and applying it to already-correct UTF-8 will crash or make things worse. Keep a backup. For emoji that became ? in the database, the data is gone — that is the utf8 vs utf8mb4 problem, not mojibake: Incorrect string value / utf8mb4.

Fix it at the source

LayerSet UTF-8 explicitly
HTML<meta charset="utf-8"> as the first child of <head>; serve Content-Type: text/html; charset=utf-8
Editor / filesSave as UTF-8 (preferably without BOM). In VS Code the status bar shows the encoding.
Browser form / fetchDefault is UTF-8 for modern HTML. Avoid legacy accept-charset traps.
Pythonopen(path, encoding="utf-8"); set PYTHONUTF8=1 on Windows; never rely on the system default for text.
NodeBuffers and fs.readFile(…, "utf8"). Be careful with latin1 / binary aliases — they are 1:1 byte maps, useful for repair, dangerous as a default.
MySQL / MariaDBConnection + table + column all utf8mb4. SET NAMES utf8mb4. See the utf8mb4 guide.
HTTP APIsJSON is UTF-8 by definition. For form bodies, declare charset. Do not "helpfully" re-encode already-decoded strings.

Double encoding

A common pipeline bug: take a correct Unicode string, encode to UTF-8 bytes, then treat those bytes as a Latin-1 string and encode to UTF-8 again. You get é-style mess. Decode once to get back to single mojibake, then once more to recover the original — or better, find the extra encode and delete it.

Related

Emoji shows as □ / tofu · MySQL Incorrect string value / utf8mb4 · Invisible zero-width characters · Base64 is not encryption (emoji must be UTF-8 bytes first) · Unexpected token in JSON · Wirebook emoji lesson · Anvil text X-ray API · UnicodeDecodeError utf-8

Questions

What is mojibake?

Text that looks like gibberish because bytes were decoded with the wrong character encoding. UTF-8 read as Windows-1252 is the most common web case.

Why do I see é instead of é?

The UTF-8 bytes for é are C3 A9. Read as Windows-1252 they become à (C3) and © (A9). Re-decode correctly as UTF-8 to fix.

Why does my emoji look like 😀?

Same bug: a 4-byte UTF-8 emoji was decoded as single-byte Windows-1252/Latin-1. The bytes are usually still recoverable.

How do I fix mojibake in Python?

If UTF-8 was misread as Latin-1: bad.encode('latin-1').decode('utf-8'). Confirm on a sample first; double-encoded text needs two passes.

Is � mojibake?

No. � (U+FFFD) means the decoder already gave up on invalid bytes. Mojibake is wrong but still reversible characters like é or ðŸ.

Published .

Try it

Related guides

Newest guides