When you are creating an application, you will usually want to be able to tell your application how to do something. There are two popular methods for accomplishing this task. You can make your application accept command-line arguments or you can create a graphical user interface. Some applications support both.
Command-line interfaces are helpful when you need to run your code on a server. Most servers do not have a monitor hooked up, especially if they are Linux servers. In those cases, you might not be able to run a graphical user interface even if you wanted to.
Python comes with a built-in library called argparse
that you can use to create a command-line interface. In this article, you will learn about the following:
- Parsing Arguments
- Creating Helpful Messages
- Adding Aliases
- Using Mutually Exclusive Arguments
- Creating a Simple Search Utility
There is a lot more to the argparse
module than what will be covered in this article. If you would like to know more about it, you can check out the documentation.
Now it’s time to get started with parsing arguments from the command-line!
Parsing Arguments
Before you learn how to use argparse
, it’s good to know that there is another way to pass arguments to a Python script. You can pass any arguments to a Python script and access those arguments by using the sys
module.
To see how that works, create a file named sys_args.py
and enter the following code into it:
# sys_args.py import sys def main(): print('You passed the following arguments:') print(sys.argv) if __name__ == '__main__': main()
This code imports sys
and prints out whatever is in sys.argv
. The argv
attribute contains a list of everything that was passed to the script with the first item being the script itself.
Here’s an example of what happens when you run this code along with a couple of sample arguments:
$ python3 sys_args.py --s 45 You passed the following arguments: ['sys_args.py', '--s', '45']
The problem with using sys.argv
is that you have no control over the arguments that can be passed to your application:
- You can’t ignore arguments
- You can’t create default arguments
- You can’t really tell what is a valid argument at all
This is why using argparse
is the way to go when working with Python’s standard library. The argparse
module is very powerful and useful. Let’s think about a common process that a command line application follows:
- pass in a file
- do something to that file in your program
- output the result
Here is a generic example of how that might work. Go ahead and create file_parser.py
and add the following code:
# file_parser.py import argparse def file_parser(input_file, output_file=''): print(f'Processing {input_file}') print('Finished processing') if output_file: print(f'Creating {output_file}') def main(): parser = argparse.ArgumentParser('File parser') parser.add_argument('--infile', help='Input file') parser.add_argument('--out', help='Output file') args = parser.parse_args() if args.infile: file_parser(args.infile, args.out) if __name__ == '__main__': main()
The file_parser()
function is where the logic for the parsing would go. For this example, it only takes in a file name and prints it back out. The output_file
argument defaults to an empty string.
The meat of the program is in main()
though. Here you create an instance of argparse.ArgumentParser()
and give your parser a name. Then you add two arguments, --infile
and --out
. To use the parser, you need to call parse_args()
, which will return whatever valid arguments were passed to your program. Finally, you check to see if the user used the --infile
flag. If they did, then you run file_parser()
.
Here is how you might run the code in your terminal:
$ python file_parser.py --infile something.txt Processing something.txt Finished processing
Here you run your script with the --infile
flag along with a file name. This will run main()
which in turns calls file_parser()
.
The next step is to try your application using both command-line arguments you declared in your code:
$ python file_parser.py --infile something.txt --out output.txt Processing something.txt Finished processing Creating output.txt
This time around, you get an extra line of output that mentions the output file name. This represents a branch in your code logic. When you specify an output file, you can have your code go through the process of generating that file using a new block of code or a function. If you do not specify an output file, then that block of code would not run.
When you create your command-line tool using argparse
, you can easily add messages that help your users when they are unsure of how to correctly interact with your program.
Now it’s time to find out how to get help from your application!
Creating Helpful Messages
The argparse
library will automatically create a helpful message for your application using the information that you provided when you create each argument. Here is your code again:
# file_parser.py import argparse def file_parser(input_file, output_file=''): print(f'Processing {input_file}') print('Finished processing') if output_file: print(f'Creating {output_file}') def main(): parser = argparse.ArgumentParser('File parser') parser.add_argument('--infile', help='Input file') parser.add_argument('--out', help='Output file') args = parser.parse_args() if args.infile: file_parser(args.infile, args.out) if __name__ == '__main__': main()
Now try running this code with the -h
flag and you should see the following:
$ file_parser.py -h usage: File parser [-h] [--infile INFILE] [--out OUT] optional arguments: -h, --help show this help message and exit --infile INFILE Input file --out OUT Output file
The help
parameter to add_argument()
is used to create the help message above. The -h
and --help
options are added automatically by argparse
. You can make your help more informative by giving it a description
and an epilog
.
Let’s use them to improve your help messages. Start by copying the code from above into a new file named file_parser_with_description.py
, then modify it to look like this:
# file_parser_with_description.py import argparse def file_parser(input_file, output_file=''): print(f'Processing {input_file}') print('Finished processing') if output_file: print(f'Creating {output_file}') def main(): parser = argparse.ArgumentParser( 'File parser', description='PyParse - The File Processor', epilog='Thank you for choosing PyParse!', ) parser.add_argument('--infile', help='Input file for conversion') parser.add_argument('--out', help='Converted output file') args = parser.parse_args() if args.infile: file_parser(args.infile, args.out) if __name__ == '__main__': main()
Here you pass in the description
and epilog
arguments to ArgumentParser
. You also update the help
arguments to add_argument()
to be more descriptive.
When you run this script with -h
or --help
after making these changes, you will see the following output:
$ python file_parser_with_description.py -h usage: File parser [-h] [--infile INFILE] [--out OUT] PyParse - The File Processor optional arguments: -h, --help show this help message and exit --infile INFILE Input file for conversion --out OUT Converted output file Thank you for choosing PyParse!
Now you can see the new description and epilog in your help output. This gives your command-line application some extra polish.
You can also disable help entirely in your application via the add_help
argument to ArgumentParser
. If you think that your help text is too wordy, you can disable it like this:
# file_parser_no_help.py import argparse def file_parser(input_file, output_file=''): print(f'Processing {input_file}') print('Finished processing') if output_file: print(f'Creating {output_file}') def main(): parser = argparse.ArgumentParser( 'File parser', description='PyParse - The File Processor', epilog='Thank you for choosing PyParse!', add_help=False, ) parser.add_argument('--infile', help='Input file for conversion') parser.add_argument('--out', help='Converted output file') args = parser.parse_args() if args.infile: file_parser(args.infile, args.out) if __name__ == '__main__': main()
By setting add_help
to False
, you are disabling the -h
and --help
flags.
You can see this demonstrated below:
$ python file_parser_no_help.py --help usage: File parser [--infile INFILE] [--out OUT] File parser: error: unrecognized arguments: --help
In the next section, you’ll learn about adding aliases to your arguments!
Adding Aliases
An alias is a fancy word for using an alternate flag that does the same thing. For example, you learned that you can use both -h
and --help
to access your program’s help message. -h
is an alias for --help
, and vice-versa
Look for the changes in the parser.add_argument()
methods inside of main()
:
# file_parser_aliases.py import argparse def file_parser(input_file, output_file=''): print(f'Processing {input_file}') print('Finished processing') if output_file: print(f'Creating {output_file}') def main(): parser = argparse.ArgumentParser( 'File parser', description='PyParse - The File Processor', epilog='Thank you for choosing PyParse!', add_help=False, ) parser.add_argument('-i', '--infile', help='Input file for conversion') parser.add_argument('-o', '--out', help='Converted output file') args = parser.parse_args() if args.infile: file_parser(args.infile, args.out) if __name__ == '__main__': main()
Here you change the first add_argument()
to accept -i
in addition to --infile
and you also added -o
to the second add_argument()
. This allows you to run your code using two new shortcut flags.
Here’s an example:
$ python3 file_parser_aliases.py -i something.txt -o output.txt Processing something.txt Finished processing Creating output.txt
If you go looking through the argparse
documentation, you will find that you can add aliases to subparsers too. A subparser is a way to create sub-commands in your application so that it can do other things. A good example is Docker, a virtualization or container application. It has a series of commands that you can run under docker
as well as docker compose
and more. Each of these commands has separate sub-commands that you can use.
Here is a typical docker command to run a container:
docker exec -it container_name bash
This will launch a container with docker. Whereas if you were to use docker compose
, you would use a different set of commands. The exec
and compose
are examples of subparsers.
The topic of subparsers are outside the scope of this tutorial. If you are interested in more details dive right into the documentation.
Using Mutually Exclusive Arguments
Sometimes you need to have your application accept some arguments but not others. For example, you might want to limit your application so that it can only create or delete files, but not both at once.
The argparse
module provides the add_mutually_exclusive_group()
method that does just that!
Change your two arguments to be mutually exclusive by adding them to a group
object like in the example below:
# file_parser_exclusive.py import argparse def file_parser(input_file, output_file=''): print(f'Processing {input_file}') print('Finished processing') if output_file: print(f'Creating {output_file}') def main(): parser = argparse.ArgumentParser( 'File parser', description='PyParse - The File Processor', epilog='Thank you for choosing PyParse!', add_help=False, ) group = parser.add_mutually_exclusive_group() group.add_argument('-i', '--infile', help='Input file for conversion') group.add_argument('-o', '--out', help='Converted output file') args = parser.parse_args() if args.infile: file_parser(args.infile, args.out) if __name__ == '__main__': main()
First, you created a mutually exclusive group. Then, you added the -i
and -o
arguments to the group instead of to the parser
object. Now these two arguments are mutually exclusive.
Here is what happens when you try to run your code with both arguments:
$ python3 file_parser_exclusive.py -i something.txt -o output.txt usage: File parser [-i INFILE | -o OUT] File parser: error: argument -o/--out: not allowed with argument -i/--infile
Running your code with both arguments causes your parser to show the user an error message that explains what they did wrong.
After covering all this information related to using argparse
, you are ready to apply your new skills to create a simple search tool!
Creating a Simple Search Utility
Before starting to create an application, it is always good to figure out what you are trying to accomplish. The application you want to build in this section should be able to search for files of a specific file type. To make it more interesting, you can add an additional argument that allows you to optionally search for specific file sizes as well.
You can use Python’s glob
module for searching for file types. You can read all about this module here:
There is also the fnmatch
module, which glob
itself uses. You should use glob
for now as it is easier to use, but if you’re interested in writing something more specialized, then fnmatch
may be what you are looking for.
However, since you want to be able to optionally filter the files returned by the file size, you can use pathlib
which includes a glob
-like interface. The glob
module itself does not provide file size information.
You can start by creating a file named pysearch.py
and entering the following code:
# pysearch.py import argparse import pathlib def search_folder(path, extension, file_size=None): """ Search folder for files """ folder = pathlib.Path(path) files = list(folder.rglob(f'*.{extension}')) if not files: print(f'No files found with {extension=}') return if file_size is not None: files = [ f for f in files if f.stat().st_size >= file_size ] print(f'{len(files)} *.{extension} files found:') for file_path in files: print(file_path)
You start the code snippet above by importing argparse
and pathlib
. Next you create the search_folder()
function which takes in three arguments:
path
– The folder to search withinextension
– The file extension to look forfile_size
– What file size to filter on in bytes
You turn the path
into a pathlib.Path
object and then use its rglob()
method to search in the folder for the extension that the user passed in. If no files are found, you print out a meaningful message to the user and exit.
If any files are found, you check to see whether file_size
has been set. If it was set, you use a list comprehension to filter out the files that are smaller than the specified file_size
.
Next, you print out the number of files that were found and finally loop over these files to print out their names.
To make this all work correctly, you need to create a command-line interface. You can do that by adding a main()
function that contains your argparse
code like this:
def main(): parser = argparse.ArgumentParser( 'PySearch', description='PySearch - The Python Powered File Searcher', ) parser.add_argument('-p', '--path', help='The path to search for files', required=True, dest='path') parser.add_argument('-e', '--ext', help='The extension to search for', required=True, dest='extension') parser.add_argument('-s', '--size', help='The file size to filter on in bytes', type=int, dest='size', default=None) args = parser.parse_args() search_folder(args.path, args.extension, args.size) if __name__ == '__main__': main()
This ArgumentParser()
has three arguments added to it that correspond to the arguments that you pass to search_folder()
. You make the --path
and --ext
arguments required while leaving the --size
argument optional. Note that the --size
argument is set to type=int
, which means that you cannot pass it a string.
There is a new argument to the add_argument()
function. It is the dest
argument which you use to tell your argument parser where to save the arguments that are passed to them.
Here is an example run of the script:
$ python3 pysearch.py -p /Users/michael/Dropbox/python101code/chapter32_argparse -e py -s 650 6 *.py files found: /Users/michael/Dropbox/python101code/chapter32_argparse/file_parser_aliases2.py /Users/michael/Dropbox/python101code/chapter32_argparse/pysearch.py /Users/michael/Dropbox/python101code/chapter32_argparse/file_parser_aliases.py /Users/michael/Dropbox/python101code/chapter32_argparse/file_parser_with_description.py /Users/michael/Dropbox/python101code/chapter32_argparse/file_parser_exclusive.py /Users/michael/Dropbox/python101code/chapter32_argparse/file_parser_no_help.py
That worked quite well! Now try running it with -s
and a string:
$ python3 pysearch.py -p /Users/michael/Dropbox/python101code/chapter32_argparse -e py -s python usage: PySearch [-h] -p PATH -e EXTENSION [-s SIZE] PySearch: error: argument -s/--size: invalid int value: 'python'
This time, you received an error because -s
and --size
only accept integers. Go try this code on your own machine and see if it works the way you want when you use -s
with an integer.
Here are some ideas you can use to improve your version of the code:
- Handle the extensions better. Right now it will accept
*.py
which won’t work the way you might expect - Update the code so you can search for multiple extensions at once
- Update the code to filter on a range of file sizes (Ex. 1 MB – 5MB)
There are lots of other features and enhancements you can add to this code, such as adding error handling or unittests.
Wrapping Up
The argparse
module is full featured and can be used to create great, flexible command-line applications. In this chapter, you learned about the following:
- Parsing Arguments
- Creating Helpful Messages
- Adding Aliases
- Using Mutually Exclusive Arguments
- Creating a Simple Search Utility
You can do a lot more with the argparse
module than what was covered in this chapter. Be sure to check out the documentation for full details. Now go ahead and give it a try yourself. You will find that once you get the hang of using argparse
, you can create some really neat applications!