Django, Actually · يونيو 2026 · 3 min read

Creating custom Django commands

Whether you’re a seasoned developer or just starting out, knowing how to create custom Django commands can significantly increase your productivity. Custom commands are helpful for adding actions (reoccurring or not) to your Django app.

Creating Custom Commands

Custom commands let you extend the python manage.py interface to build your own background tools, data importers, maintenance scripts, and more.

In this article we'll create a custom command that imports articles from Medium into a portfolio database.

Why Custom Commands Matter

Custom management commands turn python manage.py into your project's personal Swiss Army knife. Instead of writing one-off scripts or cron jobs outside Django, you get:

Project Structure

For this example you'll need the companion importer module — grab it here: Article Importers. Once added, your app directory should look like this:

writing/
    __init__.py
    models.py
    importers/
    management/
        __init__.py
        commands/
            __init__.py
            import_articles.py
    tests.py
    views.py

Django automatically discovers custom commands inside the management/commands/ directory — no registration needed.

Implementing the Command

Edit management/commands/import_articles.py:

from django.core.management.base import BaseCommand, CommandError
from writing.models import Series


class Command(BaseCommand):
    help = 'Import articles from Medium into the portfolio database.'

    def add_arguments(self, parser):
        parser.add_argument(
            'source', choices=['medium'],
            help='Platform to import from.',
        )
        parser.add_argument(
            '--username', default='sifusherif',
            help='Platform username (default: sifusherif).',
        )
        parser.add_argument(
            '--series', dest='series_slug', default=None,
            help='Slug of the Series to assign imported articles to.',
        )
        parser.add_argument(
            '--dry-run', action='store_true',
            help='Fetch and display what would be imported without writing to the database.',
        )
        parser.add_argument(
            '--overwrite', action='store_true',
            help='Re-import and overwrite articles that were previously imported.',
        )

    def handle(self, *args, **options):
        series = None
        if options['series_slug']:
            try:
                series = Series.objects.get(slug=options['series_slug'])
            except Series.DoesNotExist:
                raise CommandError(
                    f"Series with slug '{options['series_slug']}' does not exist. "
                    f"Create it in the admin first."
                )

        if options['source'] == 'medium':
            from writing.importers.medium import MediumImporter
            importer = MediumImporter(username=options['username'])
        else:
            raise CommandError(f"Unknown source: {options['source']}")

        self.stdout.write(
            f"Fetching articles from Medium (@{options['username']})…"
        )

        try:
            imported, skipped = importer.import_all(
                series=series,
                dry_run=options['dry_run'],
                overwrite=options['overwrite'],
            )
        except RuntimeError as exc:
            raise CommandError(str(exc))

        is_dry_run = options['dry_run']
        prefix = '[DRY RUN] ' if is_dry_run else ''

        if is_dry_run:
            self.stdout.write('\nArticles that would be imported:')
            for a in imported:
                self.stdout.write(f'  • {a.title} ({a.published_at})')
        else:
            for a in imported:
                self.stdout.write(f'  ✓ {a.title}')

        self.stdout.write(self.style.SUCCESS(
            f'\n{prefix}Imported: {len(imported)}  |  Skipped (already exist): {len(skipped)}'
        ))

        if skipped and not options['overwrite']:
            self.stdout.write(
                self.style.WARNING(
                    'Use --overwrite to re-import skipped articles.'
                )
            )

Running the Command

# Basic usage
python manage.py import_articles medium

# With options
python manage.py import_articles medium --username=yourusername --series=python-deep-dives --overwrite

# Preview what would be imported without touching the database
python manage.py import_articles medium --dry-run

What the Code Does


Some Built-In Django Commands Worth Knowing

python manage.py check --deploy

Runs Django's deployment checklist in one shot — checks HTTPS settings, DEBUG flags, security headers, and more. Run this before every production deploy.

python manage.py showmigrations

Lists all migrations across every app alongside their applied status. Useful when debugging migration state across environments or after a botched rollback.

python manage.py changepassword <username>

Resets a user's password directly from the shell — no need to create a new superuser or touch the admin UI. Handy when you're locked out.

More in Django, Actually
6 Use Cases for Javascript Proxies
Streamlining Your Dependencies with ni: A Beginner’s Tutorial to the Package Manager of the Future.
Using CSS to Implement a Dark Mode Theme for Your Websites