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:
- Full access to your models, settings, and ORM
- Clean argument parsing and help text out of the box
- A consistent interface your whole team can use
- Easy scheduling via cron, GitHub Actions, Celery Beat, and so on
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
class Command(BaseCommand)— Inherits from Django's base class, which provides argument parsing, output styling, and proper integration with the management system.add_arguments(self, parser)— Defines all command-line arguments using Python'sargparse.handle(self, *args, **options)— The main logic: validates the series, initializes the importer, runs the import, and gives clear feedback on what happened.
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.