Simple Introduction to Command Line Argument in Python
Tags: python, argparse, cli, beginners
Published: 2021-05-14 12:54:50

## Introduction

At some point you would want your program to be provided data even before running it. This is just like providing a function some data before the function's body actually executes. Just as functions can accept arguments when called, a command line application can as well be modeled thus. 

In this post, we'll build a program that would retrieve Github's Zen. Amount to be retrieved and how it should be outputted would be affected by the supplied command line arguments. 

### What you stand to gain

If you follow through to the end of this post, you would learn/refreshen your previous understanding/have a new concept of:

- argparse python standard library and it's application to command line application.
- python requests library 
- python I/O standard library
- python time standard library
- a command line application that interfaces with github API.

### Requirements & Uses

- familiarity with python programming language. 
- python version 3.8 is used in this post.

This post is well detailed and about ~11 minutes read but you'll enjoy it, I guarantee and if you don't, give the post a thumbs down after reading through to the end ( I would appreciate you let me know why you didn't enjoy the post in the comment session. This would really help me. )

## Getting started

If you're a python developer, then, you must have used `python file_name.py` command in your terminal/command prompt to invoke a program, run a script for some automations, start a webserver and many more. If all you want  is to make your program run as designed/setup to run, then you're fine with just that. 

Command line arguments are data sent to a program to affect its state and/or behavior on it's invocation. python argparse library would be used in this post. 

> NOTE:
> argparse is not the only library to implement command line argument in python, but it is the recommended way.

> Disclaimer:
> Kindly note that although, in this post, I relate functions to command line arguments, they are entirely two different things. The reason for relating function to command line arguments is because of their similarity and at some point as a developer you'll eventually learn about functions.

Before we begin, [clone project folder](https://github.com/spaceofmiah/learn_ci_args.git) ( if you want to follow through, remove all codes in `github_zen.py` ), navigate to cloned folder in your command line and setup a virtual environment with `python -m venv env` do also activate your environment 

```shell
# windows system
env/Scripts/activate 

# unix system
source env\bin\activate
```

finally install all required dependencies specified in requirements.txt file with `pip install -r requirements.txt`. All code samples would be done in github_zen.py file.

Every function has a signature made of the following (not all, but most relevant to this article):
- parameters which could either be positional, named, required or have a default.
- parameter data type
- parameter description/help text

Same holds true for command line arguments and incorrect usage would trigger an error. Let's take each components and see how they're  modeled.  Add the following code to `github_zen.py` file

```python 
import argparse

# Instantiate ArgumentParser and provide it with program name &
# description. To be shown when --help is called on program module
parser = argparse.ArgumentParser(
	prog="GitZen", 
	description="Zen of Git"
)
```
First line imports argparse and the second line instantiate  ArgumentParser class. Above call to ArgumentParser constructor takes a `description` and `prog` named arguments. The value supplied to the description argument should be a description of what the application does as this would be displayed when program `help` is run on the command line. The `prog` argument is used as the name of the program. `prog` argument defaults to the file name if nothing is supplied.  ArgumentParser constructor takes [other arguments](https://docs.python.org/3/library/argparse.html?#argumentparser-objects). 

### Parameters
Parameters serves as variable placement to a callable and/or runnable ( function, method, program e.t.c. ) to retain data to compute some functionality.

> #### Without parameters, there are no arguments.


### Optional Command Line Parameters

For programs to be provided command line arguments, such programs must have in some ways defined the parameters for such arguments. ArgumentParser class provide a method `add_argument` which defines the argument(s) applicable to a program. All program arguments definitions are stored until `parse_args` is called.

#### First Approach

```python
......
......

parser.add_argument(
    "-n",  "--num",
    type=int,
    default=1
)

args = parser.parse_args()        # retrieves all stored arguments
print(args.num)                   # outputs value given to num argument
```

Above code adds an argument to your program. The argument is a flag. Flag arguments are  not required on program invocation  using the `python` command. The positional strings separated by comma with either a single/double hyphen are the different name(s) representing the given argument that is `num` argument. Flag name could be anything, but we've decided to stick with `-n` and `--num` (want more flag names for a single argument, you could add some more. I would actually love to add `--number`, buhhh!!!! let's keep it simple). 

One reason why you might consider having different representing names for an argument is for usability purpose (I can get very lazy and decide to go with the shortest flag `-n`, i may also want to consider going with the longest flag `--num` so someone else reading my program invocation can easily understand what that flag is meant for). When nothing is supplied to a flag argument, it defaults to None. Above code defaults `num` flag argument to 1 instead. Every defined argument are retrieved as string from the command line and turned into their distinct types as declared. Any value supplied to `num` argument would be cast to type `int` as that's what it's been declared with.

##### Correct Program Invocation (First Approach)
 
Although above argument is optional, it'll be that, if you don't specify the flag at all on program invocation.

```shell
python github_zen.py
```

Above command would run the program just fine and defaults the value of `num` argument to what was set as default on argument declaration.

If the flag is specified on program invocation, it would require you to provide a value, otherwise an error would be raised e.g 

```shell
python github_zen.py --num
``` 

Above command would raise an error as it's expecting a value which was not supplied. To right this error `python github_zen.py --num 3` would do. It can be liken to keyword argument (**kwargs), which although not required but if you were to include any keyword, then you must supply a value for it.

Familiar with unix system and it's commands ? If yes, then you must have come across directory listing command `ls` command. 

`ls` command can take flag argument e.g `ls -l` or `ls -la` without requiring you to pass any value afterwards and doesn't throw any error. This can be achieved with argparse library.

Replace above `num` argument declaration with

#### Second Approach

```python
.......
.......

parser.add_argument(
    "-n", "--num",
    const=1,
    action="store_const",
)

......
......
```

`const` keyword and the value specified for `action` keyword argument makes the difference in above declaration. Although `action` wasn't explicitly added to the first approach for flag `num` argument, add_argument has `action` default to the value, `store`, which was used for the first approach. 

##### Correct Program Invocation (Second Approach)

With this second approach you can explicitly state the flag e.g 

```shell
python github_zen.py --num
``` 

If the program is invoked with above command, `const` value as defined in argument declaration ( `const=1` ) which is 1, would be supplied to `num` argument. As a `const` ( constant ) it's value cannot be change, which is why no value is required for the argument on program invocation.

```shell
python github_zen.py --num 4
```

Above invocation would attempt to override the value of a constant which would raise an error.

If --num or -n flag is not given on program invocation e.g

```shell
python github_zen.py 
```

Above program invocation would default `num` argument to `None`. This default value can be changed with the `default` keyword on argument declaration e.g `default=any_value_of_choice`.

**Why would you choose one approach over the other:**

- **What optional means to your program**: If your program can run without the argument supplied, it would make much sense to have the program use the second approach, either or not the argument is supplied, the program would still run - a good example for this is the `ls` command on unix system.

-  **Extent at which a user is allowed to affect your program behaviour on invocation**: If your program requires less user interaction on invocation, then the second approach would work fine. If you want your program to work with/without user interaction on invocation ( less restrictive ), the first approach is consider the route. 

**JUST A MINUTE** ⏲
*Say you're building a greeting application that would send a greeting to the given name after -n argument, but also works fine if no name is given and -n flag is omitted on program invocation. Lets say this is save in `hello.py`. This program would output `Hello Universe` if invoked with `python hello.py` but output `Hello Doe` if invoked with `python hello.py -n Doe`. What approach would you use in this scenario (First / Second) ?*

Well, for this program, the first approach will be used. `--num or -n` argument would tell how many Zen our program should retrieve from Github API on a single invocation. To continue make sure your codebase is this

```python
import argparse

# Instantiate and describes what program does when help is
# called on the module.
parser = argparse.ArgumentParser(
    prog="GitZen",
    description="Zen of Git"
)
parser.add_argument(
    "-n", "--num",
    type=int,
    default=1,
)

args = parser.parse_args()        # retrieves all stored arguments
print(args.num)                   # outputs value given to num argument
```

Since we'll be interacting with Github API without authentication,  we're only allowed to 60 request per hour. For that reason, you wouldn't want your users to consume all the request in a single program invocation `python github_zen.py --num 60`. Say you want to limit the numerical value  users are allowed to supply when using --num argument to max of 5 and min of 1,  `choices` keyword argument comes to the rescue. This argument would set a constraint on the value specified and if the constraint is violated, error would be raised. Update your code to set a constraint that would raise an error if user should provides any value `<1 or >5` 

```python
.......
.......

parser.add_argument(
	.....
	.....
	choices = [ value for value in range(1, 6) ]
)

.......
.......
```
We're using list comprehension to generate a list containing numerical values from 1 through 5.

At this state, if this program is used by someone that don't understand what the program arguments are meant for, they'll be completely lost. It's important to make code/program simple and well documented- it'll save you some stress, trust me. Try running this command `python github_zen.py -h` you'll notice `--num` argument doesn't have much descriptive content. To add some descriptive text for `--num` argument, add this update

```python
.......
.......

parser.add_argument(
	.....
	.....
	help= "Defines the number of zen to retrieve. Max of 5 and Min of 1. Defaults to 1 if flag not used in invocation"
)

.......
.......
```

Anyone who knows how to read and run help command on your program should now know how `--num` argument works.

`--num` argument is in place and ready to ride. If we want to retrieve the amount of zen requested by a user, then we'll need an engine to compute that. Let's add a dummy engine that would mimic the functionality of retrieving zen from Github API and output retrieved zen to the standard output/cli. Bare for now this dummy engine, it'll be revised shortly.

```python
......
......
......

# Save the value for num argument instead of print it out
zens_to_retrieve = args.num

# Engine retrieving the number of zen required by user
while zens_to_retrieve > 0:
    print(args.num)
    zens_to_retrieve -= 1
```

Value supplied to num argument is saved in zen_to_retrieve variable, which is used to power the engine of the while loop. In every loop, we simply outputs the number of zen the user requires. This dummy engine is used, so as to keep the application focused on command line argument and also not to exhaust the number of API request that can be made to github's API. 

### Positional Command Line Parameters

We've seen how to define and use flags (optional arguments) in our program. Let's include another argument. This argument would tell where we want the output for our program to be rendered. Right below the definition of `--num` argument include the following code

```python
.......
.......
parser.add_argument(
   "out",
    type=str,
    choices=[ "log", "txt", ],
    help = "Defines where zen would be rendered. This is required"
)
.......
.......
```

As discussed earlier, any argument defined with hyphens in their name, are considered optional. Above example, `out` argument is declared without hyphens, which automatically makes the argument required and positional. Only one name can be specified with this approach unlike flags that can have several names. If you prefer your arguments to use the flag approach but still want them required on program invocation, then consider setting `required` keyword argument to `True` for such argument definition. `out` argument has constraint to determine where response would be rendered. For this program, supported rendering platforms are `log` (cli) and `txt` (.txt file). You could add more like json, csv e.t.c. 

The below code shows the implementation for generating filename to render the program's output. 

```python
.......
.......
from datetime import datetime

......
......

zens_to_retrieve = args.num
output = args.out

# Create a different file name on every run with datetime lib. &
# replace all spaces on datetime generated text by underscores &
# colons by hyphens. 
# All of these is so that file name can meet supported naming format.

date_time_list = datetime.now().strftime("%c").split(" ")
time_list = "_".join(date_time_list).split(":")
file_name = "-".join(time_list)
file_name_and_extension = f"{file_name}.{output}"

......
......
```
Above code  imports python's datetime standard library and generates a file name out of the current datetime each time the program is run. The datetime is formatted using '%c' which generates a readable datetime e.g Thu May 13 13:39:36 2021. Some conversions are done on the generated datetime so it becomes Thu_May_13_13-39-36_2021 which would make it compatible with supported file naming convension.

Now that we've different output options for rendering program's response, there's need to re-write the rendering engine to cater for the different usecases. Replace the while loop with the below code

```python
......
......

# Zen retrieval engine
if output != "log":
    file = open(f"{file_name_and_extension}", "w")
    while zens_to_retrieve > 0:
        file.write(f"Zen {zens_to_retrieve}\n")
        zens_to_retrieve -= 1
    file.close()
else:
    while zens_to_retrieve > 0:
        print(f"> Zen {zens_to_retrieve} \n")
        zens_to_retrieve -= 1
```

Above code should be functional as is. Based on the value you supply on program invocation, you'll have different behaviour. e.g

Run the program with
```shell
python github_zen.py "log" -n 4
``` 
you should get a response on your terminal/command line in this form

```shell
Zen 5
Zen 4
Zen 3
Zen 2
Zen 1
```

Run the program with 

```shell
python github_zen.py "txt" -n 4
```

and the program should create a `.txt` file in the current directory with similar content as what was outputted in the terminal.

Now that we've the `--num`  and `out` argument in place, lets add the functionality of retrieving zen from Github API with the python request library. This would make the content of our program real and non repeating. If you followed the setup process, you should have requests library installed. import it with `import requests`. We'll also need time standard library, so kindly import it with `import time`. time library would be used to sleep the program for 20 seconds on every iteration - This is to make sure we don't have same zen repeated for all iterations ( You could experiment by commenting out the time.sleep statement )

Update while loop block that operates on file (`.txt`) with the below code

```python
.......
.......

time.sleep(20)
response = requests.get("https://api.github.com/zen")
file.write(f"> {response.text}\n")
zens_to_retrieve -= 1

.......
.......
```

Second line makes request to Github's zen API with requests library. `file.write` now writes the content retrieve as response from Github to file and lastly the counter is decremented.

Update while loop block that operates on log with the below code

```python
.......
.......

time.sleep(20)
response = requests.get("https://api.github.com/zen")
print(f"> {response.text}")
zens_to_retrieve -= 1
.......
.......
```

If you've followed through to this point, you should have a decent understanding on how command line arguments works in python using argparse standard library. With this, you can delve into integrating command line argument to your new or existing program. If you don't have anything to work on, then extend this program:

*No Pressure, But You Could Try These*
- _**Custom File Name**:_ *The current approach uses datetime as file name. You can add another argument for custom filename, so instead of using formatted datetime as file name, the custom filename should be used.*

- _**JSON output**:_ *Care to extend the functionality of the program, you could add `json` format to the already existing argument and provide implementation for that*

Do you find this article useful in anyway? like and share it then :) ( honestly, i appreciate it ). Your feedback and suggestions are also welcomed.

You can find the full code on my [github](https://github.com/spaceofmiah/learn_ci_args/).

Happy Coding.