Join my Laravel for REST API's course on Udemy πŸ‘€

Create a custom Django manage.py command

December 19, 2020  ‐ 5Β min read

When you used Django before you undoubtedly used a subcommand of manage.py, such as runserver and migrate. You can also create custom django commands that can be invoked via manage.py. This can be useful for administrative tasks in your application. Like running a background job for example.

Create a command

Django looks for custom management commands in the folder management/commands of the apps you registered in the INSTALLED_APPS setting. The command itself is just a python file in the commands folder.

If we would have a plants app in our Django application that should have a waterplants command, to water the plants, we add a waterplants.py file to the commands folder.

So that our plants app has the following files. Just an fyi, files starting with a underscore(_) are not registered as custom management command.

$ tree plants
plants
β”œβ”€β”€ admin.py
β”œβ”€β”€ apps.py
β”œβ”€β”€ __init__.py
β”œβ”€β”€ management
β”‚   └── commands
β”‚       └── waterplants.py
β”œβ”€β”€ migrations
β”‚   └── __init__.py
β”œβ”€β”€ models.py
β”œβ”€β”€ tests.py
└── views.py

$ tree plants lol :)

If you run python manage.py help you should see your new command as available subcommand.

$ python manage.py help

Type 'manage.py help <subcommand>' for help on a specific subcommand.

Available subcommands:

...

[plants]
    waterplants

...

However, running the command at this state doesn't do a whole lot. Actually, it throws an exception:

AttributeError: module 'plants.management.commands.waterplants' has no attribute 'Command'.

What Django expects a class Command which is a subclass of BaseCommand in this waterplants.py we just created. To get rid of this exception can past in the following piece of code.

# ./plants/management/commands/waterplants.py
from django.core.management.base import BaseCommand


class Command(BaseCommand):
    help = 'Water the plants'

    def handle(self, *args, **options):
        pass

The handle method is the entry point of the command. When the command is invoked by Django the handle method will be called.

As you see we have set a help property for the Command class. This string is used in the help message for the management command if we pass the --help flag. We see that we get some optional arguments for free from the BaseCommand class.

$ python manage.py waterplants --help
usage: manage.py waterplants [-h] [--version] [-v {0,1,2,3}]
                             [--settings SETTINGS] [--pythonpath PYTHONPATH]
                             [--traceback] [--no-color] [--force-color]
                             [--skip-checks]

Water the plants

optional arguments:
  -h, --help            show this help message and exit
  --version             show program's version number and exit
  -v {0,1,2,3}, --verbosity {0,1,2,3}
                        Verbosity level; 0=minimal output, 1=normal output,
                        2=verbose output, 3=very verbose output
  --settings SETTINGS   The Python path to a settings module, e.g.
                        "myproject.settings.main". If this isn't provided, the
                        DJANGO_SETTINGS_MODULE environment variable will be
                        used.
  --pythonpath PYTHONPATH
                        A directory to add to the Python path, e.g.
                        "/home/djangoprojects/myproject".
  --traceback           Raise on CommandError exceptions
  --no-color            Don't colorize the command output.
  --force-color         Force colorization of the command output.
  --skip-checks         Skip system checks.

Accessing models

Since you're not gonna make management commands for the sake of it. You probably want them to interact with your models in some way.

There is not much special about doing this from a command compared to any other place in your Django application. You can just import the model class in your command file and interact with it as you would normally do from a view for example.

from django.core.management.base import BaseCommand
from plants.models import Plant


class Command(BaseCommand):
    help = 'Water the plants'

    def handle(self, *args, **options):
        plants = Plant.objects.all()

        for plant is plants:
            plant.water()

Writing output

A simple print() will work. But the BaseCommand comes with two other options as well. The BaseCommand sets the stdout and stderr output streams as its properties and we can write to them by calling a write function.

from django.core.management.base import BaseCommand
from plants.models import Plant


class Command(BaseCommand):
    help = 'Water the plants'

    def handle(self, *args, **options):
        plants = Plant.objects.all()

        for plant in plants:
            plant.water()
            self.stdout.write(f'watered plant "{plant}"')

Now that we added that to our command we actually see some output when we run the custom management command.

$ python manage.py waterplants
watered plant "Monstera"
watered plant "Ficus"
watered plant "Cactus"

If you like you can also add some colors to the output. If we were to change the line that writes to stdout to:

self.stdout.write(self.style.SUCCESS(f'watered plant "{plant}"'))

We would see the output having green text when rerunning the command.

Some useful style options you get for writing output are:

  • Success: self.style.SUCCESS('...')
  • Notice: self.style.NOTICE('...')
  • Warning: self.style.WARNING('...')
  • Error: self.style.ERROR('...')

Command errors

When throwing exceptions in your command class you normally want to use the CommandError class that is provided by Django. Since Django will intercept it and write the error message to stderr, besides that it exits the command with a non-zero return code as well.

from django.core.management.base import BaseCommand, CommandError
from plants.models import Plant


class Command(BaseCommand):
    help = 'Water the plants'

    def handle(self, *args, **options):
        plants = Plant.objects.all()

        if not plants:
            raise CommandError('No plants to water')

        for plant in plants:
            plant.water()
            self.stdout.write(self.style.SUCCESS(f'watered plant "{plant}"'))

Custom return codes

As said above, the CommandError class exits the script with a non-zero return code. It returns 1 to be precise. Using a different return code might be useful in some cases.

To do so you can easily pass the returncode keyword argument when throwing the exception.

from django.core.management.base import BaseCommand, CommandError
from plants.models import Plant


class Command(BaseCommand):
    help = 'Water the plants'

    def handle(self, *args, **options):
        plants = Plant.objects.all()

        if not plants:
            raise CommandError('No plants to water', returncode=2)

        for plant in plants:
            plant.water()
            self.stdout.write(self.style.SUCCESS(f'watered plant "{plant}"'))