Extensibility API

pluralio — Pluralization and singularization for Python.

Public API:
  • pluralize(): Convert a word to plural form.

  • singularize(): Convert a word to singular form.

  • supported_languages(): List registered language codes.

  • add_irregular(): Add an irregular word pair (both directions).

  • add_plural(): Add only the singular→plural direction.

  • add_singular(): Add only the plural→singular direction.

  • add_uncountable(): Mark a word as invariable.

  • add_plural_rule(): Insert a pluralization regex rule.

  • add_singular_rule(): Insert a singularization regex rule.

  • register_language(): Register a new language with its rules.

  • is_plural(): Check if a word is in plural form.

  • is_singular(): Check if a word is in singular form.

  • join(): Join words into a natural-language list.

  • ordinal(): Convert a number to its ordinal string (1 → “1st”).

  • template(): Interpolate pluralize/singularize into string templates.

  • LanguageRules: Dataclass for custom language rules.

  • register(): Low-level registry registration.

  • get_rules(): Low-level registry lookup.

Supported languages (built-in):
  • English (en)

  • Spanish (es)

  • Portuguese (pt)

  • French (fr)

  • Italian (it)

  • Esperanto (eo)

Note

Language rules are registered as a side effect of importing this package. Always use import pluralio (or from pluralio import pluralize) rather than importing from submodules directly, e.g. avoid from pluralio.core import pluralize, because the latter bypasses registration and will raise ValueError for any non-default language.

Example

>>> import pluralio
>>> pluralio.pluralize("cat")
'cats'
>>> pluralio.pluralize("gato", lang="es")
'gatos'
>>> pluralio.singularize("cities")
'city'
>>> pluralio.pluralize("libro", lang="eo")
'libroj'
class pluralio.LanguageRules(code: str, irregular_plurals: dict[str, str] = <factory>, irregular_singles: dict[str, str] = <factory>, plural_rules: list[tuple[~re.Pattern[str], str]] = <factory>, singular_rules: list[tuple[~re.Pattern[str], str]] = <factory>, uncountable: set[str] = <factory>)[source]

Bases: object

Container for all pluralization/singularization rules of a language.

code

ISO 639-1 language code (e.g. "en", "es", "fr").

Type:

str

irregular_plurals

Mapping of singular → plural for words that do not follow regex rules. Keys and values are lowercase. Checked before regex rules during pluralization.

Type:

dict[str, str]

irregular_singles

Mapping of plural → singular for words that do not follow regex rules. Keys and values are lowercase. Checked before regex rules during singularization. Typically the inverse of irregular_plurals, but may include extra entries (e.g. Spanish accent restoration).

Type:

dict[str, str]

plural_rules

Ordered list of (compiled_regex, replacement) tuples applied during pluralization. First match wins.

Type:

list[tuple[re.Pattern[str], str]]

singular_rules

Ordered list of (compiled_regex, replacement) tuples applied during singularization. First match wins.

Type:

list[tuple[re.Pattern[str], str]]

uncountable

Set of lowercase words that are invariable — both pluralize and singularize return them unchanged. Checked first, before irregulars and regex.

Type:

set[str]

Warning

frozen=True prevents reassigning attributes (rules.code = "fr" raises FrozenInstanceError), but the mutable containers (dict, list, set) can still be modified in place. This is by design — the extensibility API (pluralio.add_irregular(), pluralio.add_plural(), etc.) mutates the contents of already-registered instances. Always use those functions instead of mutating containers directly.

code: str
irregular_plurals: dict[str, str]
irregular_singles: dict[str, str]
plural_rules: list[tuple[Pattern[str], str]]
singular_rules: list[tuple[Pattern[str], str]]
uncountable: set[str]
pluralio.add_irregular(singular: str, plural: str, lang: str = 'en') None[source]

Add an irregular word pair for both pluralize and singularize.

This registers the word in both irregular_plurals (singular → plural) and irregular_singles (plural → singular), so that both pluralize() and singularize() use the mapping. The entries are stored in lowercase for case-insensitive matching.

Parameters:
  • singular – The singular form of the word.

  • plural – The plural form of the word.

  • lang – Language code to register the pair in. Defaults to "en".

Example

>>> add_irregular("person", "people")
>>> pluralize("person")
'people'
>>> singularize("people")
'person'
pluralio.add_plural(singular: str, plural: str, lang: str = 'en') None[source]

Add only the singular→plural direction for an irregular word.

Unlike add_irregular(), this only affects pluralize(). singularize() will not know about the inverse mapping.

This is useful when the plural→singular direction follows a different rule or needs a separate entry (e.g. Spanish accent restoration).

Parameters:
  • singular – The singular form of the word.

  • plural – The plural form of the word.

  • lang – Language code. Defaults to "en".

Example

>>> add_plural("joven", "jóvenes", lang="es")
>>> pluralize("joven", lang="es")
'jóvenes'
pluralio.add_plural_rule(pattern: str, replacement: str, lang: str = 'en') None[source]

Insert a pluralization regex rule at the top (highest priority).

Rules are checked in order, and the first match wins. By inserting at position 0, the new rule takes priority over all existing rules.

Parameters:
  • pattern – Regular expression pattern to match (string, will be compiled internally).

  • replacement – Replacement string for re.sub.

  • lang – Language code. Defaults to "en".

Example

>>> add_plural_rule(r"us$", "i")
>>> pluralize("cactus")
'cacti'
pluralio.add_singular(plural: str, singular: str, lang: str = 'en') None[source]

Add only the plural→singular direction for an irregular word.

Unlike add_irregular(), this only affects singularize(). pluralize() will not know about the forward mapping.

This is useful for Spanish accent restoration where the plural form has a predictable ending but the singular requires an accent that the regex rules cannot infer.

Parameters:
  • plural – The plural form of the word.

  • singular – The singular form of the word.

  • lang – Language code. Defaults to "en".

Example

>>> add_singular("alemanes", "alemán", lang="es")
>>> singularize("alemanes", lang="es")
'alemán'
pluralio.add_singular_rule(pattern: str, replacement: str, lang: str = 'en') None[source]

Insert a singularization regex rule at the top (highest priority).

Rules are checked in order, and the first match wins. By inserting at position 0, the new rule takes priority over all existing rules.

Parameters:
  • pattern – Regular expression pattern to match (string, will be compiled internally).

  • replacement – Replacement string for re.sub.

  • lang – Language code. Defaults to "en".

Example

>>> add_singular_rule(r"i$", "us")
>>> singularize("cacti")
'cactus'
pluralio.add_uncountable(word: str, lang: str = 'en') None[source]

Mark a word as uncountable (invariable).

Uncountable words are returned unchanged by both pluralize() and singularize(). They are checked before irregulars and regex rules, so they take highest priority.

Parameters:
  • word – The word to mark as uncountable.

  • lang – Language code. Defaults to "en".

Example

>>> add_uncountable("data")
>>> pluralize("data")
'data'
>>> singularize("data")
'data'
pluralio.get_rules(lang: str) LanguageRules[source]

Retrieve the rules for a given language code.

Parameters:

lang – ISO 639-1 language code (e.g. "en", "es").

Returns:

The LanguageRules instance for the requested language.

Raises:

ValueError – If lang is not registered. The error message includes the list of supported languages.

Example

>>> from pluralio.registry import get_rules
>>> rules = get_rules("en")
>>> rules.code
'en'
pluralio.is_plural(word: str, lang: str = 'en') bool[source]

Check if a word is in its plural form.

A word is considered plural if singularizing it produces a different word. Uncountable (invariable) words are valid as both singular and plural, so they return True.

Parameters:
  • word – The word to check.

  • lang – ISO 639-1 language code. Defaults to "en".

Returns:

True if the word is in plural form (or is uncountable), False otherwise.

Raises:

Example

>>> is_plural("cats")
True
>>> is_plural("cat")
False
>>> is_plural("sheep")
True
>>> is_plural("gatos", lang="es")
True
pluralio.is_singular(word: str, lang: str = 'en') bool[source]

Check if a word is in its singular form.

A word is considered singular if pluralizing it produces a different word. Uncountable (invariable) words are valid as both singular and plural, so they return True.

Parameters:
  • word – The word to check.

  • lang – ISO 639-1 language code. Defaults to "en".

Returns:

True if the word is in singular form (or is uncountable), False otherwise.

Raises:

Example

>>> is_singular("cat")
True
>>> is_singular("cats")
False
>>> is_singular("sheep")
True
>>> is_singular("gato", lang="es")
True
pluralio.join(words: Iterable[str], *, conjunction: str = 'and', final_sep: str = ', ', sep: str = ', ') str[source]

Join words into a natural-language list.

Parameters:
  • words – Iterable of strings to join.

  • conjunction – Conjunction word for the last item. Defaults to "and".

  • final_sep – Separator before the conjunction. Defaults to ", ". Pass "" for no comma before the conjunction (Oxford comma removal).

  • sep – Separator between items (except before the conjunction). Defaults to ", ".

Returns:

A natural-language string joining all words.

Example

>>> join(["apple"])
'apple'
>>> join(["apple", "banana"])
'apple and banana'
>>> join(["apple", "banana", "carrot"])
'apple, banana, and carrot'
>>> join(["apple", "banana", "carrot"], final_sep="")
'apple, banana and carrot'
>>> join(["apple", "banana", "carrot"], conjunction="or")
'apple, banana, or carrot'
pluralio.ordinal(number: int | str) str[source]

Convert a number to its ordinal string representation.

Parameters:

number – An integer or a string representation of an integer.

Returns:

The ordinal string (e.g. "1st", "2nd", "3rd", "11th").

Raises:

ValueError – If number cannot be converted to an integer.

Example

>>> ordinal(1)
'1st'
>>> ordinal(2)
'2nd'
>>> ordinal(3)
'3rd'
>>> ordinal(11)
'11th'
>>> ordinal(21)
'21st'
>>> ordinal("101")
'101st'
>>> ordinal(0)
'0th'
pluralio.pluralize(word: str, lang: str = 'en', count: int | None = None) str[source]

Convert a word to its plural form.

The function follows the three-step priority chain (uncountable → irregular → regex) and preserves the input casing in the output.

Parameters:
  • word – The singular word to pluralize.

  • lang – ISO 639-1 language code. Defaults to "en".

  • count – Optional integer count. When count == 1 the word is returned unchanged (singular form). Any other value (including 0, negative numbers, and None) produces the plural form.

Returns:

The plural form of word, with original casing preserved.

Raises:

Example

>>> pluralize("cat")
'cats'
>>> pluralize("box")
'boxes'
>>> pluralize("child")
'children'
>>> pluralize("gato", lang="es")
'gatos'
>>> pluralize("item", count=1)
'item'
>>> pluralize("Library")
'Libraries'
>>> pluralize("mother-in-law")
'mothers-in-law'
pluralio.register(rules: LanguageRules) None[source]

Register a language’s rules in the global registry.

If a language with the same code already exists, it is overwritten. The regex application cache is cleared to prevent stale results.

Parameters:

rules – The LanguageRules instance to register.

Raises:

ValueError – If rules.code is empty.

Example

>>> from pluralio.registry import LanguageRules, register
>>> register(LanguageRules(code="xx"))
pluralio.register_language(lang: str, *, plural_rules: list[tuple[str, str]] | None = None, singular_rules: list[tuple[str, str]] | None = None, irregular_plurals: dict[str, str] | None = None, irregular_singles: dict[str, str] | None = None, uncountable: set[str] | None = None) None[source]

Register a new language with its pluralization rules.

This is the high-level API for adding a completely new language. It compiles the regex patterns, normalizes keys to lowercase, auto-generates inverse irregular mappings, and registers the resulting LanguageRules in the global registry.

If plural_rules is omitted, a default rule of (r"$", "s") is used (append s). If singular_rules is omitted, a default rule of (r"s$", "") is used (strip trailing s).

For every entry in irregular_plurals, the inverse is automatically added to irregular_singles unless an explicit entry already exists.

Parameters:
  • lang – ISO 639-1 language code for the new language.

  • plural_rules – List of (pattern, replacement) string tuples for pluralization. Checked in order; first match wins.

  • singular_rules – List of (pattern, replacement) string tuples for singularization. Checked in order; first match wins.

  • irregular_plurals – Mapping of singular → plural for irregular words. Keys and values are normalized to lowercase.

  • irregular_singles – Mapping of plural → singular for irregular words. Keys and values are normalized to lowercase. Auto-populated from irregular_plurals where possible.

  • uncountable – Set of invariable words. Normalized to lowercase.

