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.
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.
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
- License review is strict: python-slugify is MIT but its required dependency text-unidecode is dual-licensed under the Artistic License and the GPL, which some corporate scanners flag on redistributed binaries
- You need deterministic output across machines: since 8.0.0 the library uses Unidecode when it happens to be importable and text-unidecode otherwise, and the two disagree on thousands of code points, so an unrelated dependency pulling in Unidecode can silently change your slugs
- You are already on Django and only handle Latin-script text: django.utils.text.slugify is in the box, has no dependencies, and does the same job for that input
- You only need to strip accents from European text: unicodedata.normalize('NFKD', s) plus a regex is about four lines and adds nothing to your dependency tree
- You expect slugs to be unique or non-empty: slugify('!!!') returns an empty string and two different titles routinely collide, so uniqueness is still your problem to solve
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 candidateThe `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-foxMulti-valued flags such as --stopwords and --replacements swallow following words, so you need the bare -- separator before the text you actually want slugged.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| awesome-slugify | PyPI | You want per-language slug functions (slugify_ru, slugify_de) rather than passing replacement tables yourself. |
| unidecode | PyPI | You only need ASCII transliteration and will write the two-line regex cleanup yourself. |
| django | PyPI | You are already on Django and your titles are Latin script: django.utils.text.slugify is built in and dependency-free. |