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 ( 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
# 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
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.
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
......
......
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.
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
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
.......
.......
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
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.
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
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
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
.......
.......
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
.......
.......
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.
......
......
......
# 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
.......
.......
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.
.......
.......
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
......
......
# 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
python github_zen.py "log" -n 4
you should get a response on your terminal/command line in this form
Zen 5
Zen 4
Zen 3
Zen 2
Zen 1
Run the program with
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
.......
.......
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
.......
.......
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.
Happy Coding.
Top comments (0)