romanizer
Fast, CPU-only romanization of names and text from any script into plain Latin letters.
from romanizer import romanize
romanize("Володимир Зеленський") # 'Volodymyr Zelenskyi'
romanize("Александр Пушкин") # 'Aleksandr Pushkin'
romanize("Γιώργος Παπανδρέου") # 'Giorgos Papandreou'
romanize("محمد صلاح") # 'Mohamed Salah'
romanize("习近平") # 'Xi Jinping'
romanize("김정은") # 'Kim Jeongeun'
romanize("नरेन्द्र मोदी") # 'Narendra Modi'
romanize("Nguyễn Văn Đức") # 'Nguyen Van Duc'
romanize("Zeleнський in Київ") # 'Zelenskyi in Kyiv'
The output is always printable ASCII. No apostrophes standing in for soft signs, no backticks
for ayin, no stray spaces before commas, no emoji, no №, no €.
Why another one
Single-library solutions all fail somewhere:
unidecode/anyasciiwork character by character. Ukrainian comes out Russian-flavoured (Volodimir,Kiyiv), soft signs become', Kazakh gets@, Japanese kanji are read as Chinese (東京→Dong Jing), and CJK output has stray spaces.uromanis good but 50–100× slower than everything else (~0.5 ms per name, 1 s to load).- Dedicated libraries (
pypinyin,pykakasi,korean-romanizer,arabic-name-romanizer) are excellent for their one script and know nothing about the others or about mixed input.
romanizer splits input into same-script runs, guesses the language inside a script (Ukrainian vs
Russian vs Bulgarian, Japanese vs Chinese, Persian vs Arabic), routes each run to the best engine,
and passes everything through one final ASCII-only layer.
Installation
pip install romanizer # Cyrillic, Greek, Latin, Armenian, Georgian, Ethiopic
pip install "romanizer[arabic]" # + Arabic, Persian, Dari, Pashto, Urdu (arabic-name-romanizer, ONNX CPU)
pip install "romanizer[cjk]" # + Chinese, Japanese, Korean
pip install "romanizer[icu]" # + Indic and South-East Asian scripts (needs libicu)
pip install "romanizer[thai]" # + Thai (pythainlp, rule-based engine only)
pip install "romanizer[all]"
Nothing here touches a GPU. Missing extras never crash: the affected script falls back to a
readable approximation and is flagged as unsupported in analyze().
Language support
Status: ✅ supported · ⚠️ supported with caveats · ❌ not supported for now.
| Language / script | Detection | Engine | What we add on top | Example | Status |
|---|---|---|---|---|---|
| Ukrainian | Cyrillic with іїєґ, or ськ/ння/apostrophe before iotated vowel |
own table (KMU 2010 national system) | Ye/Yi/Yu/Ya word-initially, зг→zgh, soft sign and apostrophe dropped |
Volodymyr Zelenskyi, Kyiv, Yevhen, Yuliia |
✅ |
| Russian | Cyrillic default, or ыэъё |
own table (BGN/PCGN, simplified) | е→ye after vowels and word-initially, ё→yo (e after hushers), hard/soft signs dropped |
Aleksandr Pushkin, Yeltsin, Yuriy, Ilya |
✅ |
| Belarusian | ў |
own table | г→h, ў→u |
Alyaksandr Lukashenka, Uladzimir |
✅ |
| Bulgarian | ъ used as a vowel |
own table (2009 official system) | ъ→a, щ→sht, -ия→-ia |
Balgarski, Tarnovo; with lang="bg" also Hristo Botev, Sofia |
✅ |
| Serbian / Macedonian | ђћљњџј / ѓќѕ |
own table (Latin alphabet, diacritics stripped as in passports) | Đ→Dj, Ћ→C |
Djordje Petrovic, Cirilica, Gjorgji |
✅ |
| Kazakh, Kyrgyz, Mongolian, Tajik, other Cyrillic | әғқңөұүһ, ҳҷӣӯ |
own table (Kazakh 2021 Latin) + anyascii for rare letters |
қ→q, ғ→gh, ң→ng |
Nursultan Nazarbaev, Qazaqstan, Jumhuri |
✅ |
| Greek | Greek script | own table (ELOT 743) | word-initial μπ/ντ/γκ→b/d/g, medial mb/nt/ng, αυ/ευ→av/ev or af/ef by context |
Giorgos Papandreou, Bambis, Dinos, Evangelos, Nafplio, Pavlos |
✅ |
| Arabic | Arabic script | arabic-name-romanizer (dictionary of ~60k attested spellings, tiny ONNX model for the rest), batched |
script segmentation so Latin words, digits and punctuation never enter the model; single letters spelled (د. → D.); arabic_country hint |
Mohamed Salah, Abdulrahman, Abdelkader (DZ) |
✅ |
| Persian, Dari | Arabic script with پچژگ |
arabic-name-romanizer 0.2, Persian-script dictionary and model |
arabic_country="AF" picks Afghan spellings (Ahmad, Mohammad) over Iranian ones |
Golshifteh Farahani, Mohammad Reza Pahlavi, Hamid Karzai, Zalmay Khalilzad |
✅ 79% agreement with Wikidata |
| Pashto | Arabic script with ټډړږښڼېۍځڅ |
same Persian-script model, country defaults to AF |
ملالۍ → Malali |
⚠️ names work, Pashto-specific consonants (ښ, ډ) are approximate |
|
| Urdu | Arabic script with ٹڈڑںہے |
same Persian-script model, country defaults to PK |
Malala Yousafzai, Mohammad Ali Janah |
✅ 83% agreement with Wikidata | |
| Hebrew | Hebrew script | none | policy fallback gives a consonant skeleton, drop removes it, raise errors |
Vnymyn Ntnyhv |
❌ no vowels without a dictionary or model |
| Chinese | Han without kana | pypinyin |
in names mode a 2–3 character token starting with one of ~200 common surnames is split into surname + given name; ü→u |
Xi Jinping, Zhang Wei, Ouyang Xiu; 北京市 → Bei Jing Shi |
✅ |
| Japanese | kana present, or lang="ja" |
pykakasi (Hepburn) |
long vowels collapsed (toukyou→Tokyo), words capitalised |
Tokyo, Yamada Taro |
⚠️ kanji name readings are inherently ambiguous |
| Korean | Hangul | korean-romanizer (Revised Romanization) |
a 3-syllable token starting with one of the 40 most common surnames is split, with the customary spelling (Kim, Lee, Park); compound surnames handled |
Kim Jeongeun, Park Chanho, Seoul; 대통령 → Daetongryeong |
⚠️ consistent RR, but Koreans spell given names ad hoc (Jong-un, Young Shik): 26% agreement with Wikidata |
| Hindi, Marathi, Nepali | Devanagari | ICU Devanagari-Latin (ISO 15919) |
schwa deletion on the ISO form (short a only, so Sunita keeps its vowel and Ram loses it), c→ch, ś/ṣ→sh, siṁha→Singh, title case |
Sachin Tendulkar, Amitabh Bachchan, Priyanka Chopra, Kolkata |
✅ with [icu], ⚠️ without |
| Bengali | Bengali script | ICU Any-Latin |
diacritics stripped, title case | শেখ হাসিনা → Sekha Hasina, ফাতেমা → Phatema, আব্দুল করিম → Abdula Karima |
❌ scholarly ISO 15919 only: inherent vowel kept (Rahima), ph for f, s for sh, j for z; 7% agreement with Wikidata |
| Punjabi, Gujarati, Tamil, Telugu, Kannada, Malayalam, Sinhala | Indic scripts | ICU Any-Latin |
diacritics stripped, title case | Sri |
⚠️ ISO-style, no inherent-vowel handling; Tamil 15% agreement with Wikidata |
| Thai | Thai script | pythainlp RTGS engine |
title case | Somchai Prayut Chanocha |
⚠️ 60 µs/name; long compounds and ทร clusters come out wrong (Chinwatn for Chinnawat); 13% agreement with Wikidata |
| Lao, Khmer, Burmese, Tibetan, Mongolian script | respective scripts | ICU Any-Latin |
⚠️ readable, not standardised | ||
| Armenian, Georgian | respective scripts | anyascii |
Erevan, Armen Sargsyan, Tbilisi |
✅ | |
| Amharic, Tigrinya | Ethiopic | anyascii |
apostrophes removed | Adis Abeba, Aby Ahmed |
⚠️ |
| Vietnamese, Polish, German, Turkish, all Latin-script languages | Latin | NFKD + own map | Đ Ł ß Ø Þ Æ Œ ı and friends, which NFKD leaves alone |
Nguyen Van Duc, Lodz, Muller, Istanbul |
✅ |
| Symbols, emoji, typographic punctuation | final layer | “ ” – — … → ASCII, №→No, €→EUR, ١٢٣→123, emoji removed, whitespace collapsed |
✅ | ||
| Mixed-script input | per run | router | each run goes to its own engine, then one ASCII pass | Zeleнський in Київ → Zelenskyi in Kyiv |
✅ |
Measured agreement with Wikidata
scripts/evaluate_wikidata.py romanizes the native-language labels of about 1,400 people per
language from Wikidata and compares them with the English labels. Token recall is the share of
English-label words that appear verbatim in our output. English labels are not gold
transliterations: they follow anglicised habits (Vladimir for Володимир), nicknames and
drop patronymics, so these are floors for comparing languages, not accuracies.
| Language | Token recall | Char similarity | Reading |
|---|---|---|---|
| Serbian, Macedonian, Bulgarian, Georgian | 84–88% | 96% | as good as the labels allow |
| Urdu, Persian | 79–83% | 91–94% | good (Persian-script model of arabic-name-romanizer 0.2) |
| Greek, Arabic | 76–77% | 91–95% | good |
Chinese, Japanese (with lang="ja"), Armenian |
67–70% | 55–92% | good; Japanese order differs from Wikidata |
| Ukrainian, Kazakh, Hindi, Russian | 54–61% | 53–90% | mostly convention differences (Zelenskyi vs Zelenskyy) |
| Belarusian | 44% | 76% | Wikidata mixes Belarusian and Russian forms |
| Korean | 26% | 79% | see the table above |
| Tamil, Thai | 13–15% | 74–75% | ISO-style output, not everyday spelling |
| Bengali | 7% | 79% | not supported in practice |
| Hebrew | 0% | 59% | not supported |
Known ambiguities
- Han without kana is read as Chinese.
山田太郎alone givesShan Tian Tai Lang; passlang="ja"when you know the source is Japanese. - Cyrillic without language-specific letters is read as Russian.
Володимирhas no Ukrainian-only letters and becomesVolodimir; with a surname (Зеленський) orlang="uk"it becomesVolodymyr. Likewise Bulgarian is only recognised by a vocalicъ(Търново), soХристо Ботевneedslang="bg"to becomeHristo Botevinstead ofKhristo Botev. When you know the language, pass it. - Names mode splits Chinese and Korean tokens that start with a common surname.
田中(Tanaka) is a Chinese surname too, so withoutlang="ja"it becomesTian Zhong;王府becomesWang Fu. Usemode="text"for running text. - Serbian written in Latin letters loses
Đ→D(Đorđe→Dorde) while the Cyrillic form givesDjordje:Đis also Vietnamese, whereDis right, and nothing in the text says which. - Dari and Iranian Persian share an alphabet. Both are detected as
fa; passarabic_country="AF"for Afghan data to preferAhmad Shah MassoudoverAhmed Shah Masoud. - Hindi schwa deletion is a heuristic. It gets
Tendulkar,Kolkata,Arvindright andDraupadiwrong (Draupdi). Useoverridesfor the names that matter.
API
from romanizer import Romanizer, romanize, romanize_many, analyze
romanize("Київ") # auto-detect
romanize("Володимир", lang="uk") # language hint
romanize_many(["محمد", "أحمد", "Київ"]) # batches Arabic runs into one model call (20× faster)
r = Romanizer(
mode="names", # or "text": no surname splitting, no title-casing
unsupported="fallback", # or "raise" / "drop" for scripts with no engine (Hebrew)
arabic_country="EG", # regional spelling preference for Arabic names
overrides={ # exact spellings that win over every engine, whole input or any token
"Зеленський": "Zelenskyy",
"محمد صلاح": "Mo Salah",
},
)
r.romanize("Володимир Зеленський") # 'Volodymyr Zelenskyy'
a = analyze("Zeleнський in Київ")
a.output # 'Zelenskyi in Kyiv'
a.segments[1].script # 'Cyrillic'
a.segments[1].lang # 'uk'
a.segments[1].engine # 'table-cyrillic'
a.segments[1].supported # True
Command line:
romanize "Володимир Зеленський" "محمد صلاح"
romanize --lang uk < names.txt > names_latin.txt
romanize --overrides known_spellings.tsv < names.txt # 'source<TAB>latin' per line
romanize --analyze "Zeleнський in Київ"
Speed
Single thread, Apple M-series, warm engines, microseconds per string:
| Input | romanize() |
romanize_many() |
|---|---|---|
| ASCII (fast path) | 0.9 | 0.6 |
| Latin with diacritics | 3 | 3 |
| Ukrainian / Russian | 7–9 | 7–8 |
| Greek | 10 | 9 |
| Chinese / Japanese / Korean | 6–13 | 6–12 |
| Hindi (ICU) | 8 | 7 |
Mixed Zeleнський in Київ |
7 | 6 |
| Thai | 58 | 58 |
| Arabic, name in dictionary | 400 | 80 |
| Arabic, name unknown (ONNX model) | 1430 | 40 |
Ten million mixed records take minutes on one core. Always use romanize_many for Arabic.
Output guarantees
- Only printable ASCII leaves the library. Every character is either mapped by a table, decomposed
by NFKD with its marks removed, mapped by an explicit table for letters NFKD cannot decompose,
or, as a last resort, transliterated by
anyascii. - Backticks, carets and tildes never survive: they only ever come from transliteration schemes.
- Whitespace is collapsed to single spaces and trimmed.
- Casing of cased scripts (Cyrillic, Greek, Latin) is preserved:
ЩЕРБАК→SHCHERBAK,Щербак→Shcherbak. Caseless scripts are title-cased in names mode.
Contributing
uv sync
uv run pytest # 100% line and branch coverage is enforced
uv run ruff check .
uv run mypy # strict
See CONTRIBUTING.md for the layout, the checklist for adding a language, and the pull request expectations. Security reports: SECURITY.md. Changes: CHANGELOG.md.
License
MIT for this package (see LICENSE; dependency licenses in THIRD_PARTY_NOTICES.md). Optional engines keep their own licenses: anyascii (ISC), pypinyin (MIT),
PyICU (MIT), pythainlp (Apache-2.0), arabic-name-romanizer (MIT), and in the [cjk] extra
pykakasi and korean-romanizer, which are GPL-3.0-or-later. If GPL is a problem for your
deployment, skip the [cjk] extra: Chinese, Japanese and Korean then fall back to anyascii.
Metadata
Release files for romanizer 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| romanizer-0.1.0.tar.gz | 38.7 kB | With provenance | Details for romanizer-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| romanizer-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for romanizer-0.1.0-py3-none-any.whl |
Total release size: 74.7 kB
Release files / romanizer-0.1.0.tar.gz
| Download URL | romanizer-0.1.0.tar.gz |
|---|---|
| Size | 38.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4cdde8c96a4e7ff1c28b5dd43b196c6d313f7c7a2036148e351b0dc529760376
|
|
BLAKE2b-256 checksum How to use checksums |
19d3cf44c54c3299f46495a3097d2b0e038a503e896045648486aea4ce44a759
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency logRelease files / romanizer-0.1.0-py3-none-any.whl
| Download URL | romanizer-0.1.0-py3-none-any.whl |
|---|---|
| Size | 36.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dd497df6a35b4170813944ca2e393032f3c10665df9f1d8ca7889991eb3156b0
|
|
BLAKE2b-256 checksum How to use checksums |
8c242b034a0f040a87254a7813d3be48645f252f2e0f994499cd22cd90cd150d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency logSecurity for romanizer 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for romanizer-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.