Skip to main content

Zero-dependency Ukrainian declension of personal names, military ranks, and appointments (port of shevchenko-js). Import name: shevchenko.

Project description

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_name usually can't be sexed — pass gender explicitly or InputValidationError is 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:

  • case is not one of CASES (for in_case),
  • no field besides gender is provided,
  • a field value is not a string,
  • an unknown parameter is passed,
  • gender is neither "masculine", "feminine", nor omitted,
  • gender is 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 explicit gender= argument, or both-gender agreement; raises InputValidationError when 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.NOMINATIVEshevchenko.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,377 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

shevchenko_py-0.4.0.tar.gz (50.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

shevchenko_py-0.4.0-py3-none-any.whl (46.7 kB view details)

Uploaded Python 3

File details

Details for the file shevchenko_py-0.4.0.tar.gz.

File metadata

  • Download URL: shevchenko_py-0.4.0.tar.gz
  • Upload date:
  • Size: 50.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for shevchenko_py-0.4.0.tar.gz
Algorithm Hash digest
SHA256 6b52b502aa2deac2bccd302ca93ca370f4170a9cf7fe22dcb46a120d5033eb75
MD5 c31dab788e870747be539ed9f831b3c7
BLAKE2b-256 b7f805c95b27f0fc698b50c1173779584ee8ea9d094a0b4f685405fb3bb550c8

See more details on using hashes here.

File details

Details for the file shevchenko_py-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: shevchenko_py-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 46.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for shevchenko_py-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3f3afbebc4a6f5e2dfaed81f79cb14399eeb06d9eba2c3f850e8b044dc1a5dce
MD5 da3e5a95c9fde991bca15b1cf7de8f11
BLAKE2b-256 2ce0a06a3d1b1c3b50f1a1628d9c86dbd41b72113ad5d0682587c957dd430bd2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page