Example

>>> register_language(
...     "fr",
...     plural_rules=[(r"$", "s")],
...     singular_rules=[(r"s$", "")],
...     irregular_plurals={"cheval": "chevaux"},
...     uncountable={"information"},
... )
>>> pluralize("chat", lang="fr")
'chats'
>>> pluralize("cheval", lang="fr")
'chevaux'
>>> singularize("chevaux", lang="fr")
'cheval'
>>> pluralize("information", lang="fr")
'information'
pluralio.singularize(word: str, lang: str = 'en') str[source]

Convert a word to its singular form.

The function follows the three-step priority chain (uncountable → irregular → regex) and preserves the input casing in the output.

Parameters:
  • word – The plural word to singularize.

  • lang – ISO 639-1 language code. Defaults to "en".

Returns:

The singular form of word, with original casing preserved.

Raises:

Example

>>> singularize("cities")
'city'
>>> singularize("mice")
'mouse'
>>> singularize("lápices", lang="es")
'lápiz'
>>> singularize("Libraries")
'Library'
>>> singularize("mothers-in-law")
'mother-in-law'
pluralio.supported_languages() list[str][source]

Return a sorted list of all registered language codes.

Returns:

Sorted list of ISO 639-1 codes (e.g. ["en", "eo", "es", "fr", "it", "pt"]).

Example

>>> from pluralio.registry import supported_languages
>>> supported_languages()
['en', 'eo', 'es', 'fr', 'it', 'pt']
pluralio.template(text: str, **kwargs: object) str[source]

Interpolate pluralize/singularize into a string template.

Placeholders use the syntax {name}, {name:pluralize}, or {name:singularize}. When :pluralize is used, the function looks for a count key in kwargs (or a custom count variable specified as {name:pluralize:count_var}) to enable count-aware pluralization.

Parameters:
  • text – Template string with placeholders.

  • **kwargs – Values for placeholder substitution. If a count key is present, it is used for count-aware pluralization. A custom count variable can be specified in the placeholder syntax (e.g. {word:pluralize:n} uses kwargs["n"] as the count).

Returns:

The interpolated string.

Raises:

KeyError – If a placeholder name is not found in kwargs.

Example

>>> template("I have {count} {word:pluralize}", count=5, word="cat")
'I have 5 cats'
>>> template("I have {count} {word:pluralize}", count=1, word="cat")
'I have 1 cat'
>>> template("The {word:singularize} is here", word="mice")
'The mouse is here'
>>> template("{n} {word:pluralize} arrived", n=3, word="box")
'3 boxes arrived'

Examples

Adding an irregular word pair:

from pluralio import add_irregular, pluralize, singularize

add_irregular("person", "people")
pluralize("person")       # "people"
singularize("people")     # "person"

Adding a plural-only mapping:

from pluralio import add_plural, pluralize

add_plural("joven", "jóvenes", lang="es")
pluralize("joven", lang="es")  # "jóvenes"

Adding a singular-only mapping (accent restoration):

from pluralio import add_singular, singularize

add_singular("alemanes", "alemán", lang="es")
singularize("alemanes", lang="es")  # "alemán"

Marking a word as uncountable:

from pluralio import add_uncountable, pluralize, singularize

add_uncountable("data")
pluralize("data")    # "data"
singularize("data")  # "data"

Adding a custom regex rule:

from pluralio import add_plural_rule, pluralize

add_plural_rule(r"us$", "i")
pluralize("cactus")  # "cacti"

Registering a new language:

from pluralio import register_language, pluralize, singularize

register_language(
    "xx",
    plural_rules=[(r"$", "s")],
    singular_rules=[(r"s$", "")],
    irregular_plurals={"foo": "foos"},
    uncountable={"bar"},
)
pluralize("word", lang="xx")  # "words"
pluralize("foo", lang="xx")   # "foos"
pluralize("bar", lang="xx")   # "bar"

Checking plural / singular:

from pluralio import is_plural, is_singular

is_plural("cats")       # True
is_singular("cat")      # True
is_plural("gatos", lang="es")   # True
is_singular("gato", lang="es")  # True