Lookalike characters: when two symbols aren’t the same symbol

Igor R.

September 23, 2026

Short answer: Unicode contains many characters that render almost identically but are completely different — Cyrillic а and Latin a, the micro sign µ and Greek μ, a hyphen and a minus. They break search, sorting, code and security checks, and you cannot tell them apart by looking. Check the code point instead.

The fastest way to settle any of these: paste the character into the Symbol Identifier, which names the exact code point you’re holding.

Why Unicode is full of them

It looks like a design flaw and it isn’t. Three deliberate decisions produced almost all of it.

Different scripts, same shapes. Latin, Greek and Cyrillic share ancestry, so a Cyrillic а looks like a Latin a because it is the same letterform, inherited independently. Unicode can’t merge them: they sort differently, uppercase differently, and belong to different alphabets. A Russian word spelled with Latin letters would be wrong even though it looked right.

Different meanings, same glyph. The Greek capital sigma Σ is a letter; the summation operator ∑ is mathematical notation. Same shape, different job — and mathematics software needs to tell them apart.

Round-trip compatibility. When older encodings were absorbed into Unicode, their characters came along even where a duplicate already existed. That’s why the micro sign µ exists alongside Greek μ, and the ångström sign alongside Å. Background in what Unicode is.

The index

Letters from other scripts

Looks likeActually isCode point
aCyrillic small letter aU+0430
oGreek small letter omicronU+03BF
ICyrillic capital letter byelorussian-ukrainian iU+0406
gLatin small letter script gU+0261

These are the dangerous ones, because whole alphabets of them exist. Typing in Russian and the Greek alphabet cover the scripts they come from.

Same shape, different meaning

CharacterOfficial nameCode pointConfused with
µMicro signU+00B5μ Greek mu, U+03BC
∑N-ary summationU+2211Σ Greek sigma, U+03A3
ςGreek small letter final sigmaU+03C2σ sigma, U+03C3
♾Permanent paper signU+267E∞ infinity, U+221E
♯Music sharp signU+266F# number sign, U+0023
∼Tilde operatorU+223C~ tilde, U+007E
∕Division slashU+2215/ solidus, U+002F
꞉Modifier letter colonU+A789: colon, U+003A
ːModifier letter triangular colonU+02D0: colon, U+003A

The µ/μ pair is the one that appears most in real documents — see the mu symbol and statistics symbols. The Σ/∑ pair is covered on the sigma symbol, and ♾ on the infinity symbol.

Punctuation that isn’t the punctuation you meant

CharacterNameCode pointMistaken for
−Minus signU+2212– hyphen-minus, U+002D
′PrimeU+2032‘ apostrophe, U+0027
;Greek question markU+037E; semicolon, U+003B

The dash family is covered in the em dash, primes in the prime symbol, the tilde in the tilde symbol, and quote substitution in straight vs curly quotes.

Normalisation will not save you

This is the assumption that costs people the most time. Unicode normalisation merges characters it considers canonically equivalent — genuinely the same character written two ways. Lookalikes are not that. They’re different characters that happen to share a shape.

Of the sixteen pairs on this page, NFKC merges exactly two: the micro sign becomes Greek mu, and the Greek question mark becomes a semicolon. Both are compatibility duplicates rather than true lookalikes. The other fourteen survive every normalisation form unchanged — Cyrillic а stays Cyrillic а forever.

So normalising your input is necessary and nowhere near sufficient. It’s the wrong tool for this job.

Where it actually bites

Domain names. This is the serious one. Registrars accept non-Latin scripts, so a domain using Cyrillic а renders identically to the Latin original. Converted to its underlying form, аpple.com is xn--pple-43d.com — a completely different address that no amount of careful reading would reveal. That’s the homograph attack, and browsers now defend against it by displaying the punycode form when a domain mixes scripts suspiciously. When your browser shows you xn-- gibberish, it isn’t broken; it’s warning you.

A homograph domain and its punycode form

Code. The classic prank is replacing a semicolon with the Greek question mark U+037E, which renders identically. The code looks perfect and the parser rejects it, or worse, parses it as something unintended. Cyrillic letters in identifiers produce two variables that look like one.

Search and find-and-replace. A document mixing µ and μ, or ɡ and g, silently returns half the matches. Nothing errors; the result is just quietly incomplete.

Spreadsheets. A minus sign U+2212 pasted from a typeset document isn’t a hyphen, so the cell becomes text rather than a number, and your sum is wrong rather than broken.

Usernames and identity. Two accounts whose names render identically is an impersonation vector, which is why serious systems restrict identifiers to a single script rather than trying to blacklist characters.

Filenames. The reverse case: lookalikes are used deliberately to get around blocked characters, as in invalid filename characters. Useful, and it has its own costs.

How to detect them

Check the code point, not the shape. Paste into the Symbol Identifier. This is the only method that always works.

Watch for mixed scripts. A single word containing both Latin and Cyrillic letters is almost never legitimate. That one rule catches the majority of malicious cases, and it’s cheap to implement — bucket each character by script and flag any field that mixes them.

Use the confusables data. Unicode publishes a machine-readable list of characters that look alike, along with a “skeleton” procedure: map every character to a canonical representative, then compare skeletons. Two strings with the same skeleton look alike, whichever characters they actually contain. This is what registrars and browsers use, and it’s the right answer for any system doing identity checks.

Don’t rely on people. Visual review is exactly what lookalikes defeat. If detection matters, it has to be automatic.

In code, a script check is usually a few lines:

python

import unicodedata as u
def scripts(s):
    return {u.name(c).split()[0] for c in s if c.isalpha()}

scripts("аpple")   # {'CYRILLIC', 'LATIN'} — mixed, worth flagging
scripts("apple")   # {'LATIN'}

That’s a rough heuristic rather than a substitute for the official confusables data, but it catches the common case immediately.

Related problems that look similar

  • Invisible characters — zero-width spaces and non-breaking spaces, which don’t look like anything at all. See invisible characters.
  • Characters that won’t render — empty boxes are a font gap, not a lookalike. See symbols showing as boxes.

Accessibility

Here, assistive technology is better off than you are. Screen readers announce characters by Unicode name, so Cyrillic а is read as a Cyrillic letter while Latin a is read as a Latin one — a distinction the screen never shows.

That cuts both ways. A heading padded with lookalikes reads as nonsense to a screen-reader user even though it looks fine, which is a good reason not to use them decoratively in text anyone else has to read. How screen readers pronounce symbols covers the pattern.

Troubleshooting

ProblemFix
Search misses obvious matchesMixed lookalikes. Check the code points and replace consistently.
A number won’t sum in a spreadsheetProbably a minus sign U+2212 rather than a hyphen. Replace it.
Code looks correct and won’t compileCheck for a Greek question mark U+037E posing as a semicolon, or Cyrillic letters in identifiers.
A browser shows xn-- in the address barWorking as intended — the domain mixes scripts and is being shown in punycode.
Two list entries look identicalThey contain different characters. Compare code points, not appearance.
Normalising didn’t merge themExpected. Normalisation only merges canonically equivalent characters.
Spell-check flags a correctly spelled wordA letter from another script is hiding in it.

Recap

SituationDo this
Unsure what a character isSymbol Identifier — check the code point
Validating usernames or domainsRestrict to one script; compare confusable skeletons
Cleaning a documentFind and replace by code point, not by eye
Writing mathematicsThe operator, not the Greek letter — ∑ not Σ
Seeing xn-- in a URLRead it carefully; that’s the warning
Relying on normalisationDon’t — it merges almost none of these

Frequently asked questions

What are lookalike characters?

Different Unicode characters that render almost identically — Cyrillic а and Latin a, the micro sign and Greek mu, a hyphen and a minus sign. They’re separate characters with separate code points, so they don’t match each other in search, sorting or comparison.

Why does Unicode include duplicates?

Three reasons: different writing systems genuinely share letterforms, some shapes have different meanings that software must distinguish, and characters from older encodings were preserved when those encodings were absorbed so that converted text could round-trip.

Does Unicode normalization fix lookalikes?

Almost never. Normalisation merges characters Unicode considers canonically equivalent. Lookalikes are different characters that share a shape, so they survive every normalisation form — only a couple of compatibility duplicates get merged.

What is a homograph attack?

Registering a domain that uses characters from another script to render identically to a real one. Browsers defend against it by displaying the punycode form of the address when the scripts look suspicious.

How can I tell which character I’ve got?

Check its code point with a Unicode identifier tool. Visual inspection is exactly what lookalikes are designed to defeat, so no amount of careful reading will settle it.

How do I stop lookalikes getting into my system?

Restrict identifier fields to a single script, flag any value that mixes scripts, and compare values using Unicode’s confusables data rather than raw string equality.

Written by Igor R. Tested on Windows 11, macOS and Linux, in Chrome, Firefox and Python. Last updated September 2026. Symbol code points follow the Unicode Standard.

Leave a Comment