Pluralization and singularization for Python — ES/EN, zero deps, type-safe
Project description
pluralio
Pluralization and singularization for Python
English · Spanish · 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 — 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
Table of contents
- Installation
- Quick start
- How it works
- API reference
- Extending rules
- Comparison
- Supported languages
- Roadmap
- 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"
# ── 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)
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. |
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 | 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% |
Supported languages
| 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 |
Roadmap
| 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 |
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/
pytest
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
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
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-1.2.0.tar.gz.
File metadata
- Download URL: pluralio-1.2.0.tar.gz
- Upload date:
- Size: 46.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd242af5f442f2d226cdc4bb9549fd1ab56aaea0ad8987258aeb81534b42b2bf
|
|
| MD5 |
11367649d0dd056cf2d39b7148d5f20f
|
|
| BLAKE2b-256 |
693ac1a770226923de59a04ef99de0caab398feb148b72b2d7c7667de8df3750
|
Provenance
The following attestation bundles were made for pluralio-1.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-1.2.0.tar.gz -
Subject digest:
bd242af5f442f2d226cdc4bb9549fd1ab56aaea0ad8987258aeb81534b42b2bf - Sigstore transparency entry: 2143471314
- Sigstore integration time:
-
Permalink:
MathiasPaulenko/pluralio@86071a1d31170ef7c277877e488550b3e730e91b -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/MathiasPaulenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@86071a1d31170ef7c277877e488550b3e730e91b -
Trigger Event:
push
-
Statement type:
File details
Details for the file pluralio-1.2.0-py3-none-any.whl.
File metadata
- Download URL: pluralio-1.2.0-py3-none-any.whl
- Upload date:
- Size: 24.7 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 |
30739bbf61766fced50e3283098500399aca794c7f8dd29ca97712f021142eb8
|
|
| MD5 |
f9852c12cdf753230db838af7e67ff2a
|
|
| BLAKE2b-256 |
b853a6f8e31761b9d98b0ad54d64a55de3d78431ad6a657be2a42ba3f288ec64
|
Provenance
The following attestation bundles were made for pluralio-1.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-1.2.0-py3-none-any.whl -
Subject digest:
30739bbf61766fced50e3283098500399aca794c7f8dd29ca97712f021142eb8 - Sigstore transparency entry: 2143471827
- Sigstore integration time:
-
Permalink:
MathiasPaulenko/pluralio@86071a1d31170ef7c277877e488550b3e730e91b -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/MathiasPaulenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@86071a1d31170ef7c277877e488550b3e730e91b -
Trigger Event:
push
-
Statement type: