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

python-slugify

python-slugify turns arbitrary text into URL-safe slugs. It decodes HTML entities, transliterates non-Latin characters into ASCII through Unidecode or text-unidecode, strips everything that is not a letter, digit, or dash, collapses repeated separators, and lowercases the result. One function, slugify(), with about a dozen keyword arguments for separator, length limits, stopwords, replacement rules, and case handling. It also installs a slugify command you can pipe text through.

Verdict

The default answer for slugs in Python, and it earns it: one function, sensible options, real transliteration. Pin your transliteration backend and check that text-unidecode's GPL dual license clears your compliance rules before shipping.

API stability5/5slugify() has kept the same call shape since the 4.x line; 8.0.0's only visible change was preferring Unidecode when installed, and the argument list has been additive ever since, so upgrades are usually a version bump and nothing else.
Docs3/5The README is the only documentation and it is a wall of assertEqual examples pulled from the test suite, which is genuinely useful for seeing exact output but gives no guidance on picking a transliteration backend or handling empty results.
Maintenance3/58.0.4 shipped February 2024 and nothing has been released since, but the repo is alive: commits through April 2026 add Python 3.14 support and drop 3.9, with 6 open issues and 11 open PRs. Work happens in main and does not reach PyPI.
Ecosystem5/520.8M weekly downloads with Airflow, Great Expectations, and a long tail of CMS and static-site tooling depending on it; if a Python project makes URLs from titles, this is usually already in the lockfile.

Use it if

  • You need readable URL slugs from user-supplied titles in any language, including Cyrillic, Greek, and CJK text that has to survive as ASCII
  • Django's built-in django.utils.text.slugify is not enough because it drops non-Latin characters entirely instead of transliterating them
  • You need truncation that respects word boundaries: max_length plus word_boundary gives you a slug that ends on a whole word rather than mid-syllable
  • You want domain-specific rewrites before transliteration, like turning % into percent or | into or, via the replacements argument
  • You want the same slugging logic available from a shell script or a Makefile through the bundled slugify CLI
Skip it if

Setup reality

pip install python-slugify is pure Python with one required dependency, text-unidecode, so it installs everywhere without wheels or compilers. Two things surprise people. First, the import name is slugify, not python_slugify, and there is a completely different PyPI package literally named slugify, so an install from memory can give you the wrong library. Second, the extra python-slugify[unidecode] swaps the transliteration backend, and so does anything else in your environment that installs Unidecode, because the module does a try-import of unidecode and only falls back to text_unidecode. Pin whichever backend you want explicitly if slug stability matters, because there is no option to choose it at call time.

Patterns

Slug a titlebasic-slug

from slugify import slugify

slugify("Hello, World! -- 2026")   # 'hello-world-2026'
slugify("C'est déjà l'été.")       # 'c-est-deja-l-ete'
slugify("Компьютер")               # 'kompiuter'
slugify("影師嗎")                  # 'ying-shi-ma'

The import is from slugify, not from python_slugify. Apostrophes become separators, so O'Brien turns into o-brien rather than obrien.

Keep the original script instead of transliteratingkeep-unicode

slugify("影師嗎", allow_unicode=True)   # '影師嗎'
slugify("Компьютер", allow_unicode=True)  # 'компьютер'
slugify("i love 🦄", allow_unicode=True)   # 'i-love'

allow_unicode keeps letters but still drops emoji and symbols. Modern browsers show percent-encoded UTF-8 paths fine, but anything reading your logs or analytics will see the encoded form.

Truncate a slug without cutting a word in halflimit-length

slugify("jaja---lol-méméméoo--a", max_length=9)
# 'jaja-lol'

slugify("jaja---lol-méméméoo--a", max_length=15, word_boundary=True)
# 'jaja-lol-a'

slugify("one two three four", max_length=12, word_boundary=True, save_order=True)
# 'one-two'

Without save_order the truncator will pull a shorter later word forward to fill the budget, so 'one two three four' becomes 'one-two-four'. Set save_order=True when the slug must read in source order.

Use underscores or dots instead of dashescustom-separator

slugify("Hello World", separator="_")   # 'hello_world'
slugify("Hello World", separator=".")   # 'hello.world'
slugify("Hello World", separator="")    # 'helloworld'

The separator is applied after the dash-based cleanup, so an input that already contains dashes still collapses through the dash path first.

Strip filler words out of the slugdrop-stopwords

slugify(
    "the quick brown fox jumps over the lazy dog",
    stopwords=["the", "over"],
)
# 'quick-brown-fox-jumps-lazy-dog'

slugify("thIs Has a stopword Stopword", stopwords=["Stopword"], lowercase=False)
# 'thIs-Has-a-stopword'

Stopword matching is case-insensitive when lowercase stays on, but with lowercase=False you match exactly, so 'Stopword' removes only the capitalised occurrence.

Rewrite symbols into words before sluggingreplacement-rules

slugify("10 | 20 %", replacements=[["|", "or"], ["%", "percent"]])
# '10-or-20-percent'

slugify("C# and F#", replacements=[["#", "sharp"]])
# 'csharp-and-fsharp'

Replacements run before transliteration, so they are your escape hatch for characters Unidecode would otherwise delete outright, like # and +.

Apply German, Cyrillic, or Greek conventionslanguage-pre-translations

from slugify import slugify
from slugify.special import GERMAN, CYRILLIC, GREEK

slugify("Über Öl Ärger")                        # 'uber-ol-arger'
slugify("Über Öl Ärger", replacements=GERMAN)   # 'ueber-oel-aerger'

These tables ship in slugify.special and are just replacement lists, so you can concatenate them or extend one with your own pairs. Without them the umlaut simply loses its diaeresis.

Allow characters the default pattern stripscustom-character-class

slugify("___This is a test___", regex_pattern=r"[^-a-z0-9_]+")
# '___this-is-a-test___'

slugify("file v2.1", regex_pattern=r"[^-a-z0-9.]+")
# 'file-v2.1'

regex_pattern describes the disallowed characters, which is the opposite of what it meant before 6.0.1. Remember to include uppercase in the class if you also pass lowercase=False.

Keep the original casingpreserve-case

slugify("Hello World", lowercase=False)   # 'Hello-World'

Case-sensitive slugs make URLs that behave differently on case-insensitive filesystems and CDNs; if you need them, normalise on lookup as well as on write.

Slug text that still contains HTML entitieshtml-entities

slugify("foo & bar & baz")              # 'foo-bar-baz'
slugify("foo & bar", entities=False)        # 'foo-amp-bar'
slugify("Ž", decimal=True)                 # 'z'

Entity decoding is on by default, which is what you want for scraped titles but surprising for text that legitimately contains '&'. Turn off entities, decimal, or hexadecimal individually.

Make the slug unique against existing rowsunique-slug

from slugify import slugify

def unique_slug(title: str, exists) -> str:
    base = slugify(title, max_length=60, word_boundary=True) or "item"
    candidate, n = base, 1
    while exists(candidate):
        suffix = f"-{n}"
        candidate = f"{base[: 60 - len(suffix)]}{suffix}"
        n += 1
    return candidate

The `or "item"` matters: slugify('!!!') and slugify('🦄') both return an empty string, which becomes a URL that routes to your index page instead of the record.

Slug text from the shellcli-usage

$ slugify Taking input from the command line
taking-input-from-the-command-line

$ echo "Taking input from STDIN" | slugify --stdin
taking-input-from-stdin

$ slugify --stopwords the in a hurry -- the quick brown fox in a hurry
quick-brown-fox

Multi-valued flags such as --stopwords and --replacements swallow following words, so you need the bare -- separator before the text you actually want slugged.

Alternatives

PackageRegistryPick it when
awesome-slugifyPyPIYou want per-language slug functions (slugify_ru, slugify_de) rather than passing replacement tables yourself.
unidecodePyPIYou only need ASCII transliteration and will write the two-line regex cleanup yourself.
djangoPyPIYou are already on Django and your titles are Latin script: django.utils.text.slugify is built in and dependency-free.