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.

545 tests · 100% coverage · mypy --strict clean · ruff clean · 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=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
  • Idempotent — slugify(slugify(x)) == slugify(x), guaranteed and tested
  • Fully typed — type hints on every public API, py.typed marker (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

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)

Source distribution for slugany 1.1.0
File Size Uploaded
slugany-1.1.0.tar.gz 53.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for slugany 1.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page