slugany
A multi-language slugify library with zero runtime dependencies. MIT-licensed, fully typed, and audited for idempotency — a clean alternative to python-slugify with no GPL baggage.
545 tests · 100% coverage ·
mypy --strictclean ·ruffclean · 1,000+ randomized contract checks passed
Why slugany?
python-slugify is the de facto standard, but it drags in text-unidecode (GPL) and over 1,000 lines of code. slugany was built from scratch to be:
| python-slugify | unicode-slugify | slugany | |
|---|---|---|---|
| Runtime deps | text-unidecode (GPL) |
unidecode (GPL) |
Zero |
| License | GPL | GPL | MIT |
| Languages | Limited | Limited | Built-in: es, pt, de, fr, it |
| Caching | No | No | lru_cache built-in (512) |
| Typing | Partial | Partial | Fully typed, py.typed marker |
| Core size | ~1,000+ lines | ~800 lines | ~1,000 lines |
| Idempotency | Not guaranteed | Not guaranteed | Guaranteed & tested |
| CLI | Separate package | No | Built-in |
| Style presets | No | No | 8 built-in |
| Emoji handling | No | No | strip, text, keep |
| Confusables | No | No | Cyrillic + Greek |
| CSS-safe | No | No | Built-in |
| Smart punctuation | No | No | Built-in |
| HTML entities | No | No | Built-in |
| Fallback | No | No | Built-in |
Features
- Zero runtime deps — only the Python standard library
- Multi-language transliteration — Spanish, Portuguese, German, French, Italian
- 8 case styles — kebab, snake, camel, pascal, dot, train, filename, url
- Smart punctuation — normalizes curly quotes, em-dashes, NBSP, zero-width chars, bullets
- HTML entity decoding —
&→&before processing - Emoji handling — strip, keep, or convert to text
- Confusable detection — Cyrillic homoglyphs → Latin equivalents
- Stopwords removal — filter out common words
- Custom replacements — pre- and post-pipeline string substitution
- CSS-safe slugs — prefix digit-leading slugs with
s- - Max length with word boundaries — truncate without breaking words
- Unicode preservation —
allow_unicode=Truekeeps non-ASCII chars - Fallback for empty slugs — never get an empty string
- Built-in
lru_cache— results cached automatically (maxsize=512) - CLI with stdin support — pipe text directly:
echo "text" | slugany - Idempotent —
slugify(slugify(x)) == slugify(x), guaranteed and tested - Fully typed — type hints on every public API,
py.typedmarker (PEP 561) - ~1,000 lines core — auditable, no bloat
Installation
pip install slugany
Requires Python 3.11+. No runtime dependencies.
Quickstart
from slugany import slugify
slugify("¡Hola Mundo!") # "hola-mundo"
slugify("Café résumé naïve") # "cafe-resume-naive"
slugify("Ñandú coração") # "nandu-coracao"
slugify("Über Straße", lang="de") # "ueber-strasse"
slugify("Hello 🎉 World") # "hello-world"
CLI
# Basic usage
slugany "Hello World"
# hello-world
# Pipe from stdin
echo "Café" | slugany
# cafe
# Case styles
slugany "hello world" --style camel
# helloWorld
slugany "hello world" --style train
# Hello-World
# Truncation with word boundary
slugany "hello-world-foo-bar" --max-length 15 --word-boundary
# hello-world-foo
# Batch mode (one slug per line)
slugany --batch < input.txt
# CSS-safe slugs
slugany "123 main st" --css-safe
# s-123-main-st
Case Styles
slugify("hello world", style="kebab") # "hello-world"
slugify("hello world", style="snake") # "hello_world"
slugify("hello world", style="camel") # "helloWorld"
slugify("hello world", style="pascal") # "HelloWorld"
slugify("hello world", style="dot") # "hello.world"
slugify("hello world", style="train") # "Hello-World"
slugify("Hello World", style="filename") # "Hello-World"
slugify("hello world", style="url") # "hello-world"
Languages
Built-in transliteration tables for five languages:
slugify("España", lang="es") # "espana"
slugify("Coração", lang="pt") # "coracao"
slugify("Über Straße", lang="de") # "ueber-strasse"
slugify("Cœur", lang="fr") # "coeur"
slugify("Caffè", lang="it") # "caffe"
Advanced
from slugany import slugify, slugify_batch, is_slug
# Stopwords — remove common words
slugify("the quick brown fox", stopwords=["the", "fox"]) # "quick-brown"
# Custom replacements — substitute before and after transliteration
slugify("hello world", replacements={"hello": "hi"}) # "hi-world"
slugify("Straße", replacements={"ß": "ss"}) # "strasse"
# Emoji handling
slugify("Hello 🎉 World", emoji_mode="strip") # "hello-world"
slugify("Hello 🎉 World", emoji_mode="text") # "hello-party-popper-world"
slugify("Hello 🎉 World", emoji_mode="keep", allow_unicode=True) # "hello-🎉-world"
# CSS-safe — prefix digit-leading slugs for CSS class names
slugify("123 main st", css_safe=True) # "s-123-main-st"
# Fallback — never get an empty string
slugify("!!!", fallback="untitled") # "untitled"
# Unicode preservation — keep non-ASCII characters
slugify("Ñandú", allow_unicode=True) # "ñandú"
# Max length with word boundary — truncate without breaking words
slugify("hello world foo bar", max_length=15, word_boundary=True) # "hello-world-foo"
# Batch processing
slugify_batch(["Hello World", "Café Résumé"]) # ["hello-world", "cafe-resume"]
# Validation
is_slug("hello-world") # True
is_slug("hello world") # False
is_slug("hello_world", separator="_") # True
is_slug("hello-wörld", allow_unicode=True) # True
# Cache inspection
from slugany import slugify
slugify.cache_info() # CacheInfo(hits=0, misses=1, maxsize=512, currsize=1)
slugify.cache_clear() # Clear the cache
Slugifier — Reusable Builder Pattern
For high-throughput scenarios, create a Slugifier once and reuse it. Config validation happens once, not per call:
from slugany import Slugifier
# Create once
s = Slugifier.style("camel", max_length=20, stopwords=["the", "a"])
# Reuse
s("The Quick Brown Fox") # "quickBrownFox"
s("A Lazy Dog") # "lazyDog"
s("Hello World") # "helloWorld"
# Inspect config
s.config # SlugConfig(style='camel', max_length=20, ...)
FastAPI / Pydantic Integration
Use slugany with Pydantic for automatic slug generation in API models:
from pydantic import BaseModel, field_validator
from slugany import slugify
class Article(BaseModel):
title: str
slug: str
@field_validator("slug", mode="before")
@classmethod
def generate_slug(cls, v: str, info) -> str:
if not v and info.data.get("title"):
return slugify(info.data["title"], style="kebab")
return slugify(v, style="kebab") if v else ""
article = Article(title="Hello World!", slug="")
print(article.slug) # "hello-world"
Slug type with Annotated
slugany ships an optional Slug type — Annotated[str, BeforeValidator(...)] — that auto-slugifies any string assigned to a field. It's available when pydantic is installed:
from pydantic import BaseModel
from slugany import Slug
class Tag(BaseModel):
name: str
slug: Slug
tag = Tag(name="Machine Learning", slug="Machine Learning")
print(tag.slug) # "machine-learning"
Or build your own constrained variant:
from typing import Annotated
from pydantic import BaseModel, StringConstraints, field_validator
from slugany import slugify
StrictSlug = Annotated[str, StringConstraints(pattern=r"^[a-z0-9]+(-[a-z0-9]+)*$")]
class Tag(BaseModel):
name: str
slug: StrictSlug
@field_validator("slug", mode="before")
@classmethod
def auto_slug(cls, v: str, info) -> str:
return slugify(v or info.data.get("name", ""))
tag = Tag(name="Machine Learning", slug="")
print(tag.slug) # "machine-learning"
FastAPI query parameter
from fastapi import FastAPI, Query
from slugany import slugify
app = FastAPI()
@app.get("/search")
async def search(q: str = Query(..., min_length=1)):
slug = slugify(q, fallback="all")
return {"query": q, "slug": slug}
deconfuse — Standalone Utility
Replace confusable Unicode homoglyphs with Latin equivalents:
from slugany import deconfuse
deconfuse("саfe") # "cafe" — Cyrillic с/а → Latin c/a
deconfuse("αβγ") # "abg" — Greek → Latin
deconfuse("Hello") # "Hello" — no change
slugify() applies deconfusion automatically. Use deconfuse() standalone when you need the raw replacement without full slugification.
Migration from python-slugify
slugany is designed as a drop-in replacement. The main difference is that all arguments are keyword-only:
# python-slugify
from slugify import slugify
slugify("Hello World", "_")
slugify("Hello World", separator="_", stopwords=["the"])
# slugany
from slugany import slugify
slugify("Hello World", separator="_")
slugify("Hello World", separator="_", stopwords=["the"])
From unicode-slugify
# unicode-slugify
from slugify import slugify
slugify("Hello World")
# slugany — same result, zero deps
from slugany import slugify
slugify("Hello World") # "hello-world"
See the migration guide for full details.
Documentation
Full documentation at mathiaspaulenko.github.io/slugany
- Basic usage
- Styles & presets
- Languages
- CLI reference
- API reference
- Slugifier
- deconfuse()
- Contracts & guarantees
- Performance
Contributing
Contributions are welcome! See CONTRIBUTING.md for development setup, PR process, and code style guidelines.
Please read our Code of Conduct before participating.
License
MIT — see LICENSE.
Metadata
Release files for slugany 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| slugany-1.1.0.tar.gz | 53.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| slugany-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 76.7 kB
Release files / slugany-1.1.0.tar.gz
| Download URL | slugany-1.1.0.tar.gz |
|---|---|
| Size | 53.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b4a6339ed5daf1e0ceaab1af740eded32841f87f2e37133cb03c3708637d2316
|
|
BLAKE2b-256 checksum How to use checksums |
60c95298eaefce05944a078ddba973f599c33371fb93f1ffa98fb75a7871b789
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / slugany-1.1.0-py3-none-any.whl
| Download URL | slugany-1.1.0-py3-none-any.whl |
|---|---|
| Size | 23.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
724aa58ef7f36bb6f812817022fbe2cf62a3d1a60493f8a4b2ea93fe39efd9bf
|
|
BLAKE2b-256 checksum How to use checksums |
33fe1f0f6dcbcf6eaa3f3f7e90b71749492a8041dedc50e564955abbb3cce0bf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log