Logo

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.


Email me if you have any questions about this post.

Subscribe to the RSS feed to keep updated.