pluralio
Pluralization and singularization for Python
English · Spanish · Portuguese · French · Italian · Esperanto · Zero dependencies · Type-safe · Extensible
Features
- Zero dependencies — pure Python standard library, nothing else to install
- Type-safe — full type hints,
py.typedmarker included (PEP 561) - 100% test coverage — 7,233 tests, every line is verified
- 6 languages — English, Spanish, Portuguese, French, Italian, Esperanto
- Extensible at runtime — add irregulars, rules, uncountables, or entire languages without touching source code
- Utility functions —
join(),ordinal(),template()for common UI tasks - Case preservation —
"Library"→"Libraries","BOX"→"BOXES" - Count-aware —
pluralize("item", count=1)→"item" - Hyphenated words —
"mother-in-law"→"mothers-in-law" - Unicode normalization — NFC/NFD inputs handled transparently
- Sphinx documentation — full API reference at mathiaspaulenko.github.io/pluralio
- Python 3.10+ — tested on 3.10, 3.11, 3.12, 3.13, and 3.14
Table of contents
- Installation
- Quick start
- How it works
- API reference
- Extending rules
- Comparison
- Performance
- Supported languages
- Roadmap
- Changelog
- Contributing
- Security
- License
Installation
pip install pluralio
Or with uv:
uv add pluralio
Quick start
import pluralio
# ── English (default) ──────────────────────────────────────────
pluralio.pluralize("cat") # "cats"
pluralio.pluralize("box") # "boxes"
pluralio.pluralize("child") # "children"
pluralio.pluralize("city") # "cities"
pluralio.singularize("cities") # "city"
pluralio.singularize("mice") # "mouse"
pluralio.singularize("children") # "child"
# ── Spanish ────────────────────────────────────────────────────
pluralio.pluralize("gato", lang="es") # "gatos"
pluralio.pluralize("lápiz", lang="es") # "lápices"
pluralio.pluralize("examen", lang="es") # "exámenes"
pluralio.singularize("lápices", lang="es") # "lápiz"
pluralio.singularize("alemanes", lang="es") # "alemán"
# ── Portuguese ────────────────────────────────────────────────
pluralio.pluralize("casa", lang="pt") # "casas"
pluralio.pluralize("coração", lang="pt") # "corações"
pluralio.pluralize("papel", lang="pt") # "papéis"
pluralio.pluralize("flor", lang="pt") # "flores"
pluralio.singularize("corações", lang="pt") # "coração"
pluralio.singularize("papéis", lang="pt") # "papel"
# ── French ────────────────────────────────────────────────────
pluralio.pluralize("chat", lang="fr") # "chats"
pluralio.pluralize("cheval", lang="fr") # "chevaux"
pluralio.pluralize("bateau", lang="fr") # "bateaux"
pluralio.pluralize("travail", lang="fr") # "travaux"
pluralio.pluralize("bijou", lang="fr") # "bijoux"
pluralio.singularize("chevaux", lang="fr") # "cheval"
pluralio.singularize("travaux", lang="fr") # "travail"
# ── Italian ────────────────────────────────────────────────────
pluralio.pluralize("gatto", lang="it") # "gatti"
pluralio.pluralize("amico", lang="it") # "amici"
pluralio.pluralize("libro", lang="it") # "libri"
pluralio.singularize("amici", lang="it") # "amico"
pluralio.singularize("libri", lang="it") # "libro"
# ── Esperanto ──────────────────────────────────────────────────
pluralio.pluralize("libro", lang="eo") # "libroj"
pluralio.pluralize("librojn", lang="eo") # "librojn" (already plural)
pluralio.singularize("libroj", lang="eo") # "libro"
# ── Count-aware ────────────────────────────────────────────────
pluralio.pluralize("item", count=1) # "item" (singular)
pluralio.pluralize("item", count=0) # "items" (plural)
pluralio.pluralize("item", count=5) # "items" (plural)
# ── Case preservation ─────────────────────────────────────────
pluralio.pluralize("Library") # "Libraries"
pluralio.pluralize("LIBRARY") # "LIBRARIES"
pluralio.singularize("Libraries") # "Library"
# ── Hyphenated words ──────────────────────────────────────────
pluralio.pluralize("mother-in-law") # "mothers-in-law"
pluralio.singularize("mothers-in-law") # "mother-in-law"
# ── Unicode normalization ─────────────────────────────────────
import unicodedata
nfd = unicodedata.normalize("NFD", "lápiz") # NFD form
pluralio.pluralize(nfd, lang="es") # "lápices" (NFC output)
# ── List supported languages ──────────────────────────────────
pluralio.supported_languages() # ["en", "eo", "es", "fr", "it", "pt"]
# ── Inspect word form ─────────────────────────────────────────
pluralio.is_plural("cats") # True
pluralio.is_singular("cat") # True
pluralio.is_plural("sheep") # True (uncountable — valid as both)
pluralio.is_singular("sheep") # True (uncountable — valid as both)
pluralio.is_plural("gatos", lang="es") # True
# ── Join words into a list ────────────────────────────────────
pluralio.join(["apple", "banana", "carrot"]) # "apple, banana, and carrot"
pluralio.join(["apple", "banana", "carrot"], final_sep="") # "apple, banana and carrot"
pluralio.join(["apple", "banana", "carrot"], conjunction="or") # "apple, banana, or carrot"
# ── Ordinals ──────────────────────────────────────────────────
pluralio.ordinal(1) # "1st"
pluralio.ordinal(2) # "2nd"
pluralio.ordinal(11) # "11th"
pluralio.ordinal(21) # "21st"
# ── Template interpolation ────────────────────────────────────
pluralio.template("I have {count} {word:pluralize}", count=5, word="cat") # "I have 5 cats"
pluralio.template("I have {count} {word:pluralize}", count=1, word="cat") # "I have 1 cat"
pluralio.template("The {word:singularize} is here", word="mice") # "The mouse is here"
How it works
Every word goes through a three-step priority chain:
┌─────────────────────────────────────────────────────────────┐
│ Input word (stripped) │
└───────────────────────────┬─────────────────────────────────┘
▼
┌─────────────────────────┐
│ 1. Uncountable check │ ← highest priority
│ word in set? │
└────────────┬────────────┘
│ no
▼
┌─────────────────────────┐
│ 2. Irregular lookup │
│ word in dict? │
└────────────┬────────────┘
│ no
▼
┌─────────────────────────┐
│ 3. Regex rules │ ← first match wins
│ (ordered list) │
└────────────┬────────────┘
│ no match
▼
┌─────────────────────────┐
│ Return word unchanged │
└─────────────────────────┘
- Uncountable words (e.g.
"sheep","information") are returned as-is. - Irregular words (e.g.
"man"→"men") are looked up in a dictionary. - Regex rules are applied in order — the first matching pattern wins.
- The casing of the input is always preserved in the output.
API reference
Core functions
| Function | Description |
|---|---|
pluralize(word, lang="en", count=None) |
Convert a word to its plural form. |
singularize(word, lang="en") |
Convert a word to its singular form. |
is_plural(word, lang="en") |
Check if a word is in plural form. |
is_singular(word, lang="en") |
Check if a word is in singular form. |
supported_languages() |
Return a sorted list of registered language codes. |
Utility functions
| Function | Description |
|---|---|
join(words, *, conjunction="and", final_sep=", ", sep=", ") |
Join words into a natural-language list. |
ordinal(number) |
Convert a number to its ordinal string (1 → "1st"). |
template(text, **kwargs) |
Interpolate pluralize/singularize into string templates. |
Extensibility functions
| Function | Description |
|---|---|
add_irregular(singular, plural, lang="en") |
Add an irregular pair (both directions). |
add_plural(singular, plural, lang="en") |
Add only the singular → plural direction. |
add_singular(plural, singular, lang="en") |
Add only the plural → singular direction. |
add_uncountable(word, lang="en") |
Mark a word as invariable. |
add_plural_rule(pattern, replacement, lang="en") |
Insert a pluralization regex rule (top priority). |
add_singular_rule(pattern, replacement, lang="en") |
Insert a singularization regex rule (top priority). |
register_language(lang, *, plural_rules=..., ...) |
Register a completely new language. |
Low-level registry
| Function | Description |
|---|---|
LanguageRules |
Dataclass holding all rules for a language. |
register(rules) |
Register a LanguageRules instance. |
get_rules(lang) |
Retrieve rules for a language (raises ValueError if not found). |
Extending rules
pluralio is designed to be extended at runtime — no need to modify the source code.
Add an irregular word
Registers both pluralize and singularize directions:
pluralio.add_irregular("person", "people")
pluralio.pluralize("person") # "people"
pluralio.singularize("people") # "person"
Add only plural or singular direction
When you need just one direction (e.g. Spanish accent restoration):
# Only pluralize direction
pluralio.add_plural("joven", "jóvenes", lang="es")
pluralio.pluralize("joven", lang="es") # "jóvenes"
# Only singularize direction (accent restoration)
pluralio.add_singular("alemanes", "alemán", lang="es")
pluralio.singularize("alemanes", lang="es") # "alemán"
Add an uncountable / invariable word
pluralio.add_uncountable("data")
pluralio.pluralize("data") # "data"
pluralio.singularize("data") # "data"
Add a regex rule
Custom rules are inserted at the top of the rule list (highest priority — first match wins):
pluralio.add_plural_rule(r"us$", "i")
pluralio.pluralize("cactus") # "cacti"
pluralio.add_singular_rule(r"i$", "us")
pluralio.singularize("cacti") # "cactus"
Register a new language
pluralio.register_language(
"fr",
plural_rules=[(r"$", "s")],
singular_rules=[(r"s$", "")],
irregular_plurals={"cheval": "chevaux"},
uncountable={"information"},
)
pluralio.pluralize("chat", lang="fr") # "chats"
pluralio.pluralize("cheval", lang="fr") # "chevaux"
pluralio.singularize("chevaux", lang="fr") # "cheval"
pluralio.pluralize("information", lang="fr") # "information"
Comparison
How does pluralio compare to other Python inflection libraries?
| Feature | pluralio | inflect | pluralsingular | pluralizer |
|---|---|---|---|---|
| Spanish pluralization | ✅ 100% | ❌ 39% | ⚠️ 74% | ❌ 40% |
| Spanish singularization | ✅ 100% | ❌ 29% | ⚠️ 87% | ❌ N/A |
| Portuguese pluralization | ✅ 100% | ❌ | ❌ | ❌ |
| French pluralization | ✅ 100% | ❌ | ❌ | ❌ |
| Italian pluralization | ✅ 100% | ❌ | ❌ | ❌ |
| Esperanto pluralization | ✅ 100% | ❌ | ❌ | ❌ |
| Singularize | ✅ | ✅ | ✅ | ❌ |
Count-aware (count=1) |
✅ | ✅ | ❌ | ❌ |
| Case preservation | ✅ | ❌ | ❌ | ✅ |
| Hyphenated words | ✅ | ❌ | ❌ | ❌ |
| Unicode normalization (NFC/NFD) | ✅ | ❌ | ❌ | ❌ |
| Idempotency (ES) | ✅ | ❌ | ❌ | ❌ |
| Uncountables (ES) | ✅ ~87 | ❌ | ❌ | ❌ |
| Accent restoration (ES) | ✅ | ❌ | ⚠️ Partial | ❌ |
| Runtime extensibility | ✅ | ❌ | ❌ | ✅ |
| Add custom languages | ✅ | ❌ | ✅ | ❌ |
| Zero dependencies | ✅ | ❌ | ✅ | ✅ |
Type hints (py.typed) |
✅ | ✅ | ❌ | ❌ |
| Sphinx documentation | ✅ | ❌ | ❌ | ❌ |
| Python 3.10+ | ✅ | ✅ | ✅ | ✅ |
| Test coverage | 100% | ~95% | ~60% | ~70% |
| Number to words | ❌ | ✅ | ❌ | ❌ |
Ordinals (1 → 1st) |
❌ | ✅ | ❌ | ❌ |
Indefinite articles (a/an) |
❌ | ✅ | ❌ | ❌ |
| Join words into a list | ✅ | ✅ | ❌ | ❌ |
| Present participle | ❌ | ✅ | ❌ | ❌ |
| Classical pluralization modes | ❌ | ✅ | ❌ | ❌ |
| Part-of-speech inflection | ❌ | ✅ | ❌ | ❌ |
| Template interpolation | ✅ | ✅ | ❌ | ❌ |
| English-only maturity | 6 languages | Deep EN | EN only | EN only |
Performance
pluralio is fast — no regex compilation at call time, lru_cache on regex application, and islower() short-circuit for the common case:
pluralize: ~1.8 µs/call
singularize: ~1.8 µs/call
Benchmark: 100,000 calls across 13 mixed-language words (English, Spanish, Portuguese, French, Italian) on Python 3.14.
Supported languages
| Language | Code | Regex rules | Irregulars | Uncountables | Status |
|---|---|---|---|---|---|
| English | en |
7 + 22 | 684 | 219 | ✅ Complete |
| Spanish | es |
9 + 8 | 352 + 81 extra singles | 92 | ✅ Complete |
| Portuguese | pt |
8 + 13 | 388 + 88 extra singles | 88 | ✅ Complete |
| French | fr |
6 + 4 | 104 + 27 extra singles | 81 | ✅ Complete |
| Italian | it |
19 + 12 | 239 + 0 extra singles | 144 | ✅ Complete |
| Esperanto | eo |
2 + 2 | 0 | 33 | ✅ Complete |
Roadmap
| Version | Goal | Status |
|---|---|---|
1.0.0 |
English + Spanish, core engine, extensibility API | ✅ Released |
1.5.0 |
Portuguese (pt) |
✅ Released |
1.6.0 |
French (fr) |
✅ Released |
1.7.0 |
Italian (it) |
✅ Released |
2.0.0 |
Rules restructured into pluralio/rules/ subpackage, performance optimization |
✅ Released |
2.1.0 |
Esperanto (eo) — trivial -j plural |
✅ Released |
2.2.0 |
Utility functions: join(), ordinal(), template() |
✅ Released |
2.3.0 |
Catalan (ca) — Romance, natural fit |
🔜 Planned |
3.0.0 |
German (de) — umlauts + multiple plural patterns |
🔜 Planned |
Changelog
See CHANGELOG.md for the full history.
Recent releases
- 2.2.0 — Utility functions:
join(),ordinal(),template() - 2.1.2 — Bug fixes:
restore()cache clearing,_match_casedigit-only source,register_languageredundant call - 2.1.1 — Bug fixes: Esperanto double-plural,
register()cache,_match_caseempty target - 2.1.0 — Esperanto (
eo) support - 2.0.0 — Rules restructured into
pluralio/rules/subpackage, performance optimization
Contributing
Contributions are welcome! Whether it's a bug report, a new language, or a feature suggestion — please read our guides first:
- 📖 Contributing Guide — development setup, coding standards, PR process
- 🤝 Code of Conduct — community expectations
- 🐛 Bug Report Template
- ✨ Feature Request Template
Quick start for contributors
git clone https://github.com/MathiasPaulenko/pluralio.git
cd pluralio
pip install -e ".[dev]"
# Run checks
ruff check pluralio/ tests/
mypy pluralio/ tests/
pytest
# Build docs locally
pip install -e ".[docs]"
python -m sphinx -b html docs docs/_build/html
Security
If you discover a security vulnerability, please see our Security Policy for responsible disclosure instructions. Do not open a public issue.
License
MIT — Copyright (c) 2025 Mathias Paulenko
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pluralio-2.2.0.tar.gz.
File metadata
- Download URL: pluralio-2.2.0.tar.gz
- Upload date:
- Size: 121.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fab171e1dfee50d5782a3082dc0fd49fb0996e11390d59c3e20e359ea7293dea
|
|
| MD5 |
aa8fc15707ec8d5fc5c80269714534c1
|
|
| BLAKE2b-256 |
aa6bd3c2ab519b4abc1cdfdeff4473f0816833a1fc8c6a665fa0446ac3ca826d
|
Provenance
The following attestation bundles were made for pluralio-2.2.0.tar.gz:
Publisher:
release.yml on MathiasPaulenko/pluralio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pluralio-2.2.0.tar.gz -
Subject digest:
fab171e1dfee50d5782a3082dc0fd49fb0996e11390d59c3e20e359ea7293dea - Sigstore transparency entry: 2158088816
- Sigstore integration time:
-
Permalink:
MathiasPaulenko/pluralio@fddf7b2929154ae479ef11fd45529293f310b22b -
Branch / Tag:
refs/tags/v2.2.0 - Owner: https://github.com/MathiasPaulenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fddf7b2929154ae479ef11fd45529293f310b22b -
Trigger Event:
push
-
Statement type:
File details
Details for the file pluralio-2.2.0-py3-none-any.whl.
File metadata
- Download URL: pluralio-2.2.0-py3-none-any.whl
- Upload date:
- Size: 55.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0afcff2c6d4c41cec26683edb5d2a7a7dd5157d7ac6495ac284d66cab65cc4b6
|
|
| MD5 |
2f82fb8fca9658a40265a52dc8666634
|
|
| BLAKE2b-256 |
0a41320da61dc6cc9eb5daeeda993aadcab9d94343f3a37271baade61f647464
|
Provenance
The following attestation bundles were made for pluralio-2.2.0-py3-none-any.whl:
Publisher:
release.yml on MathiasPaulenko/pluralio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pluralio-2.2.0-py3-none-any.whl -
Subject digest:
0afcff2c6d4c41cec26683edb5d2a7a7dd5157d7ac6495ac284d66cab65cc4b6 - Sigstore transparency entry: 2158088824
- Sigstore integration time:
-
Permalink:
MathiasPaulenko/pluralio@fddf7b2929154ae479ef11fd45529293f310b22b -
Branch / Tag:
refs/tags/v2.2.0 - Owner: https://github.com/MathiasPaulenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fddf7b2929154ae479ef11fd45529293f310b22b -
Trigger Event:
push
-
Statement type: