mrkeyoor.com_
Thu 06 Aug 15:39 UTC
PyPIUtilsupdated 06 Aug 2026

inflection

inflection is a straight port of the Rails inflector: one Python file, no dependencies, twelve public functions. It converts between naming conventions (camelize, underscore, dasherize, parameterize), makes strings human-readable (humanize, titleize, ordinalize), and guesses English plurals and singulars (pluralize, singularize, tableize). Everything is regular expressions applied in order against two module-level rule lists, PLURALS and SINGULARS, plus a small set of hardcoded irregulars for person, man, human, child, sex, move, cow and zombie. That design is the whole story: it is fast, it is predictable, and its knowledge of English is limited to about sixty patterns. Most people install it transitively, not deliberately, because it is what a schema generator or serialiser in their dependency tree uses to turn DeviceType into device_type.

Verdict

For turning class names into table names and snake_case into camelCase it is a fine sixty-line dependency that has not needed to change. Do not let it near arbitrary English plurals or non-ASCII text, because it will fail quietly rather than loudly.

API stability5/5Twelve functions, unchanged signatures since 0.4 in 2018, and the last release was 2020. Nothing can break because nothing moves. Type hints were added in 0.5.0 and the module ships a py.typed marker.
Docs3/5inflection.readthedocs.io is the autodoc output of the module's docstrings, which do at least carry runnable examples for every function including the awkward ones like camelize(underscore('IOError')). There is no guidance beyond that: no page on extending the rules, no list of which irregulars are covered, and no discussion of where the plural rules break down.
Maintenance2/5Last release 0.5.1 on 22 August 2020, last commit to the repository 13 August 2023, and 23 open issues plus 9 open PRs with no recent responses. Nothing is on fire because a pure-regex module with no dependencies does not rot, but if a rule is wrong for you, it will stay wrong.
Ecosystem4/5Around 12M weekly downloads, almost all of it transitive: OpenAPI and protobuf code generators, serialisation layers and schema tools reach for it because it is small and matches Rails. That makes it a de facto standard for convention conversion even though few people install it on purpose.

Use it if

  • You are converting between naming conventions across a boundary: snake_case Python attributes to camelCase JSON keys, model class names to table names, column names to form labels
  • You want the exact Rails behaviour because you are porting a Rails app, talking to a Rails API, or matching table names that ActiveRecord already generated
  • You need something with no dependencies and no import cost in a library that other people will install, where pulling in a large NLP or number-formatting package would be rude
  • The vocabulary you are pluralising is your own: entity names, resource names, table names, all of which you control and can spot-check
Skip it if

Setup reality

pip install inflection installs one pure-Python module with no dependencies and works on every Python 3 version, so there is nothing to configure and nothing to build. The catch is what happens after import. The rule lists are module-level mutable globals, so if you want a custom plural you are appending to inflection.PLURALS or adding to inflection.UNCOUNTABLES, and that change is visible to every other package in the interpreter that imported inflection. Order matters too: rules are tried top to bottom and the first match wins, so a new rule appended to the end of the list will never fire if a broader pattern above it already matches, and you have to insert at position 0. Two functions raise rather than returning something sensible on empty input: camelize('', False) raises IndexError because it indexes the first character. Guard your call sites if the strings come from user data. Finally, note that the metadata declares requires-python >= 3.5 and lists no classifiers past 3.8, which reflects the 2020 release date rather than any actual incompatibility.

Patterns

Convert between naming conventionscase-conversion

import inflection

inflection.underscore('DeviceType')        # 'device_type'
inflection.underscore('HTTPResponse')      # 'http_response'
inflection.camelize('device_type')         # 'DeviceType'
inflection.camelize('device_type', False)  # 'deviceType'
inflection.dasherize('device_type')        # 'device-type'

underscore also turns hyphens into underscores, so it doubles as a normaliser for mixed input. The pair is not a perfect round-trip: camelize(underscore('IOError')) gives 'IoError' because underscore cannot tell an acronym from a word. camelize('', False) raises IndexError, so guard empty strings.

Convert dict keys for an API boundaryjson-key-conversion

import inflection

def to_camel(obj):
    if isinstance(obj, dict):
        return {inflection.camelize(k, False): to_camel(v) for k, v in obj.items()}
    if isinstance(obj, list):
        return [to_camel(v) for v in obj]
    return obj

to_camel({'user_id': 1, 'shipping_address': {'zip_code': '10001'}})
# {'userId': 1, 'shippingAddress': {'zipCode': '10001'}}

You have to write the recursion yourself; inflection only handles single strings. Keys that are already camelCase pass through unchanged, but a key containing a dot such as 'user.profile_name' becomes 'user.profileName', which is rarely what you want. pyhumps does this whole function for you if it is all you need.

Derive a table name from a class namemodel-to-table-name

import inflection

inflection.tableize('RawScaledScorer')   # 'raw_scaled_scorers'
inflection.tableize('fancyCategory')     # 'fancy_categories'
inflection.tableize('Person')            # 'people'

class Model:
    @classmethod
    def table_name(cls) -> str:
        return inflection.tableize(cls.__name__)

tableize is underscore followed by pluralize on the whole string, so it inherits every plural bug: a model named Foot maps to the table foots. Pin the mapping explicitly for any name you are not sure about rather than discovering it after the migration ran.

Guess plural and singular formspluralize-and-singularize

import inflection

inflection.pluralize('post')      # 'posts'
inflection.pluralize('analysis')  # 'analyses'
inflection.pluralize('sheep')     # 'sheep'   (in UNCOUNTABLES)
inflection.pluralize('child')     # 'children' (hardcoded irregular)

inflection.singularize('indices') # 'index'
inflection.singularize('news')    # 'news'

Verified failures to know about: foot gives foots, tooth gives tooths, goose gives gooses, criterion gives criterions, and cow gives kine because the Rails irregular list says so. singularize('media') returns 'medium'. Neither function is idempotent, and they are not inverses of each other.

Teach it a word it gets wrongcustom-plural-rule

import inflection

# insert at 0: rules are tried in order and the first match wins
inflection.PLURALS.insert(0, (r'(?i)(f)oot$', r'\1eet'))
inflection.SINGULARS.insert(0, (r'(?i)(f)eet$', r'\1oot'))
inflection.UNCOUNTABLES.add('staff')

inflection.pluralize('foot')     # 'feet'
inflection.pluralize('staff')    # 'staff'

Appending instead of inserting does nothing, because the catch-all rules at the bottom of the list already matched. These are process-global mutations shared with every other importer of inflection, so do it once at startup in an application, never inside a library you publish. If you need more than two or three of these, you have outgrown the package.

Turn a field name into UI texthuman-readable-labels

import inflection

inflection.humanize('employee_salary')  # 'Employee salary'
inflection.humanize('author_id')        # 'Author'  (strips trailing _id)
inflection.titleize('raiders_of_the_lost_ark')  # 'Raiders Of The Lost Ark'
inflection.titleize('x-men: the last stand')   # 'X Men: The Last Stand'

humanize drops a trailing _id, which is what you want for a foreign key label and wrong if your column is genuinely named national_id. It also lowercases everything before capitalising the first character, so humanize('HTTPStatus') returns 'Httpstatus'. titleize capitalises every word including articles, which no style guide agrees with.

Build a slug from a titleurl-slugs

import inflection

inflection.parameterize('Donald E. Knuth')       # 'donald-e-knuth'
inflection.parameterize('Hello, World!', '_')    # 'hello_world'

parameterize calls transliterate first, which means any character without an ASCII decomposition is deleted rather than replaced. A title in Cyrillic or Chinese produces an empty slug, and 'Strasse' spelled with the sharp s comes out as 'strae'. If titles come from users, use python-slugify, which handles this and also caps length and strips stopwords.

Strip accents from textascii-approximation

import inflection

inflection.transliterate('alamolo')   # unchanged
inflection.transliterate('Ærøskøbing')  # 'rskbing'  <- characters dropped
inflection.transliterate('Привет')      # ''         <- everything dropped

This is NFKD normalisation plus encode('ascii', 'ignore') in two lines, and the docstring is honest that unmapped characters are ignored. That behaviour is fine for stripping accents off Western European text and destructive for anything else. Unidecode maps Cyrillic, Greek and CJK to readable ASCII instead of deleting it.

Render positions as 1st, 2nd, 3rdordinal-numbers

import inflection

inflection.ordinalize(1)     # '1st'
inflection.ordinalize(11)    # '11th'
inflection.ordinalize(1002)  # '1002nd'
inflection.ordinal(3)        # 'rd'   (suffix only)

ordinal returns just the suffix, ordinalize returns the number with it attached, and mixing them up is a common typo. Both take abs() of the input, so ordinalize(-3) returns '-3rd'. English only; there is no locale hook.

Wrap the calls so bad input cannot crash yousafe-wrapper

import inflection

PLURAL_OVERRIDES = {'foot': 'feet', 'tooth': 'teeth', 'goose': 'geese',
                    'criterion': 'criteria', 'cow': 'cows'}

def plural(word: str) -> str:
    if not word:
        return word
    return PLURAL_OVERRIDES.get(word.lower(), inflection.pluralize(word))

def lower_camel(word: str) -> str:
    return inflection.camelize(word, False) if word else word

An explicit override dict beats mutating the global rule lists: it is visible in review, it does not leak into other packages, and it is trivial to test. The empty-string guard on camelize is not optional, because the lowercase branch indexes string[0] directly.

Pin the conversions your schema depends ontest-the-mapping

import pytest
import inflection

EXPECTED = {
    'Person': 'people',
    'Category': 'categories',
    'Analysis': 'analyses',
    'Status': 'statuses',
}

@pytest.mark.parametrize('model,table', EXPECTED.items())
def test_table_names(model, table):
    assert inflection.tableize(model) == table

Worth doing once for every entity name in your system. The value is not catching a regression in inflection, which has not changed since 2020, but catching the moment someone adds a model whose plural the rules get wrong, before it becomes a table name in production.

Check whether you actually need the bigger librarycompare-with-inflect

# pip install inflect
import inflect, inflection

p = inflect.engine()
for word in ['foot', 'tooth', 'goose', 'criterion', 'cow', 'person']:
    print(word, inflection.pluralize(word), p.plural(word))
# foot foots feet
# tooth tooths teeth
# goose gooses geese
# criterion criterions criteria
# cow kine cows
# person people people

Run this over your own vocabulary before deciding. inflect is a much larger dependency with a slower engine and it also does articles and number-to-words, so if the only divergence is two entity names, an override dict on top of inflection is the cheaper answer.

Alternatives

PackageRegistryPick it when
inflectPyPIYou pluralise real English text and need irregulars, indefinite articles, and numbers spelled out as words
pyhumpsPyPIYou only convert case styles and want it applied recursively through nested dicts and lists of API payloads
python-slugifyPyPIThe job is URL slugs and you need real Unicode handling, stopword removal and length limits rather than parameterize's ASCII drop
UnidecodePyPIYou need non-Latin scripts turned into readable ASCII instead of deleted