Skip to main content

habesha-names

Ethiopian/Eritrean name intelligence for Python: Fidel handling, transliteration, spelling variants, patronymic-aware parsing, and fuzzy matching.

PyPI version Python versions License

The problem

Habesha names are patronymic: a full name is a given name followed by the father's given name (and sometimes the grandfather's) — there is no family surname, so "first/last name" fields are semantically wrong. One name written in Fidel has many legitimate Latin spellings (ተስፋዬ → Tesfaye, Tesfai, Tesfay, ...), because there is no standard romanization and Fidel itself has homophone letters (ሀ/ሐ/ኀ, ሰ/ሠ, ጸ/ፀ) whose choice varies by writer. Compound given names split, join, and abbreviate (Gebremedhin / Gebre Medhin / G/Medhin), and token order swaps freely across documents. Western name-matching logic applied to this reality produces duplicate customer records, failed KYC lookups, and rejected remittance matches for the same real person.

Install

pip install habesha-names

Zero runtime dependencies (stdlib only). Fully typed — ships py.typed.

60-second tour

Every example below is a doctest and runs in CI against the real package — the outputs shown are actual outputs.

Transliterate

>>> from habesha_names import transliterate
>>> transliterate("ተስፋዬ")
'Tesfaye'

Homophone guarantee: Fidel spellings that sound identical transliterate identically, whichever homophone letters the writer chose.

>>> transliterate("ፀሐይ") == transliterate("ጸሀይ") == "Tsehay"
True

Parse

>>> from habesha_names import parse
>>> p = parse("Ato Abebe Bikila")
>>> (p.title, p.given, p.patronym)
('Ato', 'Abebe', 'Bikila')

Fidel input parses the same way (title recognized, roles assigned):

>>> pf = parse("ወይዘሮ ጸሐይ ገብረመድህን")
>>> (pf.title, pf.given, pf.patronym)
('Weizero', 'ጸሀይ', 'ገብረመድህን')

By default the second token is a patronym (has_surname='no'). For diaspora data, where the second token may be an inherited surname, the parser records uncertainty instead of deciding:

>>> parse("Abebe Bikila", assume_diaspora=True).has_surname
'unknown'

Match

>>> from habesha_names import match
>>> result = match("Tesfaye", "Tesfai")
>>> round(result.score, 2)
0.91
>>> [(pair.token_a, pair.token_b, pair.method) for pair in result.pairs]
[('Tesfaye', 'Tesfai', 'jaro_winkler')]
>>> result.swapped
False

match returns a MatchResult (usable directly in comparisons) carrying the score, the aligned token pairs with the method that matched each pair, a swapped flag, and human-readable notes. Unrelated names score low:

>>> match("Abebe", "Girma").score
0.0

Variants

The Latin spellings of a name your database is likely to contain:

>>> from habesha_names import variants
>>> variants("Haile", n=5)
['Haile', 'Hayle', 'Hailie', 'Khaile', 'Haille']

Phonetic key

Spelling variants collapse to one blocking/indexing key:

>>> from habesha_names import phonetic_key
>>> phonetic_key("Tesfaye") == phonetic_key("Tesfai")
True

Guess gender

Lexicon-backed and honest about its limits: only the given name is used (the patronym is the father's given name — it says nothing about the bearer), and a name the lexicon does not know returns 'unknown' rather than a guess from spelling shape:

>>> from habesha_names import guess_gender
>>> guess = guess_gender("Ato Abebe Bikila")
>>> (guess.gender, guess.confidence)
('m', 0.9)
>>> guess_gender("ፀሐይ ገብረመድህን").gender
'f'
>>> guess_gender("Maram").gender
'unknown'

To fidel (reverse transliteration)

Latin spellings back to Ethiopic script. Lexicon-first: a recognized spelling returns the entry's stored conventional fidel — including the homophone letters (ፀ, ሐ …) that rules alone could never pick, because the practical romanization is lossy. Unrecognized spellings are inverted by rule onto post-collapse fidel: a phonetic spelling that transliterates back consistently, not necessarily the conventional orthography.

>>> from habesha_names import to_fidel
>>> to_fidel("Tsehay")                       # recognized: conventional fidel
'ፀሐይ'
>>> to_fidel("Tsehai") == to_fidel("Tsehay")  # recorded variants too
True
>>> transliterate(to_fidel("Yohannis"))       # unrecognized: rule path
'Yohanis'

Normalize and script detection

>>> from habesha_names import normalize, is_ethiopic
>>> normalize("ፀሐይ")                    # Fidel homophones collapse: ፀ→ጸ, ሐ→ሀ
'ጸሀይ'
>>> is_ethiopic("ተስፋዬ")
True

KYC, AML, and remittance matching

A remittance operator keys in the sender as spelled on the form; the KYC record was created years earlier from a different document, with the tokens in the other order and the patronym abbreviated. Same person, zero exact overlap between the strings:

>>> from habesha_names import match
>>> result = match("Tesfai G/Medhin", "Gebremedhin Tesfaye")
>>> round(result.score, 2)
0.94
>>> result.swapped
True
>>> for note in result.notes:
...     print(note)
a: abbreviation 'G/Medhin' expanded with top candidate 'Gebre' (candidates: Gebre (0.8), Girma (0.2))
b: given 'Gebremedhin' is a joined compound (Gebre + Medhin)
tokens aligned across roles (order swap tolerated)

Scores are calibrated to three bands:

score reading
≥ 0.85 likely the same person
0.60 – 0.85 review zone — route to an analyst
≤ 0.60 likely different people

The middle band is intentional: records like "Tesfaye Girma" vs "Tesfahun Girma" (different given name, shared patronym — plausibly siblings) land there by design, because a shared patronym is exactly the near-match a human should see.

What this library does not do: it performs no identity verification and no sanctions or watchlist screening, and it makes no claim that two records belong to the same person. It is a deterministic name-processing primitive — parsing, normalization, and explainable similarity scoring — for use inside such systems, with every score accompanied by the evidence (pairs, notes) an auditor would ask for.

Linguistic decisions

The practical transliteration scheme's defaults are native-speaker reviewed: every consonant and vowel choice was reviewed and signed off by Robel Shemeles (task-3b, 2026-07-14), with each choice recorded in the project's internal decisions log. Homophone collapse happens in normalize, which transliterate applies unconditionally, so the transliteration table deliberately has no rows for collapsed letter series. Changing any table cell is a linguistic decision, not a refactor — the contract and the reasoning are documented in the translit/schemes.py docstring.

Data provenance

The bundled lexicons (given names, titles, compound elements) are common-name reference data — seeded during development, then reviewed entry-by-entry by a native speaker — not scraped data and not records of any individual. As of 0.2.0 every bundled entry is reviewed and ships "verified": true — 380 given names, 12 titles, and the compound prefixes and second elements. Per-file origins, generation sources, and verification status are documented in DATA_PROVENANCE.md.

Versioning and stability

The public API — parse, match, variants, transliterate, normalize, phonetic_key, is_ethiopic, and (new in the 0.2 line) guess_gender and to_fidel — is stable within a minor series; everything else is internal. Transliteration outputs are part of the contract: changing any transliteration table cell changes outputs and therefore bumps the minor version, never a patch release.

Known limitations

All three limitations carried into 0.1.0 are resolved in the 0.2 line: Bekele ↔ Bikila no longer collide (0.90 → 0.40, via the richer phonetic key), spaced-compound spelling rewrites like "Gebrie Medhin" vs "Gebremedhin" now align through a phonetic-key fallback in the parser, and the golden corpus is no longer purely mechanical — it carries a native-speaker-curated pair set, against which the matcher's variant weight was calibrated.

What is still imperfect:

  • The golden corpus records 7 known_fail pairs. These are documented engine limits, not silent bugs: the test suite asserts each one keeps failing until the engine or the data changes. Three come from the generated corpus — two names whose conventional Latin spelling is far from the raw transliteration, so the Fidel spelling and the Latin canonical score below the same-person gate (ፍሬሕይወት/Firehiwot, አፈወርቅ/Afework), plus Ali ↔ Ayele, two distinct names that legitimately share a phonetic key and so score 0.90. Four come from the curated set and mark two structural limits: a name shortened by a skipped generation ("Abebe Kebede Tadesse" vs "Abebe Tadesse") lands in the review zone (0.81) rather than reading as a non-match, and a pair where one role matches and the other differs ("Dawit Alemu" vs "Dawit Girma", 0.69) is scored the same way whether the two are siblings or strangers — which is the review-zone behaviour the score bands promise, so it cannot be tightened without contradicting them.
  • Lexicon coverage. 380 given-name entries cover roughly 62% of name occurrences in the development corpus; the remainder gets rule-based handling only, which is weaker for names whose conventional spelling diverges from their transliteration.
  • Final-vowel-class pairs. Same-name spellings that differ in final-vowel class and have no lexicon entry rest on damped edit similarity alone — deliberately, since that ending often marks a genuinely different name (Haile/Hailu, Berhane/Berhanu).
  • Role tuning. The matcher's role weights, swap penalty, and missing-role scale remain accepted-as-shipped heuristics. The curated corpus is still small enough that no value in their plausible ranges changes a verdict, so calibrating them would be fitting to noise. VARIANT_WEIGHT is the one constant the curated pairs did move.

Contributing

Issues are welcome, especially edge-case names: spellings that fail to match, romanizations we do not generate, parsing mistakes on real name structures. Note that linguistic decisions — transliteration table cells, lexicon entries, variant rules — require native-speaker sign-off (Robel) before they land; new lexicon entries ship "verified": false until reviewed, which is why the shipped lexicon is entirely verified: true — the flag tracks the queue, not the release.

Development

python -m venv .venv
.venv\Scripts\activate.bat
pip install -e .[dev]
check.bat        # pytest -q && ruff check . && mypy src --strict

See ARCHITECTURE.md for the design and CHANGELOG.md for release history.

License

MIT — see LICENSE.

Metadata

Release files for habesha-names 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for habesha-names 0.2.0
File Size Uploaded Provenance Actions
habesha_names-0.2.0.tar.gz 163.7 kB With provenance Details for habesha_names-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for habesha-names 0.2.0
File Interpreter ABI Platform Actions
habesha_names-0.2.0-py3-none-any.whl Python 3 none any Details for habesha_names-0.2.0-py3-none-any.whl

Total release size: 240.9 kB

Release files / habesha_names-0.2.0.tar.gz

Download URL habesha_names-0.2.0.tar.gz
Size 163.7 kB
Tags Source
SHA-256 checksum
How to use checksums
67dd12eeb26960fcaea2fd83de35d8186f8834f1dba181e49eb56a8bdd101c7b
BLAKE2b-256 checksum
How to use checksums
2526a06e4341a83db077c7ccbedb7366d798390f1973315edb0783ceec870867
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.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 Jul 21, 2026.

Transparency log

Release files / habesha_names-0.2.0-py3-none-any.whl

Download URL habesha_names-0.2.0-py3-none-any.whl
Size 77.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
98c9c7e2b1845111278f2e5db03a92496715f8cff49f62e0f646573856f74026
BLAKE2b-256 checksum
How to use checksums
551c35b68a59281af9f067eb76af6d652834b657b5027ba3eebe0b63ab64674d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.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 Jul 21, 2026.

Transparency log

Security for habesha-names 0.2.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.

Select from 2 files

Attestation for habesha_names-0.2.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 Jul 21, 2026.

Transparency log

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

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page