Pluralization and singularization for Python
English · Spanish · Zero dependencies · Type-safe · Extensible
- Zero dependencies — pure Python standard library, nothing else to install
- Type-safe — full type hints,
py.typedmarker included (PEP 561) - 100% test coverage — 869 tests, every line is verified
- Extensible at runtime — add irregulars, rules, uncountables, or entire languages without touching source code
- Case preservation —
"Library"→"Libraries","BOX"→"BOXES" - Count-aware —
pluralize("item", count=1)→"item" - Hyphenated words —
"mother-in-law"→"mothers-in-law" - Python 3.10+ — tested on 3.10, 3.11, 3.12, and 3.13
- Installation
- Quick start
- How it works
- API reference
- Extending rules
- Comparison
- Supported languages
- Roadmap
- Contributing
- Security
- License
pip install pluralioOr with uv:
uv add pluralioimport 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"
# ── 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"
# ── List supported languages ──────────────────────────────────
pluralio.supported_languages() # ["en", "es"]
# ── Inspect word form ─────────────────────────────────────────
pluralio.is_plural("cats") # True
pluralio.is_singular("cat") # True
pluralio.is_plural("sheep") # False (uncountable)
pluralio.is_singular("sheep") # False (uncountable)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.
| 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. |
| 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. |
| 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). |
pluralio is designed to be extended at runtime — no need to modify the source code.
Registers both pluralize and singularize directions:
pluralio.add_irregular("person", "people")
pluralio.pluralize("person") # "people"
pluralio.singularize("people") # "person"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"pluralio.add_uncountable("data")
pluralio.pluralize("data") # "data"
pluralio.singularize("data") # "data"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"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"How does pluralio compare to other Python inflection libraries?
| Feature | pluralio | inflect | Inflector | pluralizer |
|---|---|---|---|---|
| Spanish support | ✅ | ❌ | ❌ | ❌ |
| Singularize | ✅ | ✅ | ✅ | ❌ |
Count-aware (count=1) |
✅ | ✅ | ❌ | ❌ |
| Case preservation | ✅ | ❌ | ✅ | ✅ |
| Hyphenated words | ✅ | ❌ | ❌ | ❌ |
| Runtime extensibility | ✅ | ❌ | ✅ | ✅ |
| Add custom languages | ✅ | ❌ | ❌ | ❌ |
| Zero dependencies | ✅ | ✅ | ✅ | ✅ |
Type hints (py.typed) |
✅ | ✅ | ❌ | ❌ |
| Python 3.10+ | ✅ | ✅ | ✅ | ✅ |
| Test coverage | 100% | ~95% | ~80% | ~70% |
| Language | Code | Regex rules | Irregulars | Uncountables | Status |
|---|---|---|---|---|---|
| English | en |
6 | 131 | 76 | ✅ Complete |
| Spanish | es |
9 | 61 + 34 extra singles | 62 | ✅ Complete |
| Portuguese | pt |
— | — | — | 🔜 Planned |
| French | fr |
— | — | — | 🔜 Planned |
| Italian | it |
— | — | — | 🔜 Planned |
| Version | Goal | Status |
|---|---|---|
1.0.0 |
English + Spanish, core engine, extensibility API | ✅ Released |
1.1.0 |
Portuguese (pt) |
🔜 Planned |
1.2.0 |
French (fr) |
🔜 Planned |
1.3.0 |
Italian (it) |
🔜 Planned |
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
git clone https://github.com/MathiasPaulenko/pluralio.git
cd pluralio
pip install -e ".[dev]"
# Run checks
ruff check pluralio/ tests/
mypy pluralio/
pytestIf you discover a security vulnerability, please see our Security Policy for responsible disclosure instructions. Do not open a public issue.
MIT — Copyright (c) 2025 Mathias Paulenko