Django Management Commands

We’ve been using Django commands since we started creating a project, an app or started the development server. The commands to perform these operations (startproject, startapp and runserver) are probably the most well known, but there are many more, and we can also write our own commands.

We can see all the commands that exist by default in our project by running the python manage.py command without arguments or python manage.py help. This is the list of all the available commands:

We can also see the help of a specific command: python manage.py help <command> where command is the name of the command we want to consult.

When to create a new command

We can write our own commands and there is no limit to how many commands we can create, but when should we create a new one?

We must create a new command when the existing commands do not solve the needs of our project and we need to do a task that is executed occasionally (CRON Jobs, importing data from a CSV, etc) either automatically or manually. For example, if we have a human resources system and we need to pay the workers, we probably need to create a pay command that has the logic to make the payment to all the workers in the company. There are probably other ways to solve the previous example but this is one of them.

Writing the first command

All the commands must be created inside an app in a management/commands directory, that is, in the App where you want to create, it must exist or create if it doesn’t a directory named management and inside another directory named commands. Inside the commands directory we will create the commands, one by module (or file).

The commands will be named the same as the module where they are, in other words, if we create a module called hello.py, then to execute this command we must do it in the following way:

Commands must inherit from the BaseCommand class and the class must necessarily be called Command since Django will search by that name. If we name the class with another name, when we execute the command we’ll get an error like this:

In the help attribute we put a text that will describe what the command does and it will be this text that will be shown when the python manage.py help <command> command is executed. The command code must be defined within the handle method.

Arguments

If you’re reading this article, you probably created at least one project with Django, so you executed the startproject command. The startproject command has an argument: the name of the project. When you create a command, it can also accept arguments.

The commands will accept two types of arguments: named and positional. Named arguments are those that have the prefix -- or - and it doesn’t matter the order in which they are passed to the command. Positional parameters do not have any prefix and have to be passed in the same order they are specified.

The arguments in the commands are handled with the standard Python library argparse so they have to satisfy its specifications. A method called add_arguments must be added to commands to allow arguments in the command.

The parser argument is an instance of argparse so all operations in this library are available and allowed. In the case of the named arguments we can put “aliases”. In the example above the argument can be specified as an -l or --lastname. To execute a command with the arguments as in the previous example it is done in the following way:

python manage.py hello Jhon --lastname Doe

Arguments with default values

Sometimes you want that just passing an argument is enough to know what to do, for example, the argument --noreload of the -runserver command doesn’t need to be passed after the argument because it has a default value, in this case it is a boolean so if this argument is present it means that it has as value: True, otherwise it will be None. In this example the value is a boolean, but it can be any type.

To create an argument with a default value we need to add the action parameter when we call the add_argument method. The value of the action parameter will depend on the type and the default value you want the argument to have, if you want it to be boolean, that means that when it’s present it must be True or False, it should have the value store_true or store_false respectively:

parser.add_argument('--noreload', action='store_true',help='...')

The arguments can also be constant, or do really interesting things. If you consult the official documentation of argparse you can know everything that can be done with the action parameter. This is an example of how we could store a constant by default:

parser.add_argument('--foo', action='store_const', const=42)

Argument lists

Many times we don’t know how much data is going to be passed to us as an argument in a command. To solve this problem, there are parameter lists where we can receive a list of values with just one parameter.

Suppose you are creating a command to delete users, but you want this command to provide the ability to delete several users at once. This is one of the cases where using a list of arguments is useful.

To create a list you must add the parameters nargs='+' and type when the add_argument method is invoked. In the case of the type parameter it can be any type accepted by argparse.

Output and styles

Printing text by standard Python output from a command is done slightly differently than printing something elsewhere in the application. Instead of using the print function we use the stdout streams for standard output, and stderr for errors. Both stdout and stderr are attributes of BaseCommand so we can use them as follows:

self.stderr.write('error')
self.stdout.write('normal')

You can also add “color” to the texts we print using the attribute style, so that what is printed has the same meaning both visually and semantically. The official documentation explains the role of each of these colors. Let’s see an example where each of the variants is used:

Testing

Testing that the written code works properly is something that most developers only do manually, but sometimes they forget to write automated tests. Automated testing ensures that every time a change is made to the project everything continues working as it should.

Here is just a simple example of how to test if a command works correctly based on what it prints on the console with self.stdout. We will test the command written earlier to remove users:

In the official documentation you can consult how to use call_command. This function is used to programmatically execute a command. The first parameter is the name of the command and the following parameters are the arguments of the command. Named arguments can also be passed as named function parameters.

In the example above, the test is first configured with the setUp method, where a user is created and then tested for successful deletion. With the call_command function, the command is executed programmatically and the standard output of the command is redirected to a StringIO instance which is then used to read the output of the command line by line. The command will try to remove the users that have ID 1 and 2, there will be a user with ID 1, since in the configuration method it is created, and being the first one, it will have ID 1, but there will not be any user with ID 2. The output of the command is checked in the last lines with `assertIn. If everything is OK, the command will print what is expected and the test will be passed successfully.

Conclusions

The commands are the perfect mechanism to create reports, maintenance tasks, among other operations that are repeated periodically either manually or through a scheduled tasks. Passing arguments is often one of the most complex tasks when creating commands with many arguments, and the official documentation of argparse is often the ideal place to solve this problem. The Django official documentation for creating commands is also an excellent resource to look at.