Django cookbook · Beginner

Write an idempotent management command

Operational scripts (cleanups, backfills) are run ad hoc from shells and behave differently each time.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Put the script in a management command with arguments, a dry-run mode, batching and a clear summary. Make it idempotent so re-running after a failure is safe.

Solution

# shop/management/commands/expire_carts.py
from datetime import timedelta

from django.core.management.base import BaseCommand
from django.utils import timezone

from shop.models import Cart


class Command(BaseCommand):
    help = "Delete carts that haven't been updated for a number of days."

    def add_arguments(self, parser):
        parser.add_argument("--days", type=int, default=30)
        parser.add_argument("--dry-run", action="store_true")
        parser.add_argument("--batch-size", type=int, default=1000)

    def handle(self, *args, days, dry_run, batch_size, **options):
        cutoff = timezone.now() - timedelta(days=days)
        stale = Cart.objects.filter(updated_at__lt=cutoff, order__isnull=True)
        total = stale.count()
        if dry_run:
            self.stdout.write(f"Would delete {total} carts older than {cutoff:%Y-%m-%d}")
            return
        deleted = 0
        while ids := list(stale.values_list("id", flat=True)[:batch_size]):
            deleted += Cart.objects.filter(id__in=ids).delete()[0]
        self.stdout.write(self.style.SUCCESS(f"Deleted {deleted} rows for {total} carts"))

Pitfalls

  • One giant delete() on millions of rows, locking tables for a long time.
  • Printing with print() instead of self.stdout, which breaks call_command output capture in tests.
  • No dry-run for destructive commands.

Variations

  • Schedule it with cron, Kubernetes CronJobs or a periodic task, and alert on failures.
  • Test it with call_command("expire_carts", days=1, stdout=out).

Further reading: Custom management commands

Reference: https://docs.djangoproject.com/en/6.0/howto/custom-management-commands/

More Django recipes

Was this page helpful?