Skip to main content

pluralio

Pluralization and singularization for Python

6 languages · 7,280 tests · Zero dependencies · Type-safe · Extensible at runtime

PyPI version Python versions CI Coverage License Code style: ruff Type checker: mypy Downloads Docs


Features

  • Zero dependencies — pure Python standard library, nothing else to install
  • Type-safe — full type hints, py.typed marker included (PEP 561)
  • 100% test coverage — 7,280 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

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).
snapshot() Deep-copy the current registry state (test isolation).
restore(state) Restore a registry state returned by snapshot().

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) ✅ ~92 ❌ ❌ ❌
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 354 92 ✅ Complete
Portuguese pt 9 + 13 393 88 ✅ Complete
French fr 6 + 4 100 92 ✅ Complete
Italian it 19 + 12 249 126 ✅ Complete
Esperanto eo 4 + 2 0 39 ✅ 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 Bug fixes: language-code normalization, rule corrections (FR/IT/PT), docs ✅ Released
2.4.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.3.0 — Bug fixes: language-code normalization, rule corrections (FR/IT/PT), doctests
  • 2.2.0 — Utility functions: join(), ordinal(), template()
  • 2.1.2 — Bug fixes: restore() cache clearing, _match_case digit-only source, register_language redundant call
  • 2.1.1 — Bug fixes: Esperanto double-plural, register() cache, _match_case empty 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:

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

Metadata

Release files for pluralio 2.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pluralio 2.3.0
File Size Uploaded
pluralio-2.3.0.tar.gz 124.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pluralio 2.3.0
File Interpreter ABI Platform
pluralio-2.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 180.9 kB

Release files / pluralio-2.3.0.tar.gz

Download URL pluralio-2.3.0.tar.gz
Size 124.2 kB
Tags Source
SHA-256 checksum
How to use checksums
44f4aa9d83cff0dd50341b00d9409557ed8e4152668d42d995167bd46929948f
BLAKE2b-256 checksum
How to use checksums
3deec28fd715cba83c7036cfb661f47a2f657c40ccc08af60da673db0231b69f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / pluralio-2.3.0-py3-none-any.whl

Download URL pluralio-2.3.0-py3-none-any.whl
Size 56.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3bb1c5e7c6e42c49b2f37b5f56d052c8f43f837615641b47897df7f756b25e40
BLAKE2b-256 checksum
How to use checksums
5f203b0ae6a70a942cba3c92b2c72451e76cce4209e1315d98ac68f57894291b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.3.0 This release

2 release files

2.2.0

2 release files

2.1.2

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.8.3

2 release files

1.8.2

2 release files

1.8.1

2 release files

1.8.0

2 release files

1.7.2

2 release files

1.7.1

2 release files

1.7.0

2 release files

1.6.2

2 release files

1.6.1

2 release files

1.6.0

2 release files

1.5.5

2 release files

1.5.4

2 release files

1.5.3

2 release files

1.5.2

2 release files

1.5.1

2 release files

1.5.0

2 release files

1.4.2

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page