Break django-admin commands into subcommands
Learn how to organize Django management commands into intuitive subcommand namespaces, making your CLI cleaner and more developer-friendly!
Published: 08 Mar, 2025
I previously wrote about how to build a Docker-like CLI using Python’s argparse. Since Django uses argparse
for manage.py
commands under the hood, I use the same pattern when building custom CLIs in Django projects.
But why should you break down your commands into subcommands? Let’s take some example commands in a todo app:
- When there’s an overdue task, send the user an email.
- Send the user an email containing a weekly report.
- Send a user an email when a new task gets assigned to them.
You could use the following commands to perform those actions:
send_overdue_email
send_weekly_report
send_newly_assigned_email
All cool. But what if you found that the following works better:
emails send_overdue
reports send_weekly
emails send_newly_assigned
This allows you to group commands together, providing a more intuitive CLI for current and future developers.
How to Do It
First, you’ll create your BaseCommand
as usual:
from django.core.management import BaseCommand
class Command(BaseCommand):
def handle(self, *args, **options):
pass
Then, you need subparsers:
from django.core.management import BaseCommand
class Command(BaseCommand):
def create_parser(self, prog_name, subcommand, **kwargs):
parser = super().create_parser(prog_name, subcommand, **kwargs)
subparsers = parser.add_subparsers()
return parser
def handle(self, *args, **options):
pass
Now you can attach any parser to the subparsers. For example:
def add_emails_parser(parent):
parser = parent.add_parser("emails", help="Commands for managing email communications.")
parser.set_defaults(handler=parser.print_help)
...
def create_parser(self, prog_name, subcommand, **kwargs):
parser = super().create_parser(prog_name, subcommand, **kwargs)
subparsers = parser.add_subparsers()
# Attach subparsers here
add_emails_parser(subparsers)
return parser
...
This will add the emails
namespace to your manage.py
commands. You can attach actions to this namespace, for example:
def add_send_overdue_action(parent):
parser = parent.add_parser("send_overdue", help="Command for sending overdue emails")
parser.add_argument("user_id", type=int)
def handle(options):
user_id = options['user_id']
print(f"Sending overdue email to {user_id}")
parser.set_defaults(handler=handle)
def add_emails_parser(parent):
parser = parent.add_parser("emails", help="Commands for managing email communications.")
parser.set_defaults(handler=parser.print_help)
subparsers = parser.add_subparsers()
add_send_overdue_action(subparsers)
To use the subparsers, implement the handle
method in your Command
:
def handle(self, *args, **options):
handler = options["handler"]
handler(options)
The handler
option will be populated thanks to our set_defaults
calls from earlier.
Here’s the full example:
from django.core.management import BaseCommand
def add_send_overdue_action(parent):
parser = parent.add_parser("send_overdue", help="Command for sending overdue emails")
parser.add_argument("user_id", type=int)
def handle(options):
user_id = options['user_id']
print(f"Sending overdue email to {user_id}")
parser.set_defaults(handler=handle)
def add_emails_parser(parent):
parser = parent.add_parser("emails", help="Commands for managing email communications.")
parser.set_defaults(handler=parser.print_help)
subparsers = parser.add_subparsers()
add_send_overdue_action(subparsers)
class Command(BaseCommand):
def create_parser(self, prog_name, subcommand, **kwargs):
parser = super().create_parser(prog_name, subcommand, **kwargs)
subparsers = parser.add_subparsers()
add_emails_parser(subparsers)
return parser
def handle(self, *args, **options):
handler = options["handler"]
handler(options)
If you type python manage.py emails send_overdue 1
, you’ll see a message printed.
Conclusion
You’ll typically structure your commands across different files, for example:
- management
- __init__.py
- commands
- __init__.py
- _overdue_action.py
- myapp.py
Django won’t turn files that start with an underscore into a command.
With the example above, this structure will allow a command such as python manage.py myapp emails send_overdue 1
,
which is great for breaking down commands even further for each Django app.
Now you have a clean way to manage commands under namespaces.