Skip to main content

slugany

CI PyPI version Python License: MIT Coverage Tests mypy ruff

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 --strict clean · ruff clean · 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 preservationallow_unicode=True keeps 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
  • Idempotentslugify(slugify(x)) == slugify(x), guaranteed and tested
  • Fully typed — type hints on every public API, py.typed marker (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

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

slugany-1.0.0.tar.gz (49.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

slugany-1.0.0-py3-none-any.whl (22.3 kB view details)

Uploaded Python 3

File details

Details for the file slugany-1.0.0.tar.gz.

File metadata

  • Download URL: slugany-1.0.0.tar.gz
  • Upload date:
  • Size: 49.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for slugany-1.0.0.tar.gz
Algorithm Hash digest
SHA256 0c96be50c317c65a9a7e0a1fc946daf265419320cbc7a7bfd227d623e41b6640
MD5 774f7bb739999fd3386ae67489bc0a61
BLAKE2b-256 09e88baeeb6f65882c5c9c8e34fd390ee41e92bedf6c447600141b9d686ff4fc

See more details on using hashes here.

Provenance

The following attestation bundles were made for slugany-1.0.0.tar.gz:

Publisher: release.yml on MathiasPaulenko/slugany

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file slugany-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: slugany-1.0.0-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

Hashes for slugany-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3b1474a1d8a92c55eda5bd21596be4a703ad15a474cc6269f2d0b50246662c1d
MD5 64681ecfdc1d177813bf12c0241b2b9e
BLAKE2b-256 46d434910178d87ab56702bb56698c6979cd9c94e7f0487a9e266a3584377380

See more details on using hashes here.

Provenance

The following attestation bundles were made for slugany-1.0.0-py3-none-any.whl:

Publisher: release.yml on MathiasPaulenko/slugany

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.0.3

2 files

1.0.2

2 files

1.0.1

2 files

This release

1.0.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page