shevchenko-py
Ukrainian grammatical-case declension of personal names, military ranks, and military appointments. A minimal, dependency-free Python port of shevchenko-js and shevchenko-ext-military.
- Zero runtime dependencies (pure Python ≥ 3.9, stdlib only)
- Tiny: ~44 KB wheel, ~170 KB installed (39 KB code + 130 KB data), ~9 ms import
- All 7 Ukrainian grammatical cases
- Inverse declension: recover the nominative from any case
(
to_nominative) — a capability the JS original does not have - Gender auto-detection from the patronymic or given name
- The upstream ML surname classifier reimplemented as ~30 lines of pure Python
Install
pip install shevchenko-py
The distribution is named shevchenko-py; the import name is shevchenko.
Don't install this alongside the unrelated shevchenko
PyPI package (a different port, anthroponyms only) — both provide the
shevchenko module.
Quick start
import shevchenko
shevchenko.in_genitive(
given_name="Тарас",
patronymic_name="Григорович",
family_name="Шевченко",
military_rank="старший солдат",
military_appointment="заступник командира взводу",
)
# {'given_name': 'Тараса', 'patronymic_name': 'Григоровича',
# 'family_name': 'Шевченка', 'military_rank': 'старшого солдата',
# 'military_appointment': 'заступника командира взводу'}
API reference
Everything is importable from the top-level shevchenko package. Anything
prefixed with _ (modules _inflector, _names, …) is internal and not a
stable interface.
Declension functions
shevchenko.in_nominative(input=None, **kwargs) -> dict # називний (хто? що?)
shevchenko.in_genitive(input=None, **kwargs) -> dict # родовий (кого? чого?)
shevchenko.in_dative(input=None, **kwargs) -> dict # давальний (кому? чому?)
shevchenko.in_accusative(input=None, **kwargs) -> dict # знахідний (кого? що?)
shevchenko.in_ablative(input=None, **kwargs) -> dict # орудний (ким? чим?)
shevchenko.in_locative(input=None, **kwargs) -> dict # місцевий (на кому? на чому?)
shevchenko.in_vocative(input=None, **kwargs) -> dict # кличний (звертання)
shevchenko.in_case(case, input=None, **kwargs) -> dict
All seven are thin wrappers around in_case with the case fixed. Upstream calls
the instrumental case (орудний) "ablative"; this port keeps that naming.
in_case(case, ...) takes the case as a string — one of shevchenko.CASES
("nominative", "genitive", "dative", "accusative", "ablative",
"locative", "vocative") — useful when the case is chosen at runtime:
for case in shevchenko.CASES:
print(shevchenko.in_case(case, gender="masculine", given_name="Тарас"))
Input
Fields may be passed as a dict, as keyword arguments, or both (keyword arguments override the dict):
shevchenko.in_vocative({"given_name": "Тарас"}, family_name="Шевченко")
# {'given_name': 'Тарасе', 'family_name': 'Шевченку'}
| Field | Meaning | Example |
|---|---|---|
given_name |
First name (ім'я) | "Тарас" |
patronymic_name |
Patronymic (по батькові) | "Григорович" |
family_name |
Surname (прізвище) | "Шевченко" |
military_rank |
Rank phrase (військове звання) | "старший солдат" |
military_appointment |
Appointment/position phrase (посада) | "заступник командира взводу" |
gender |
"masculine" / "feminine"; optional, see below |
"masculine" |
All fields are optional strings, but at least one non-gender field is
required. Unknown keys raise InputValidationError (so givenName= typos are
caught, not silently ignored). Values are NFC-normalized before matching.
The gender parameter
gender applies to the three name fields. If omitted, it is auto-detected from
the patronymic (preferred) or the given name — same logic as
detect_gender. Two consequences:
- Input with a patronymic or a recognizable given name never needs
gender. - A lone
family_nameusually can't be sexed — passgenderexplicitly orInputValidationErroris raised.
Military fields don't need gender at all: each word inside a rank or
appointment phrase carries its own grammatical gender (медична сестра
declines as feminine regardless of the person).
Return value
A dict containing only the fields you passed in (never gender), each
value inflected in the requested case. Field order is fixed: given_name,
patronymic_name, family_name, military_rank, military_appointment.
Behavior notes
-
Letter case is preserved:
ШЕВЧЕНКО→ШЕВЧЕНКА,Шевченко→Шевченка. -
Hyphenated names decline part by part (
Нечуй-Левицький→Нечуя-Левицького); monosyllabic non-final surname parts stay frozen (Драй-Хмара→Драй-Хмари). -
Unknown words in military phrases — proper names, abbreviations, qualifiers already in genitive — pass through unchanged:
shevchenko.in_ablative(military_appointment='оператор БПЛА "Фурія"') # {'military_appointment': 'оператором БПЛА "Фурія"'}
-
Unknown names that no declension rule matches are returned unchanged rather than guessed at.
Errors
InputValidationError (subclass of ValueError) is raised when:
caseis not one ofCASES(forin_case),- no field besides
genderis provided, - a field value is not a string,
- an unknown parameter is passed,
genderis neither"masculine","feminine", nor omitted,genderis omitted for name fields and cannot be auto-detected.
detect_gender
shevchenko.detect_gender(input=None, **kwargs) -> str | None
Detects the grammatical gender from patronymic_name (checked first) or
given_name endings. Accepts family_name too, but a surname alone is never
used for detection. Returns "masculine", "feminine", or None when
undetectable — unlike the declension functions it does not raise on ambiguity.
shevchenko.detect_gender(patronymic_name="Григорович") # 'masculine'
shevchenko.detect_gender(given_name="Оксана") # 'feminine'
shevchenko.detect_gender(family_name="Шевченко") # None
Input rules are the same as for declension: dict and/or kwargs, at least one
field, strings only, unknown keys raise InputValidationError.
to_nominative
shevchenko.to_nominative(input=None, case=None, **kwargs) -> dict
The inverse of the declension functions: recovers nominative forms from fields
written in any grammatical case. Same five fields as in_case, dict
and/or kwargs.
shevchenko.to_nominative(
given_name="Тараса", patronymic_name="Григоровича", family_name="Шевченка",
military_rank="старшого солдата",
military_appointment="заступника командира взводу",
)
# {'given_name': 'Тарас', 'patronymic_name': 'Григорович',
# 'family_name': 'Шевченко', 'military_rank': 'старший солдат',
# 'military_appointment': 'заступник командира взводу',
# 'gender': 'masculine', 'alternatives': {...}}
How it works: candidates are generated by mechanically inverting the forward declension rules, then each is verified by declining it forward — every returned value round-trips exactly. Since distinct nominatives can share an oblique form (Сірка ← Сірк or Сірко), the result carries deterministic ranking metadata:
"gender"— resolved from the patronymic suffix (works in every case), an explicitgender=argument, or both-gender agreement; raisesInputValidationErrorwhen unresolvable."alternatives"— per-field list of other valid readings, present only for genuinely ambiguous fields. The true nominative is always either the returned value or in this list (100% recall on the full upstream corpora).
Behavior notes:
- All name fields are assumed to share one case; unambiguous fields
(patronymics invert deterministically) pin the case for ambiguous ones.
Pass
case=when the source case is known — it narrows ambiguity further. - Mixed input is handled:
to_nominative(family_name="Іванов", given_name="Івана")returns both in nominative. - Already-nominative input comes back unchanged.
- In military phrases only the leading adjectives and head noun invert; genitive attributes after the head stay put («заступника командира взводу» → «заступник командира взводу»).
- Vocative is excluded from the default search (it never occurs in documents
and collides with nominative -о forms); pass
case="vocative"explicitly.
Measured on the upstream corpora: patronymics and military ranks invert at
100% top-1; given names ≈99%, family names ≈93% top-1 (the remaining gap is
orthography-codified ambiguity — Верес/Вересов, Сашко/Сашок, Тичин/Тичина —
reported via alternatives); military appointments 96.7% top-1 / 99.3% with
alternatives. When neither the patronymic nor an explicit gender decides,
the given name is inverted against the bundled dictionary of 1,144 real names
(«Івана» → Іван → masculine) before an error is raised.
Linguistic grounding, sources, and design details:
docs/inverse-declension.md.
Constants
| Constant | Value |
|---|---|
shevchenko.CASES |
("nominative", "genitive", "dative", "accusative", "ablative", "locative", "vocative") |
shevchenko.NOMINATIVE … shevchenko.VOCATIVE |
The individual case strings |
shevchenko.GENDERS |
("masculine", "feminine") |
shevchenko.MASCULINE, shevchenko.FEMININE |
The individual gender strings |
shevchenko.__version__ |
Package version string |
The constants are plain strings/tuples, so gender="feminine" and
gender=shevchenko.FEMININE are interchangeable.
InputValidationError
class InputValidationError(ValueError)
Raised by all public functions on invalid input; see Errors.
Performance
Measured on Python 3.14 with python benchmark.py (CPython, Windows, single
thread):
| Operation | Speed |
|---|---|
| Cold import (data load; regexes compile lazily) | ~8 ms, once |
| Full person (3 names + rank + appointment), 1 case | ~140 µs (≈7,000/s) |
| Single name field | ~20–24 µs (≈42–50,000/s) |
| Military rank / appointment phrase | ~40–50 µs (≈21–25,000/s) |
detect_gender |
~1 µs (≈700,000/s) |
| Ambiguous surname, first time (pure-Python RNN) | ~0.9 ms |
| Ambiguous surname, repeated (memoized) | ~10 µs |
| Real-corpus throughput (name triple, one case) | ≈13,000/s |
to_nominative: full person, first time |
~3.3 ms |
to_nominative: full person, repeated names |
~0.7 ms (~0.3 ms with case=) |
to_nominative: military rank / appointment, repeated |
~70–100 µs |
to_nominative: patronymic only |
~85 µs |
The only slow path is the first classification of an ambiguous surname (the neural network runs in pure Python); results are memoized, so each unique surname pays it once per process.
Fidelity
The suite (2,410 tests) covers:
- Forward declension against the complete upstream corpora: every anthroponym test case, every military rank and appointment in all 7 cases, and the 1,144-name gender detection dataset. Also differentially verified against the live JS library on 3,990 additional word/case combinations — zero mismatches.
- Inverse declension against the same corpora, both as aggregate accuracy
floors and as the round-trip property
in_case(case, to_nominative(x)) == x. The recall invariant — the true nominative is never absent from result + alternatives — is asserted over every corpus form. - All upstream functional unit tests (letter case, syllables, alphabet encoding, classifier input encoding and decode threshold) are ported; upstream's input-validation suite is not, since this port's API surface deliberately differs.
- Data integrity: every regex in the shipped data must compile under
Python's
re(the fail-fast guard for upstream rule re-syncs).
How it works
Declension is rule-based: 99 regex rules from upstream (shevchenko/data/)
keyed by gender, word class, and usage, applied by priority. Surnames whose word
class is ambiguous (e.g. feminine -а/-я, masculine -ий/-ой/-их) are
classified noun-vs-adjective by the upstream neural network — an
Embedding→SimpleRNN(16)→Dense model small enough (4 KB) to run in pure Python
(shevchenko/_classifier.py). Two upstream regexes use variable-width
lookbehinds unsupported by Python's re and are rewritten to equivalent forms at
load time (shevchenko/_inflector.py).
The runtime data is regenerated from the upstream sources by
tools/build_data.py (dev-only): it strips documentation-only fields, drops
empty case entries, minifies, and verifies every pattern compiles under
Python's re. Data loading uses plain file reads instead of
importlib.resources, whose import chain alone costs ~25 ms.
Differences from shevchenko-js
| shevchenko-js | shevchenko-py | |
|---|---|---|
| Field names | givenName, familyName, … |
given_name, family_name, … |
| Military fields | separate extension, registerExtension(...) |
built in |
gender |
always required | auto-detected when omitted |
| API style | async/await |
plain synchronous calls |
| Validation errors | InputValidationError extends TypeError |
InputValidationError(ValueError) |
| Inverse (case → nominative) | not available | to_nominative |
License
MIT. Declension rules, gender rules, model weights, and test corpora are from shevchenko-js © Oleksandr Tolochko et al., MIT-licensed.
Metadata
Release files for shevchenko-py 0.8.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 | |
|---|---|---|---|
| shevchenko_py-0.8.0.tar.gz | 54.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| shevchenko_py-0.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 105.1 kB
Release files / shevchenko_py-0.8.0.tar.gz
| Download URL | shevchenko_py-0.8.0.tar.gz |
|---|---|
| Size | 54.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
296675b9fc5ea0797e6eaf26a451629c8148f7b5a6f006b40bc0c3b409b6e101
|
|
BLAKE2b-256 checksum How to use checksums |
cb1fe066bd559bf1cdb6bbaf36904e3712dda20cc4ef53ec5fb9d47141fde980
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|
Release files / shevchenko_py-0.8.0-py3-none-any.whl
| Download URL | shevchenko_py-0.8.0-py3-none-any.whl |
|---|---|
| Size | 50.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0e8aac4e3ba8e6e116979049e5cd99deee750d34fb6f87604145e3e13d4667dc
|
|
BLAKE2b-256 checksum How to use checksums |
caadc9f4a6a2eaa30188d39b7e0a52029a15902ea8f309306219c28fa797722a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|