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.

I'm currently available for freelance work. If you know someone who would benefit from my expertise, please direct them to my work page, or email me their contact details.


Email me if you have any questions about this post.

Subscribe to the RSS feed to keep updated.