python 52 lines · 3 tabs

Auto-Generate Unique Slugs in Django with a pre_save Signal and Model Method

Shared by codesnips Jul 2026
3 tabs
from django.db import models
from django.utils.text import slugify


class Article(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220, unique=True, blank=True)
    body = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.title

    def build_unique_slug(self):
        base = slugify(self.title)[:200] or "article"
        candidate = base
        suffix = 2
        qs = Article.objects.all()
        if self.pk:
            qs = qs.exclude(pk=self.pk)
        while qs.filter(slug=candidate).exists():
            candidate = "{0}-{1}".format(base, suffix)
            suffix += 1
        return candidate
3 files · python Explain with highlit

This snippet shows a common Django task: generating a URL-friendly slug from a title and guaranteeing it is unique, without repeating the logic every time an object is saved. The work is split so the model owns the slug-building rules while a pre_save signal enforces them automatically on every write.

In articles/models.py, Article exposes a build_unique_slug method rather than baking the logic into save. It uses slugify to normalize the title, then loops, appending a numeric suffix (-2, -3, ...) until it finds a value not already taken. The uniqueness check excludes the current row via exclude(pk=self.pk) so an update that keeps the same slug does not collide with itself. The method is deliberately side-effect-free — it returns a string instead of assigning it — which makes it easy to unit test and reuse. Keeping unique=True on the field means the database is the final arbiter, so even a race that slips past the Python check still fails loudly instead of silently duplicating.

The term "debounce" here means the slug is only recomputed when it actually needs to be: an empty slug, or a title change on a persisted object. In articles/signals.py, the ensure_article_slug receiver is wired to pre_save. It computes title_changed by loading the old row and comparing, but only when the instance already has a pk. New objects, or objects whose slug is blank, always get a fresh slug; existing objects only regenerate when their title moved. This avoids churning the slug — and breaking existing URLs — on unrelated field updates.

Computing the slug in pre_save rather than save keeps the behavior consistent no matter how the object is written, including bulk-ish flows and admin saves that call save directly. articles/apps.py connects the receiver inside ready(), the canonical place to register signals so they load exactly once. The main trade-off is implicitness: signals hide behavior from the model's own save, so a developer reading Article alone might not see where slugs come from — hence the explicit method as documentation. For heavy-write systems the read-back in the signal adds a query per update, which a Meta.constraints unique index and retry-on-IntegrityError can complement.


Related snips

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semantic HTML Example</title>

Semantic HTML5 elements and accessibility best practices

html html5 semantics
by Alex Chang 2 tabs
python
class Product(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    cost = models.DecimalField(max_digits=10, decimal_places=2)
    margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)

Django model signals vs overriding save

django python models
by Priya Sharma 2 tabs
python
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [

Django URL namespacing and reverse lookups

django python urls
by Priya Sharma 3 tabs
ruby
# Vulnerable: user input is concatenated directly into SQL.
email = params[:email]
password = params[:password]

sql = "SELECT * FROM users WHERE email = '#{email}' AND password_hash = '#{password}'"
user = ActiveRecord::Base.connection.execute(sql).first

SQL injection prevention with unsafe and safe query patterns

sql-injection owasp database
by Kai Nakamura 3 tabs
python
import graphene
from graphene_django import DjangoObjectType
from blog.models import Post, Comment


class PostType(DjangoObjectType):

Django GraphQL with Graphene

django python graphql
by Priya Sharma 2 tabs
ruby
class AddSettingsToAccounts < ActiveRecord::Migration[7.1]
  disable_ddl_transaction!

  def change
    add_column :accounts, :settings, :jsonb, null: false, default: {}

Postgres JSONB Partial Index for Feature Flags

rails postgres jsonb
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Auto-Generate Unique Slugs in Django with a pre_save Signal and Model Method — share card
Link copied