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.
478 tests · 100% coverage ·
mypy --strictclean ·ruffclean · 13,800+ randomized idempotency 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 | ~550 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 per language
- 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) - ~550 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 10 --word-boundary
# hello-world
# 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"}) # "strass"
# Emoji handling
slugify("Hello 🎉 World", emoji_mode="strip") # "hello-world"
slugify("Hello 🎉 World", emoji_mode="text") # "helloparty-popperworld"
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"
# 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
from typing import Annotated
from pydantic import BaseModel, StringConstraints
from slugany import slugify, is_slug
Slug = Annotated[str, StringConstraints(pattern=r"^[a-z0-9]+(-[a-z0-9]+)*$")]
class Tag(BaseModel):
name: str
slug: Slug
@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 s → Latin c
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
- 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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file slugany-1.0.1.tar.gz.
File metadata
- Download URL: slugany-1.0.1.tar.gz
- Upload date:
- Size: 50.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
824c384614b063e2d58e93539ddbf54c4293dc702a248ffd4918d10c38620abc
|
|
| MD5 |
782fd3d0b4bbaeb1fb0e503c3eef3ab9
|
|
| BLAKE2b-256 |
79e3931c39905057859c41f583b0e160916bf555c3694ffc59ffe6a258ff9fa3
|
Provenance
The following attestation bundles were made for slugany-1.0.1.tar.gz:
Publisher:
release.yml on MathiasPaulenko/slugany
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
slugany-1.0.1.tar.gz -
Subject digest:
824c384614b063e2d58e93539ddbf54c4293dc702a248ffd4918d10c38620abc - Sigstore transparency entry: 2529988637
- Sigstore integration time:
-
Permalink:
MathiasPaulenko/slugany@8c33e38f4a88b43bc94828aebe28798d8aa78320 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/MathiasPaulenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8c33e38f4a88b43bc94828aebe28798d8aa78320 -
Trigger Event:
push
-
Statement type:
File details
Details for the file slugany-1.0.1-py3-none-any.whl.
File metadata
- Download URL: slugany-1.0.1-py3-none-any.whl
- Upload date:
- Size: 22.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4243a77c95e0d02b65f779574012b10ff147d5c006aa7b8ef88492d836efbde
|
|
| MD5 |
c82db18c4bf3c9a4885b732e38778d47
|
|
| BLAKE2b-256 |
b4c5b6e81826b359435284c5b90e0d282a6b07bc10f626e9257dce17cd305d58
|
Provenance
The following attestation bundles were made for slugany-1.0.1-py3-none-any.whl:
Publisher:
release.yml on MathiasPaulenko/slugany
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
slugany-1.0.1-py3-none-any.whl -
Subject digest:
e4243a77c95e0d02b65f779574012b10ff147d5c006aa7b8ef88492d836efbde - Sigstore transparency entry: 2529988726
- Sigstore integration time:
-
Permalink:
MathiasPaulenko/slugany@8c33e38f4a88b43bc94828aebe28798d8aa78320 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/MathiasPaulenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8c33e38f4a88b43bc94828aebe28798d8aa78320 -
Trigger Event:
push
-
Statement type: