Pros and cons of running code at the command line as opposed to within an IDE, notebook, or Quarto document
Organizing a Python script file
Documenting functions and scripts with docstrings
Reading command-line arguments using sys and argparse
Using if __name__ == "__main__": so that a script can be both run and imported
7.2 Introduction
Up to this point, all of the code in this book has been written and executed interactively, one code cell at a time, within a Quarto document. This is a great way to explore data, test ideas, and document a workflow. However, once a process is working, you may want to run it without opening an IDE, apply it to new inputs without editing the code, schedule it to run automatically, or share it with colleagues who do not use Python regularly. In these cases, it is useful to save the code as a script, or a plain text file with a .py extension, and run it from the command line.
In this chapter, we discuss the pros and cons of running code at the command line, how to organize a script file, how to document code using docstrings, and how to allow the user to provide inputs, or arguments, when the script is run. As an example, we build a script that converts a temperature between degrees Fahrenheit, degrees Celsius, and Kelvin. The user provides the temperature, the original units, and the desired units. This chapter builds upon the material on functions, If…Else statements, f-strings, and modules presented in Chapter 2.
The example scripts are stored in the scripts folder provided with the book files. Since the scripts only make use of modules that are included with Python, no additional libraries are needed.
Building scripts is one step towards building your own Python package. This is not covered in this text. If you are interested in this topic, we recommend the text Python Packages by Beuzen and Timbers, which steps through the process of creating, testing, documenting, releasing, and updating Python packages. This text is freely available.
7.3 Command Line vs. Interactive Environments
The command line is a text-based interface for interacting with your computer. You type a command, press Enter, and the result is printed back as text. On Windows, the command line is accessed using the Anaconda Prompt or Miniforge Prompt, PowerShell, or the Command Prompt. On macOS and Linux, it is accessed using the Terminal application. Most IDEs, including VS Code, PyCharm, and RStudio, also provide a terminal window. The program that interprets the commands is called a shell.
When you work in a Jupyter notebook or Quarto document, code is executed interactively, and the results, tables, and graphs are displayed directly below the code that generated them. When you run a script from the command line, the entire file is executed from top to bottom in a new Python session, and only what the script explicitly prints or saves is returned. Each approach has advantages and disadvantages.
7.3.1 Advantages of the Command Line
Automation and batch processing: A script can be run repeatedly with different inputs, looped over many files, or chained together with other scripts and command-line tools without any manual interaction.
Scheduling: Scripts can be run at set times using tools such as Task Scheduler on Windows or cron on macOS and Linux. For example, you could download and process new satellite imagery every night.
Remote and high-performance computing: Remote servers, cloud machines, and high-performance computing (HPC) clusters generally do not provide a graphical interface. Instead, jobs are submitted as scripts that are run from the command line.
Reproducibility: In a notebook, it is possible to run cells out of order or to rely on a variable that was defined in a cell that has since been deleted. A script always starts from a clean session and runs from top to bottom, so it is more likely to produce the same result each time it is run.
Reusability and sharing: If a script accepts arguments, others can use it by providing their own inputs without needing to open or edit the code. They do not need to know how the code works, only how to call it.
Resources and long-running processes: Running a script does not require an IDE or a browser, and the process does not depend on an interactive session staying open. This is beneficial for processes that take hours or days to complete.
Version control: Scripts are plain text files, so changes are easy to track using version control software, such as Git. In contrast, notebook files also store outputs and metadata, which can make changes difficult to compare.
7.3.2 Disadvantages of the Command Line
Less interactive exploration: You cannot easily inspect a variable, view a DataFrame, or adjust a graph partway through a script. This makes notebooks better suited to exploratory data analysis.
Limited display of results: Graphs, maps, and tables are not displayed alongside the code. Instead, they must be saved to files and opened separately.
More difficult debugging: When a script fails, the error message and traceback are printed, but the variables that existed at the time of the error are not available to inspect unless you use a debugger.
Less documentation: A Quarto document or notebook combines code, results, and narrative text. A script only contains code and comments, so it is less effective for communicating a workflow or results to others.
Learning curve: You need to learn some basic shell commands, how to activate a conda environment, and how file paths are interpreted relative to the current working directory.
These approaches are not mutually exclusive. A common workflow is to explore data and develop code interactively in a notebook or Quarto document and then, once the process is stable, move the code into functions within a script that can be run from the command line or imported into other code.
7.4 Starting with a Function
Before building a script, we write and test the core functionality interactively. To convert between three units, we could write six separate formulas: one for each pair of units. A simpler approach is to first convert the original temperature to Celsius and then convert from Celsius to the desired units. This only requires four formulas:
Fahrenheit to Celsius: C = (F - 32) × 5/9
Kelvin to Celsius: C = K - 273.15
Celsius to Fahrenheit: F = C × 9/5 + 32
Celsius to Kelvin: K = C + 273.15
The first version of the function, defined below, accepts the temperature (value), the original units (from_unit), and the desired units (to_unit). Units are provided as single-character strings: “F”, “C”, or “K”. An If…Else statement converts the value to Celsius, and a second If…Else statement converts the Celsius value to the desired units.
Note that the last conversion returns 80.33000000000004 as opposed to 80.33. This is not an error in the formula. Computers store decimal numbers in binary, and many decimal values cannot be represented exactly, so small floating point errors are common. For this reason, we will round the result when it is printed.
This function works, but it has some issues that become important once other people use it. What happens if the user provides lowercase units, such as “f”, or a unit that does not exist, such as “X”? Since neither matches “F” or “K”, the value is assumed to already be in Celsius, and an incorrect result is silently returned. The function also accepts temperatures below absolute zero (0 K, -273.15 °C, or -459.67 °F), which are not physically possible. We address these issues below.
7.5 Docstrings
7.5.1 What is a Docstring?
Comments, which begin with #, explain individual lines or blocks of code to someone reading the code. A docstring is different: it is a string literal placed as the first statement inside a function, class, method, or module that describes what the object does and how to use it. Docstrings are defined using triple quotes (""") so that they can span multiple lines. Unlike comments, docstrings are stored with the object and can be accessed using the help() function or the __doc__ attribute. They are also used by IDEs to display information when you hover over a function name and by tools that automatically generate documentation websites.
The general conventions for docstrings are described in PEP 257:
The first line is a short summary that ends with a period.
If more detail is needed, the summary is followed by a blank line and then a longer description.
The docstring describes what the function does and how to use it, not how the code works internally. Implementation details belong in comments.
Several formats are commonly used for the longer description, including the NumPy style, Google style, and reStructuredText style. We use the NumPy style here since it is used by NumPy, Pandas, scikit-learn, and many other scientific libraries, so it will look familiar if you have read their documentation. In this style, the description is divided into sections, such as Parameters, Returns, Raises, and Examples, and each section title is underlined with dashes.
7.5.2 Documenting the Function
Below, we redefine the function with a docstring. We also make the function more robust:
The units are converted to uppercase using the upper() method so that “f” and “F” are treated the same.
If either unit is not “F”, “C”, or “K”, a ValueError is raised using the raise keyword. This stops the function and returns an informative error message as opposed to an incorrect result.
If the temperature is below absolute zero, a ValueError is raised. Note that the Celsius value is rounded before it is compared to absolute zero. Due to floating point error, -459.67 °F converts to a value very slightly less than -273.15 °C, which would otherwise be flagged as invalid.
The valid units and absolute zero are stored as constants, which are variables defined at the top of a script whose values are not meant to change. By convention, constant names are written in all uppercase letters.
The Examples section uses the format of the interactive Python prompt, in which >>> precedes the code and the expected output is listed on the next line. This shows the user exactly how to call the function.
VALID_UNITS = ("F", "C", "K")ABSOLUTE_ZERO_C =-273.15def convert_temperature(value, from_unit, to_unit):"""Convert a temperature from one unit to another. The temperature is first converted to Celsius and then converted from Celsius to the desired unit. Parameters ---------- value : float Temperature to convert. from_unit : str Original unit: "F" (Fahrenheit), "C" (Celsius), or "K" (Kelvin). Not case-sensitive. to_unit : str Desired unit: "F", "C", or "K". Not case-sensitive. Returns ------- float Temperature in the desired unit. Raises ------ ValueError If either unit is not "F", "C", or "K" or if the temperature is below absolute zero. Examples -------- >>> convert_temperature(212, "F", "C") 100.0 >>> convert_temperature(0, "c", "k") 273.15 """ from_unit = from_unit.upper() to_unit = to_unit.upper()for unit in (from_unit, to_unit):if unit notin VALID_UNITS:raiseValueError(f"'{unit}' is not a valid unit. Use F, C, or K.")# Step 1: convert the original value to Celsius.if from_unit =="F": celsius = (value -32) *5/9elif from_unit =="K": celsius = value + ABSOLUTE_ZERO_Celse: celsius = value# Round before comparing so that floating point error does not flag# exactly absolute zero (for example, -459.67 F) as invalid.ifround(celsius, 6) < ABSOLUTE_ZERO_C:raiseValueError(f"{value}{from_unit} is below absolute zero.")# Step 2: convert from Celsius to the desired unit.if to_unit =="F":return celsius *9/5+32elif to_unit =="K":return celsius - ABSOLUTE_ZERO_Celse:return celsius
Once the function is defined, its docstring can be printed using help().
help(convert_temperature)
Help on function convert_temperature in module __main__:
convert_temperature(value, from_unit, to_unit)
Convert a temperature from one unit to another.
The temperature is first converted to Celsius and then converted from
Celsius to the desired unit.
Parameters
----------
value : float
Temperature to convert.
from_unit : str
Original unit: "F" (Fahrenheit), "C" (Celsius), or "K" (Kelvin).
Not case-sensitive.
to_unit : str
Desired unit: "F", "C", or "K". Not case-sensitive.
Returns
-------
float
Temperature in the desired unit.
Raises
------
ValueError
If either unit is not "F", "C", or "K" or if the temperature is
below absolute zero.
Examples
--------
>>> convert_temperature(212, "F", "C")
100.0
>>> convert_temperature(0, "c", "k")
273.15
We now test the updated function. Lowercase units now work, and invalid input results in an error. Here, we use try and except to catch the errors so that the messages are printed without stopping the code. Without try and except, the error would stop execution.
Since the examples in a docstring include the expected output, they can be used as simple tests. The doctest module, which is included with Python, finds the examples in a file’s docstrings, runs them, and reports whether the output matches. For example, python -m doctest -v scripts/convert_temp.py runs the examples for the script built in this chapter.
7.6 Anatomy of a Script File
A script is simply a text file with a .py extension, and it can be written in any text editor or IDE. Although Python does not require a specific structure, scripts are easier to read and maintain when they follow a consistent layout. A typical script is organized in the following order:
Shebang line (optional): On macOS and Linux, a first line of #!/usr/bin/env python allows the script to be run directly (for example, ./convert_temp.py) as opposed to python convert_temp.py. It is ignored on Windows, so it does no harm to include it.
Module docstring: A docstring at the very top of the file describes the purpose of the script and how to use it. It is returned when the script is imported and help() is applied to it.
Imports: All import statements are placed together near the top of the file so that it is easy to see what the script depends on.
Constants: Values that are used throughout the script and do not change.
Functions: The functions that perform the work, each with its own docstring.
A main() function: A function that reads the user’s input, calls the other functions, and prints or saves the results.
The main guard: An if __name__ == "__main__": statement that calls main() only when the file is run as a script. This is discussed in more detail later in the chapter.
The skeleton below demonstrates this structure.
script_template.py
#!/usr/bin/env python"""One-line summary of what the script does.Longer description of the script and examples of how to run it."""import sysCONSTANT_NAME =10def helper_function(x):"""Summary of what the function does."""return x * CONSTANT_NAMEdef main():"""Run the script."""print(helper_function(2))return0if__name__=="__main__": sys.exit(main())
PEP 8, the style guide for Python code, also recommends using four spaces for each level of indentation, two blank lines between top-level functions, lowercase function and variable names with words separated by underscores (for example, convert_temperature), and lines that are not overly long. Following these conventions makes your code easier for others, and your future self, to read.
7.7 Running a Script
To run a script, open a command-line window, activate the conda environment that contains Python and any required libraries, change the working directory to the folder that contains the script (or the project folder), and then call python followed by the path to the script. Below, cd (change directory) is used to move to the folder where the book files were unzipped. You will need to change the path to match the location on your computer.
Anaconda Prompt or Miniforge Prompt (Windows)
conda activate pybookcd C:\Users\yourname\Documents\pyBookpython scripts\convert_temp.py 98.6 F C
Terminal (macOS or Linux)
conda activate pybookcd ~/Documents/pyBookpython scripts/convert_temp.py 98.6 F C
The working directory matters because relative file paths are interpreted relative to the folder from which the script is run, not the folder in which the script is saved. In the examples above, the script is called from the pyBook folder, so the path to the script includes the scripts folder. If you run a script and it cannot find a file, check the working directory and the paths used in the code. You can confirm which Python installation is being used with python --version and where python on Windows or which python on macOS and Linux.
On Windows, paths can be typed with backslashes or forward slashes at the command line. In the remainder of the chapter, we use forward slashes.
7.8 Command-Line Arguments
7.8.1 sys.argv
When a script is run from the command line, the values typed after the script name are its command-line arguments. The simplest way to access them is with the argv variable from the sys module. sys.argv is a list of strings in which the first element is the name of the script and the remaining elements are the arguments in the order in which they were provided.
The script below uses the first version of our function. When it is run, it prints sys.argv so that we can see what it contains. It then extracts the three arguments by their index. Since all arguments are read in as strings, the temperature must be converted to a float using float() before it can be used in calculations.
scripts/convert_temp_argv.py
"""Convert a temperature using arguments read with sys.argv.Usage: python convert_temp_argv.py <temperature> <from_unit> <to_unit>"""import sysdef convert_temperature(value, from_unit, to_unit):"""Convert a temperature between "F", "C", and "K"."""if from_unit =="F": celsius = (value -32) *5/9elif from_unit =="K": celsius = value -273.15else: celsius = valueif to_unit =="F":return celsius *9/5+32elif to_unit =="K":return celsius +273.15else:return celsiusif__name__=="__main__":print(f"sys.argv: {sys.argv}") temperature =float(sys.argv[1]) from_unit = sys.argv[2] to_unit = sys.argv[3] result = convert_temperature(temperature, from_unit, to_unit)print(f"{temperature}{from_unit} = {result:.2f}{to_unit}")
When the script is run with the expected arguments, it works as intended.
python scripts/convert_temp_argv.py 98.6 F C
sys.argv: ['scripts/convert_temp_argv.py', '98.6', 'F', 'C']
98.6 F = 37.00 C
However, this approach has several limitations. If the units are provided in lowercase, an incorrect result is returned with no indication that anything is wrong.
python scripts/convert_temp_argv.py 98.6 f c
sys.argv: ['scripts/convert_temp_argv.py', '98.6', 'f', 'c']
98.6 f = 98.60 c
If an argument is missing, the script fails with an IndexError that does not explain to the user what they did wrong.
python scripts/convert_temp_argv.py 98.6 F
sys.argv: ['scripts/convert_temp_argv.py', '98.6', 'F']
Traceback (most recent call last):
File "scripts\convert_temp_argv.py", line 30, in <module>
to_unit = sys.argv[3]
~~~~~~~~^^^
IndexError: list index out of range
In summary, when using sys.argv, you are responsible for checking the number of arguments, converting data types, validating the values, writing error messages, and documenting how the script should be called. All arguments must also be provided in a fixed order, and there is no simple way to make an argument optional. For anything more than a quick, personal script, it is better to use argparse.
7.8.2 argparse
The argparse module, which is included with Python, handles these tasks for you. You describe the arguments that the script accepts, and argparse reads them from the command line, converts them to the correct data types, checks that the values are valid, writes a usage message and error messages, and automatically creates a help page.
Below is the function from our final script that defines the arguments. The process consists of three steps:
An ArgumentParser object is created. The description is printed at the top of the help page, and the epilog is printed at the bottom.
Each argument is defined using the add_argument() method.
The arguments are read using the parse_args() method, which returns a Namespace object in which each argument is stored as an attribute. For example, the temperature is accessed as args.temperature.
def parse_args(argv=None): parser = argparse.ArgumentParser( description="Convert a temperature between Fahrenheit (F), Celsius (C), and Kelvin (K).", epilog="Example: python convert_temp.py 98.6 F C", ) parser.add_argument("temperature", type=float,help="temperature value to convert") parser.add_argument("from_unit", type=str.upper, choices=VALID_UNITS, metavar="from_unit", help="original unit: F, C, or K") parser.add_argument("to_unit", type=str.upper, choices=VALID_UNITS, metavar="to_unit", help="desired unit: F, C, or K") parser.add_argument("-d", "--decimals", type=int, default=2,help="number of decimal places in the result (default: %(default)s)") parser.add_argument("-q", "--quiet", action="store_true",help="print only the converted value")return parser.parse_args(argv)
argparse supports two types of arguments:
Positional arguments are required and are identified by their position. Their names do not start with a dash. Here, temperature, from_unit, and to_unit are positional arguments, so they must be provided, in that order, every time the script is run.
Optional arguments are identified by a name that starts with one dash (short form, such as -d) or two dashes (long form, such as --decimals). They can be provided in any order or left out, in which case the default value is used. It is common to provide both a short and a long form of the name.
The parameters of add_argument() control how each argument is handled:
type: a function that is applied to the string read from the command line. type=float converts the temperature to a float, and type=int converts the number of decimal places to an integer. If the conversion fails, an error message is printed. type=str.upper converts the units to uppercase so that the user can provide either “f” or “F”.
choices: a list or tuple of allowed values. Any other value results in an error message that lists the valid options.
default: the value used when an optional argument is not provided.
action="store_true": creates a flag, or an optional argument that does not take a value. If -q is included, args.quiet is True. If it is left out, args.quiet is False.
help: the description of the argument that is printed on the help page. %(default)s is replaced with the default value.
metavar: the name used for the argument in the usage message and help page. Without it, arguments that have choices are displayed using the list of choices ({F,C,K}), which makes the two unit arguments indistinguishable.
Note that parse_args() accepts an optional list of strings. If no list is provided, the arguments are read from the command line. Providing a list allows the argument parsing to be tested from within Python without opening a command-line window, which we demonstrate later in the chapter.
7.8.3 Using the Script
Once the arguments are defined, argparse automatically adds a -h or --help option that prints the help page. This means that a user can learn how to use the script without reading the code.
python scripts/convert_temp.py --help
usage: convert_temp.py [-h] [-d DECIMALS] [-q] temperature from_unit to_unit
Convert a temperature between Fahrenheit (F), Celsius (C), and Kelvin (K).
positional arguments:
temperature temperature value to convert
from_unit original unit: F, C, or K
to_unit desired unit: F, C, or K
options:
-h, --help show this help message and exit
-d DECIMALS, --decimals DECIMALS
number of decimal places in the result (default: 2)
-q, --quiet print only the converted value
Example: python convert_temp.py 98.6 F C
The examples below demonstrate several valid calls to the script: a basic conversion, lowercase units, a negative temperature, a custom number of decimal places, and the quiet flag. Note that the optional arguments can be placed before or after the positional arguments.
python scripts/convert_temp.py 98.6 F Cpython scripts/convert_temp.py 212 f cpython scripts/convert_temp.py -40 C Fpython scripts/convert_temp.py 300 K F --decimals 1python scripts/convert_temp.py -q 0 C K
The quiet flag is useful when the output of the script will be used by another program or command. For example, in PowerShell, $f = python scripts/convert_temp.py 300 K F -q saves the converted value to a variable.
Since names that start with a dash are interpreted as optional arguments, you might expect -40 to cause a problem. argparse treats values that look like negative numbers as positional arguments as long as none of the script’s optional arguments themselves look like negative numbers, so -40 is read as the temperature.
When the user provides invalid input, argparse prints the usage message and a description of the problem, and the script stops. The examples below demonstrate an invalid unit, a missing argument, and a temperature that cannot be converted to a float. Compare these messages to the IndexError returned by the sys.argv version of the script.
python scripts/convert_temp.py 98.6 F Xpython scripts/convert_temp.py 98.6 Fpython scripts/convert_temp.py abc F C
usage: convert_temp.py [-h] [-d DECIMALS] [-q] temperature from_unit to_unit
convert_temp.py: error: the following arguments are required: to_unit
[exit code: 2]
argparse cannot check whether the temperature is above absolute zero since this depends on the units. This check is performed by convert_temperature(), which raises a ValueError. In the main() function, the error is caught with try and except and a short message is printed as opposed to a full traceback.
python scripts/convert_temp.py -500 F C
Error: -500.0 F is below absolute zero.
[exit code: 1]
7.8.4 Exit Codes
When a script finishes, it returns an exit code to the shell, which is shown in brackets in the output above. By convention, an exit code of 0 indicates success, and any other value indicates that an error occurred. argparse uses an exit code of 2 for invalid arguments. In our script, main() returns 0 if the conversion succeeds and 1 if it fails, and sys.exit(main()) passes this value to the shell. Exit codes allow other scripts, scheduled tasks, and automated workflows to check whether a step succeeded before moving on to the next step. The exit code of the last command can be printed using echo $LASTEXITCODE in PowerShell, echo %ERRORLEVEL% in the Command Prompt, or echo $? on macOS and Linux.
Error messages are written to the standard error stream (sys.stderr) as opposed to standard output (sys.stdout), which is where print() writes by default. Both are displayed in the command-line window. However, when the output of a script is saved to a file or passed to another program, only standard output is included, so error messages do not get mixed in with the results.
7.9 The Complete Script
The complete script is provided below. It follows the structure described above: a shebang line, a module docstring with examples, imports, constants, the convert_temperature() function, the parse_args() function, the main() function, and the main guard. Each function has a docstring. The main() function is kept short: it reads the arguments, calls convert_temperature(), handles any error, and prints the result using an f-string. Note the use of a nested f-string format specification, {result:.{args.decimals}f}, which rounds the result to the number of decimal places provided by the user.
scripts/convert_temp.py
#!/usr/bin/env python"""Convert a temperature between Fahrenheit, Celsius, and Kelvin.This script can be run from the command line or imported as a module sothat the convert_temperature() function can be used in other code.Examples--------Convert 98.6 degrees Fahrenheit to Celsius: python convert_temp.py 98.6 F CConvert 300 Kelvin to Fahrenheit and round the result to 1 decimal place: python convert_temp.py 300 K F --decimals 1"""import argparseimport sysVALID_UNITS = ("F", "C", "K")UNIT_NAMES = {"F": "Fahrenheit", "C": "Celsius", "K": "Kelvin"}ABSOLUTE_ZERO_C =-273.15def convert_temperature(value, from_unit, to_unit):"""Convert a temperature from one unit to another. The temperature is first converted to Celsius and then converted from Celsius to the desired unit. Parameters ---------- value : float Temperature to convert. from_unit : str Original unit: "F" (Fahrenheit), "C" (Celsius), or "K" (Kelvin). Not case-sensitive. to_unit : str Desired unit: "F", "C", or "K". Not case-sensitive. Returns ------- float Temperature in the desired unit. Raises ------ ValueError If either unit is not "F", "C", or "K" or if the temperature is below absolute zero. Examples -------- >>> convert_temperature(212, "F", "C") 100.0 >>> convert_temperature(0, "c", "k") 273.15 """ from_unit = from_unit.upper() to_unit = to_unit.upper()for unit in (from_unit, to_unit):if unit notin VALID_UNITS:raiseValueError(f"'{unit}' is not a valid unit. Use F, C, or K.")# Step 1: convert the original value to Celsius.if from_unit =="F": celsius = (value -32) *5/9elif from_unit =="K": celsius = value + ABSOLUTE_ZERO_Celse: celsius = value# Round before comparing so that floating point error does not flag# exactly absolute zero (for example, -459.67 F) as invalid.ifround(celsius, 6) < ABSOLUTE_ZERO_C:raiseValueError(f"{value}{from_unit} is below absolute zero.")# Step 2: convert from Celsius to the desired unit.if to_unit =="F":return celsius *9/5+32elif to_unit =="K":return celsius - ABSOLUTE_ZERO_Celse:return celsiusdef parse_args(argv=None):"""Define and read the command-line arguments. Parameters ---------- argv : list of str, optional Arguments to parse. If None (the default), the arguments provided at the command line are used. Returns ------- argparse.Namespace Parsed arguments, which are accessed as attributes (for example, args.temperature). """ parser = argparse.ArgumentParser( description="Convert a temperature between Fahrenheit (F), Celsius (C), and Kelvin (K).", epilog="Example: python convert_temp.py 98.6 F C", ) parser.add_argument("temperature", type=float,help="temperature value to convert") parser.add_argument("from_unit", type=str.upper, choices=VALID_UNITS, metavar="from_unit", help="original unit: F, C, or K") parser.add_argument("to_unit", type=str.upper, choices=VALID_UNITS, metavar="to_unit", help="desired unit: F, C, or K") parser.add_argument("-d", "--decimals", type=int, default=2,help="number of decimal places in the result (default: %(default)s)") parser.add_argument("-q", "--quiet", action="store_true",help="print only the converted value")return parser.parse_args(argv)def main(argv=None):"""Run the temperature conversion from the command line. Parameters ---------- argv : list of str, optional Arguments to parse. If None (the default), the arguments provided at the command line are used. Returns ------- int Exit code: 0 if the conversion succeeded and 1 if it failed. """ args = parse_args(argv)try: result = convert_temperature(args.temperature, args.from_unit, args.to_unit)exceptValueErroras err:print(f"Error: {err}", file=sys.stderr)return1if args.quiet:print(f"{result:.{args.decimals}f}")else:print(f"{args.temperature}{UNIT_NAMES[args.from_unit]} = "f"{result:.{args.decimals}f}{UNIT_NAMES[args.to_unit]}")return0if__name__=="__main__": sys.exit(main())
7.10 Importing a Script as a Module
7.10.1 The Main Guard
Any .py file can be imported as a module, just like math or os (Chapter 2). When a file is imported, all of the code in it is executed. Without a main guard, importing our script would immediately try to read arguments from the command line and run a conversion, which is not what we want.
Python sets a special variable, __name__, for each file that it runs. When a file is run directly as a script, __name__ is set to "__main__". When a file is imported, __name__ is set to the name of the module (in this case, "convert_temp"). So, the code inside if __name__ == "__main__": only runs when the file is executed as a script. This allows the same file to serve as both a command-line tool and a module of reusable functions.
7.10.2 Reusing the Function
Below, we import the script from within Python. Since the scripts folder is not on the list of locations that Python searches for modules, we add it to sys.path first. Note that importing the module does not print anything since main() is not called. We can then print the module docstring, use the convert_temperature() function directly, and apply it to many values using list comprehension.
Convert a temperature between Fahrenheit, Celsius, and Kelvin.
This script can be run from the command line or imported as a module so
that the convert_temperature() function can be used in other code.
Examples
--------
Convert 98.6 degrees Fahrenheit to Celsius:
python convert_temp.py 98.6 F C
Convert 300 Kelvin to Fahrenheit and round the result to 1 decimal place:
python convert_temp.py 300 K F --decimals 1
temps_f = [32, 50, 68, 86, 104]temps_c = [round(convert_temp.convert_temperature(t, "F", "C"), 1) for t in temps_f]print(temps_c)
[0.0, 10.0, 20.0, 30.0, 40.0]
Since main() accepts an optional list of arguments, the full command-line behavior can also be tested from within Python by providing the arguments as a list of strings. This is equivalent to running python scripts/convert_temp.py 300 K F --decimals 1 at the command line.
In a larger project, it is common to store reusable functions in one or more module files and to keep the scripts that are run from the command line short. The scripts import the functions they need, read the user’s arguments, and call the functions. This avoids copying the same function into multiple scripts.
7.11 Running a Script on Many Inputs
One of the main benefits of a command-line script is that it can be called repeatedly using a loop in the shell. The examples below convert a set of temperatures from Fahrenheit to Celsius. In a geospatial workflow, the same approach could be used to apply a processing script to every raster file in a folder by providing the input and output file paths as arguments.
PowerShell (Windows)
foreach($tin32,50,68,86){ python scripts/convert_temp.py$t F C }
Terminal (macOS or Linux)
for t in 32 50 68 86;dopython scripts/convert_temp.py $t F C;done
7.12 Concluding Remarks
In this chapter, we discussed when it makes sense to run code at the command line as opposed to interactively, how to organize a script, how to document code using docstrings, and how to accept user-provided arguments using sys and argparse. Although the temperature conversion example is simple, the same pattern applies to more complex processes. For example, a script could accept the path to an input raster, the path to an output file, and processing settings and then use Rasterio (Chapter 10) to complete the analysis.
If you would like to explore further, the argparse tutorial in the Python documentation describes additional options, such as subcommands and mutually exclusive arguments. The logging module can be used to record the progress of long-running scripts, and the pathlib module simplifies working with file paths provided as arguments. Third-party libraries, such as Click and Typer, offer alternative approaches to building command-line interfaces. Lastly, a collection of scripts and modules can be organized into a Python package that is installed into an environment, which allows its commands to be run from any folder.
7.13 Questions
List two advantages and two disadvantages of running a process at the command line as opposed to within an IDE, notebook, or Quarto document.
Explain the difference between a comment and a docstring. How can the docstring of a function be accessed?
List the components of a typical script file in the order in which they generally appear.
Explain the purpose of if __name__ == "__main__":. What would happen if a script without it were imported as a module?
What does sys.argv contain? What are two limitations of reading arguments using sys.argv?
Explain the difference between positional and optional arguments when using argparse. What is a flag?
Explain the purpose of the type and choices parameters of the add_argument() method.
What is an exit code? What do exit codes of 0 and non-zero values conventionally indicate, and why are they useful?
Why does the working directory matter when running a script that uses relative file paths?
7.14 Exercises
Exercise 7.1: A Command-Line Distance Calculator
Introduction
In this exercise, you will build a documented Python script that can be run from the command line and imported as a module. The script will calculate the great-circle distance between two locations on the Earth’s surface given their latitude and longitude, which is a common task when working with geospatial point data. Great-circle distance can be calculated using the haversine formula:
where \(\phi_1\) and \(\phi_2\) are the latitudes of the two points, \(\lambda_1\) and \(\lambda_2\) are their longitudes (all in radians), and \(r\) is the radius of the Earth (use 6,371 km).
Use only modules that are included with Python (e.g., math, argparse, sys, and csv). Follow the structure, documentation, and argument-handling practices demonstrated in Chapter 7.
Objectives
Organize a script file using a consistent structure
Document a module and its functions using docstrings
Accept and validate user-provided arguments using argparse
Return informative error messages and exit codes
Reuse the script’s functions by importing it as a module
Deliverables
The script file (distance.py)
A short document (Markdown, Quarto, or plain text) that includes the commands you ran, their output, and answers to the questions below
Questions and Tasks
Task 1. Create a file named distance.py that includes, in order: a shebang line, a module docstring, imports, constants (e.g., the Earth’s radius and the supported units), functions, a main() function, and the main guard (if __name__ == "__main__":).
Task 2. Write a function named haversine_distance() that accepts the latitude and longitude of two points in decimal degrees and returns the distance in kilometers. Write a second function named convert_distance() that converts a distance in kilometers to miles (“mi”), meters (“m”), or kilometers (“km”).
Task 3. Write a NumPy-style docstring for each function that includes a one-line summary and Parameters, Returns, Raises, and Examples sections. Include at least two examples in each docstring. For example, the distance between Morgantown, WV (39.6295, -79.9559) and Pittsburgh, PA (40.4406, -79.9959) is approximately 90 km.
Task 4. Validate the input values. haversine_distance() should raise a ValueError with an informative message if a latitude is not between -90 and 90 degrees or a longitude is not between -180 and 180 degrees. convert_distance() should raise a ValueError if an unsupported unit is provided.
Task 5. Use argparse to define the following arguments:
Four positional arguments: lat1, lon1, lat2, and lon2 (all floats)
An optional -u/--units argument that accepts “km”, “mi”, or “m” (default: “km”)
An optional -d/--decimals argument that defines the number of decimal places in the printed result (default: 2)
A -q/--quiet flag that prints only the number
Provide a description, an epilog with an example command, and a help message for every argument. Use the metavar parameter where it improves the help page.
Task 6. In main(), catch any ValueError raised by your functions, print an error message to standard error, and return an exit code of 1. Return 0 if the calculation succeeds. Pass the returned value to sys.exit() in the main guard.
Task 7. Run the script from the command line to (1) print its help page, (2) calculate the distance between Morgantown and Pittsburgh in kilometers and in miles, (3) calculate a distance using the quiet flag, and (4) provide an invalid latitude and an invalid unit. Include the commands and their output in your deliverable. Note that negative longitudes are provided directly (e.g., -79.9559). What exit codes were returned for the invalid inputs, and why do they differ?
Task 8. Use the doctest module (python -m doctest -v distance.py) to run the examples in your docstrings. Fix any examples that fail. (Hint: round the results in your examples so that small floating point differences do not cause failures.)
Task 9. In a separate Python session, Jupyter notebook, or Quarto document, import your script as a module, print its module docstring, and use haversine_distance() and a list comprehension to calculate the distance from Morgantown to each of three other cities of your choosing. Explain why importing the script did not run main().
Task 10 (Challenge). Add an optional --csv argument that accepts the path to a CSV file with “name”, “lat”, and “lon” columns. When it is provided, the script should ignore the second point and print the distance from the first point to every location in the file. Use the csv module to read the file, and explain how you handled the positional arguments that are no longer needed in this mode.