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.
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.
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
- You are pluralising arbitrary English. The rules miss common irregulars outright: foot becomes foots, tooth becomes tooths, goose becomes gooses, criterion becomes criterions. Meanwhile cow becomes kine, because the Rails list says so. inflect has a much larger rule set and handles these
- You need round-trips to be stable. pluralize and singularize are separate rule lists, not inverses: singularize('media') returns 'medium', so a value that arrives already plural can come back as something you did not put in. camelize(underscore('IOError')) returns 'IoError', which is the documented behaviour and still surprising
- You have non-English or non-Latin text. transliterate() is NFKD normalisation followed by encode('ascii', 'ignore'), so Cyrillic and CJK input come back as an empty string, and the German sharp s in Strasse is silently deleted rather than expanded. Unidecode does this properly
- You want to extend it. There is no public API for registering rules: _irregular is private, and adding a plural means appending a regex tuple to the module-global PLURALS list, which mutates state for every other library in the process that also imported inflection
- You expect maintenance. 0.5.1 was published in August 2020 and the repository was last pushed in August 2023. It is not archived and it is not broken, but nothing has shipped in years, the PyPI classifier still says Development Status 4 - Beta, and there are 23 open issues plus 9 open PRs sitting there
- You only need one conversion. camelize and underscore are two regex substitutions each; if that is all you want, copy them and skip the dependency
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 droppedThis 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 wordAn 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) == tableWorth 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 peopleRun 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
| Package | Registry | Pick it when |
|---|---|---|
| inflect | PyPI | You pluralise real English text and need irregulars, indefinite articles, and numbers spelled out as words |
| pyhumps | PyPI | You only convert case styles and want it applied recursively through nested dicts and lists of API payloads |
| python-slugify | PyPI | The job is URL slugs and you need real Unicode handling, stopword removal and length limits rather than parameterize's ASCII drop |
| Unidecode | PyPI | You need non-Latin scripts turned into readable ASCII instead of deleted |