8. Scripting with import, Libraries, and Files#

8.1. Lesson overview#

In a previous lesson, we covered how we can use functions to bundle up reusable code blocks. These functions will work in the JupyterLab notebook or the Python file where we defined it, but what if we want to use it in another notebook or Python file? Are we forced to copy and paste these functions over to the new location if we plan on reusing them in another program?

An important “Pythonic” style convention is to avoid duplication of code whenever possible, and with the import built-in keyword we can import Python code from one program into another, whether that is a separate Python file or a JupyterLab notebook. In this lesson, we will demonstrate this ability by first creating a basic Python code file that contains a function and then import this function into a JupyterLab notebook. Then we will import and explore other built-in libraries, including the external library NumPy. Finally, we will cover how to read and write to external files with Python.

8.2. The import keyword#

The import built-in keyword is used to import code from a module (i.e., a file that contains Python code) into your code. To learn how this all works, we will first create a Python file that contains a function definition (a.k.a. a module!), and then we will import the module into a JupyterLab notebook using import.

Definition: module

Another name for a file that contains Python code.

8.2.1. Creating the module#

Open up Spyder, the Python IDE that we covered in Getting Started. Create a new file and copy the following example code:

def resistance(i, v):         
    # return resistance
    return i * v

Save the file as “resistor.py” to a convenient working folder.

8.2.2. Loading the module into JupyterLab using import#

Now launch JupyterLab from Anaconda Navigator as covered in Getting Started. Use the JupyterLab file manager to navigate to the folder where you saved “resistor.py”. In this folder create a new Jupyter notebook and in the first code cell, type the following:

import resistor

The import resistor statement will scan for any files in the current directory that are named “resistor.py” and will import the entire file as a module object named resistor (i.e., it names the module object based on the filename). The resistance function can then be called through the imported resistor module:

import resistor
r = resistor.resistance(i = 10, v = 5)

Note how we call resistor.resistance() to access the resistance() function. To cut down on keystrokes, we can alternatively import just resistance() function from the resistor module via the command:

from resistor import resistance
r = resistance(i = 10, v = 5)

The from keyword can only be used with the import keyword to import specific code from a Python module. The from resistor import resistance import pattern means that the module resistor will be inspected to find some Python code that is named resistance, which will then be imported. You can import any Python code from a module, like a variable, function, class, or in fact another module.

Let us create a new Python file in the same folder as the “resistor.py” file. Open up Spyder, type the following code, and save it as “ohms_law.py”:

from resistor import resistance

def voltage(i, r):         
    # return voltage
    return i * r
    
def current(v, r):         
    # return current
    return v / r
    
CU_CONDUIT = 58.0

Now we can import multiple things from the “ohms_law.py” file as a module:

from ohms_law import resistance, voltage, CU_CONDUIT
r = resistance(i = 10, v = 5)
v = voltage(i = 10, r = r)
print(r, v, CU_CONDUIT)

In this example, resistance() is imported via a two-step route: it is first called via the ohms_law module import (“ohms_law.py”), which then imports resistance() from “resistor.py”. We also imported another function and variable from theohms_law module by typing their names using the statement import resistance, voltage, CU_CONDUIT. Notice that we use a comma to separate the various objects we wished to import. We did not import current from ohms_law, so that function is unavailable to our code. It is usually a good practice to import the specific functionality you need from a module with from - import pattern instead of importing the top level module, but that is by no means a hard rule.

8.2.3. Example: Importing your work#

Let’s try importing two functions from a previous lesson. First, create a Python file called importExample.py and copy the functions my_name() and convert_force() to this file. Next, open up a JupyterLab session in the same folder as your importExample.py file and import both functions using the from - import structure. Finally, demonstrate that both functions work by running them in the interactive shell.


Solution:

A copy of importExample.py can be found here. This file was made in JupyterLab by creating a new PY file and copying the two functions into the file. From here, a new IPython notebook file is created and the two functions are imported with the following command:

from importExample import my_name, convert_force

The , character allows us to list multiple objects to be imported from a common module. We could have written the from - import scheme two times over with each function, but the above example is more concise.

From here, we can call the two functions to ensure they are working properly. For my_name(), no input arguments are needed. For convert_force(), let’s convert 20 N to pounds-force. The code below issues the function calls:

my_name()
converted_force, converted_units = convert_force(initial_force=20, initial_units="N", converted_units="lbf")
print(f"The converted force is {converted_force} {converted_units}")
My name is Goldy Gopher!
The converted force is 4.5 lbf

As seen above, both functions work and now are deployable in other code!

Note

Remember that we could have imported both functions through the import call import importExample. If we went this route, we would need to include the library name to each function. So our function calls would now be importExample.my_name() and importExample.convert_force().


8.3. Python libraries#

So far, we have created a useful module “ohms_law.py” which lets us import functions instead of duplicating the function in our code. Instead of constantly creating new functions for our needs, we can also import additional modules into Python made by other people.

A Python library is a collection of modules that are gathered into one package. There are two types of Python libraries, the first are “built-in” libraries which are bundled along with Python, and second are “external” libraries which are available online to download. There are over 400,000 external libraries which can be installed with a Python Package Manager (e.g., Anaconda). In this section, let us explore some popular libraries.

Definition: library

A collection of modules that are gathered into one package.

8.3.1. The random library#

The random library is useful for generating random numbers. It is a built-in library found in Python. Let us try importing random and utilizing some functions from it:

from random import randint, random, gauss
print("Random integer 0 - 75:", randint(0, 75))
print("Random float 0 - 1:", random())
print("Random float from gauss distribution with mean = 10 and std = 5:", gauss(10, 5))
Random integer 0 - 75: 51
Random float 0 - 1: 0.8010090432893598
Random float from gauss distribution with mean = 10 and std = 5: 17.933945409951637

In the code above, we try out a few functions from random. The first function used, randint(), returns a random integer from the range of the passed min and max values. Next, the function random() is used to return a random float between 0 - 1. The last function used is gauss(), which returns a random float from a Gaussian (normal) distribution.

8.3.2. The math library#

The math library is a simple and useful built-in library that contains multiple functions and mathematical constants. The code below shows an example of importing the constant pi (i.e., \(\pi\)) and the function log() (i.e., log base 10, \(\log_{10}\)):

from math import pi, log
van_der_pauw_constant = pi / log(2)
print("VDP constant:", round(van_der_pauw_constant, 4))
VDP constant: 4.5324

8.3.3. Example: Random trigonometry#

Using both the random and math libraries, create a simple code block that displays the cosine of five random numbers between 0 - 2\(\pi\). Output values should be floating point numbers with three significant figures after the decimal point. You will need to look at math library’s documentation to see how to implement a cosine function in Python.


Solution:

There are a few ways one can do this depending on your overall Python knowledge. Since we have not covered object-oriented programming or the NumPy library yet, we will use a for loop to take the cosine of five random numbers between 0 - 2\(\pi\). An example code block is shown below:

from random import random
from math import cos, pi

for i in range(5):
    value = random() * (2 * pi)
    output = cos(value)
    print(f"The cosine of {value:.3f} is {output:.3f}.")
The cosine of 1.063 is 0.486.
The cosine of 6.050 is 0.973.
The cosine of 3.405 is -0.966.
The cosine of 1.303 is 0.264.
The cosine of 3.149 is -1.000.

8.3.4. The time library#

The time built-in library has useful functionality for getting the current time or sleeping a python program to pause execution. The code below imports time() and sleep() from the time library and then runs a for loop where the program delays for one second and then prints out a timestamp:

from time import time, sleep
start_time = time()
for i in range(4):
    # sleep 1 second
    sleep(1)
    # get a timestamp
    timestamp = time() - start_time
    print("timestamp:", round(timestamp, 4))
timestamp: 1.0012
timestamp: 2.0027
timestamp: 3.0042
timestamp: 4.0057

8.3.5. The datetime library#

Similar to the time library, the built-in datetime library has useful functionality to parse and work with dates. The example below creates a datetime object named today and then formats the object into a string with the .strftime() datetime method.

from datetime import datetime
today = datetime.today()
print("Datetime object:", today)
today_string = today.strftime("%B %d %Y")
print("Formatted date string:", today_string)
Datetime object: 2024-08-08 09:10:03.980956
Formatted date string: August 08 2024

8.3.6. The NumPy library#

NumPy is not a built-in Python library, but it is included in the default Anaconda base environment. It is an open source library that focuses on scientific computing. NumPy is a commonly used library for many scientific applications, and we will explore this library further in a later chapter. For now, let us rewrite our example code from the math library example, but now using NumPy:

import numpy as np

van_der_pauw_constant = np.pi / np.log(2)
print("VDP constant:", round(van_der_pauw_constant, 4))
VDP constant: 4.5324

Notice in this example we introduce the as keyword in conjunction with import. The import - as pattern allows you to import something using an alias name. Here, we import the numpy library using the alias np, which is commonly done with this library to cut down on keystrokes. Therefore, we do not have to type out numpy.pi to utilize the value of \(\pi\), but instead we can simply type np.pi.

8.4. Finding documentation#

There are a few ways we can investigate what functions and constants a library offers. One way we can do this is to first import the library into Python and then pass its name through the help() function. For example, we can do this to the math library with the following commands:

import math
help(math)

Furthermore, Python modules are simply Python code files, so we can look at the source file to understand how a library works. For example, the random source file can be in the Python GitHub repository. This file also exists on your computer, and when you call from random import random, you are importing the random.py file as a module.

Alternatively, you can search online to understand a library (e.g., “python docs library name”). Online documentation often will include “Getting Started” guides and tutorials to get you familiar with the library. Since many libraries have documentation that is unwieldy to display with the help() command, reading a library’s documentation is often best suited through a web browser.

8.5. Working with files#

Often you will want to read a text file as input or write a text file as output of your Python program. The basic functionality of opening a file for reading or writing is done with the open() built-in function. The open() function takes in one required positional argument and many optional keyword arguments:

open(file, mode = "r", buffering = -1, encoding = None, errors = None, newline = None, closefd = True, opener = None)

The file positional argument specifies the path to the file to be opened. Of the optional keyword arguments, the mode argument is the one you will use the most. The mode specifies how open() should treat the file that is being opened. There are several modes, and you can read up on all the modes in the documentation, but the most common are r for read, w for write, a for append. By default, the mode is set to r to simply read the file. Below is a summary of the behavior, and we will try each out in the following examples.

Mode

Behavior

w

Creates file or clears the file contents, opens it for writing.

r

Opens the file only for reading, writing is not allowed.

a

Opens the file for writing, appends content to the end of the file

As a reminder, you can check out all the arguments and modes of open() with the help() function:

help(open)

8.5.1. File paths#

A file path is a string that contains a listing of the folders and subfolders to where the file is located followed by the name of the file. For example, we can generate the file path of a file called tutorial_test.txt with the following commands:

# generate a file path with current directory
directory = "./"
filename = "tutorial_test.txt"
file_path = directory + filename
print(file_path)
./tutorial_test.txt

The directory ./ is shorthand for the current directory that the Python shell is operating in. If you are unsure of what directory you are in, you can use the convenience function %pwd that is available in JupyterLab to get the absolute folder path of the current working folder:

# generate a file path using JupyterLab command
directory = %pwd
filename = "tutorial_test.txt"
file_path = directory + "/" + filename
print(file_path)
/srv/docs/intro/CEMS_Python_Toolkit/tutorial_test.txt

Notice that %pwd will not include a trailing slash, so if we want to include our filename in file_path we need to add a / between directory and filename.

We can also get the absolute file path for a file in Python using the os.path built-in module:

# generate a file path with absolute path using os.path
from os import path
directory = path.abspath(".")    # get absolute path from the current directory
filename = "tutorial_test.txt"
file_path = directory + "/" + filename
print(file_path)
/srv/docs/intro/CEMS_Python_Toolkit/tutorial_test.txt

Hey! Listen!

The words “directory” and “folder” are often used interchangeably; they both refer to file containers in a computer file system.

8.5.2. Writing to a file#

Danger

In the examples below, we will be using the w mode that will clear contents of a file being opened. We will be running code blocks with “tutorial_test.txt” as the file path. If you happen to have data saved with that file name, make sure you change the file name before trying out the code below. Do not blame this tutorial for eating your homework.

Let us first try writing to a file. The following code block will open the file called “tutorial_test.txt” in the current directory:

# write data to a file called "tutorial_test.txt" in current directory
f = open("./tutorial_test.txt", "w")
f.write("hello world")
f.close()

This is the most basic using the open() function to write a file. We pass the file path to the function as the first argument, "./tutorial_test.txt", and then specify the mode we want to open the file (in this case w) as the second argument. We assign f to be the file object that represents the opened file. We then run the write() function to write contents to the now empty file. Finally, we close the file object with f.close() to make sure the file is released from editing. If you use a text editor like TextEdit on macOS or Notepad on Windows to open the file “tutorial_test.txt”, you should see “hello world” printed out.

Hey! Listen!

If the file does not already exist, w mode will create it. Therefore, w mode will either create a file or clear out the contents of a pre-existing file.

8.5.3. Using with with files#

This simple way of editing files works, but it does require keeping track of all the open files and making sure they are closed. The recommended way to work on files is to utilize the with keyword to edit a file through the with open() as f: statement followed by a code block that contains the write() function.

Let us re-open our “tutorial_test.txt” file and write some different text:

# write data to a file called "tutorial_test.txt" in current directory
with open("./tutorial_test.txt", "w") as f:
    f.write("goodbye world")

Notice that the code block above does not have the command f.close() anywhere. That is because f.close() gets automatically run at the end of the with code block. Using the with open() as f: pattern is useful for containing all the logic around writing or reading to a file in one code block. We will be using this pattern for the rest of the examples.

If you open up “./tutorial_test.txt” again in a text editor, you will see the content of the file has changed. It is now only “goodbye world”, as “hello world” was deleted when the file was opened using the w mode.

8.5.4. Reading a file#

Now instead of using a text editor to open a file, let us use Python to read the contents of a file:

data = ""
# read data from "./tutorial_test.txt"
with open("./tutorial_test.txt", "r") as f:
    data = f.read()
print("File data:", data)
File data: goodbye world

When a file is opened in r mode (i.e., read mode), you can use the f.read() function to read the entire contents of the file. In this case, “goodbye world” was printed out due to our previous write command. Let us try to put “hello world” back at the beginning of the file using two simple steps:

data = ""
# read data from "./tutorial_test.txt"
with open("./tutorial_test.txt", "r") as f:
    data = f.read()
    
print("Initial file data:", data)

with open("./tutorial_test.txt", "w") as f:
    f.write("hello world")
    f.write(data)    

with open("./tutorial_test.txt", "r") as f:
    data = f.read()
print("File data after writing:", data)
Initial file data: goodbye world
File data after writing: hello worldgoodbye world

Well we got halfway there, we were able to put “hello world” in the file, but it is not in its own line. To fix this, let us use the newline escape character (\n) that we introduced in an earlier lesson to insert an additional blank output line between the two strings:

with open("./tutorial_test.txt", 'w') as f:
    f.write("hello world" + "\n")
    f.write("goodbye world")    

data = ""
with open("./tutorial_test.txt", 'r') as f:
    data = f.read()
print("file data after writing:", data)
file data after writing: hello world
goodbye world

8.6. Using CSV files with the csv and numpy libraries#

So far we have shown a few simple ways to open, write, and read strings of data to files. In science and engineering applications, strings of data are often stored in an “array-style” format consisting of rows and columns. In order to be able separate out these data strings from one another, a “delimiter character” is often placed between these strings. Commonly used delimiters include commas (,), semicolons (;), quotes (“ ” or ‘ ’), pipes ( | ), slashes ( / or \ ), and tabs.

In particular, comma delimiters are a very popular choice in separating tabulated data. This has led to the creation of the Comma Separated Value (CSV) file format. We have already gone over how we can parse and write files using the open() function and the .read() and .write() methods. These simple commands are somewhat cumbersome in handling strings of data separated with delimiters (e.g., 2.0, 52.1, -73.2). Fortunately, there are both built-in and external libraries that can handle the CSV file format. In this section we will go over two of these libraries.

8.6.1. Writing a file with the csv library#

The built-in csv library is a simple, but powerful library designed to read, write, and modify CSV files. The library has two primary functions when working with CSV files: csv.reader() and csv.writer(). Let us first demonstrate how to write to a CSV file using the `csv.writer() function with the example below:

import csv
from random import gauss

data = []

# generate array of random gaussian distribution (mean 10, std 3) in format of
# [ increment, random_float ]
for i in range(20):
    data.append([i, gauss(mu = 10, sigma = 3)])

# write data to "./tutorial_txt.csv" in current directory
with open("./tutorial_txt.csv", "w", newline = "") as f:
    writer = csv.writer(f, delimiter = ',')
    writer.writerow(["Timestamp", "Data"])
    for i in data:
        writer.writerow(i)

In this example, we demonstrate how csv.writer() works by creating a file that contains data calculated from a Gaussian distribution. We first declare an empty list called data. Next, we utilize a for loop to generate a list of two numbers (i.e., i and the number resulting from the gauss() function call), which are then appended to data using the .append() method.

Afterwards, we open our file using the familiar with open() as f: pattern, but now include the optional argument newline = "". This optional argument ensures that no additional line returns are placed after writing a comma delimited row of data and is recommended when working with CSV files.

Next, we create a writer object that is instantiated by calling csv.writer(). The function csv.writer() takes in a file object as a required first argument, and allows for many optional formatting options. In our example, we specifically specify that delimiter = ",", which is technically redundant since the CSV file format uses , as its delimiter. We have added this for educational purposes in order to show how an optional format can be included.

After creating the writer object, we call the writer.writerow() method with a list that contains the names of each column we want in our file. This will be placed in the first row of our data file, so we can identify each data column. In general, it is highly recommended to include a header row in your data files for readability! We then loop through each entry list entry of data and use .writerow() to write each entry to the file.

8.6.2. Reading a file with the csv library#

Now that we have written a file with csv.writer(), we can try to read it with the csv.reader() function:

data = []

with open("./tutorial_txt.csv", "r", newline = "") as f:
    reader = csv.reader(f, delimiter = ',')
    for i in reader:
        data.append(i)
        
print("Data from file:", data)
Data from file: [['Timestamp', 'Data'], ['0', '8.752231021598503'], ['1', '10.44377105259593'], ['2', '9.774241368892826'], ['3', '16.16364942783288'], ['4', '10.337361596182236'], ['5', '7.607600277776483'], ['6', '7.060097702321116'], ['7', '11.73182015799266'], ['8', '5.076299345672038'], ['9', '8.133984123827288'], ['10', '13.577547142365455'], ['11', '8.687951112429399'], ['12', '1.8212427912863518'], ['13', '8.620258848069469'], ['14', '7.8440773748233115'], ['15', '12.636184221177668'], ['16', '9.56996571490079'], ['17', '9.813435529607816'], ['18', '9.182642778463114'], ['19', '12.970390973691483']]

Similar to before, we have an empty list called data, and we use the with open() as f: pattern to open our file for reading (again adding the additional newline = "" to the open() function). We call the csv.reader() function to create a reader object which we can use to read the file. We then iterate through the reader object to loop through the rows in the file, and we append each row to the data list.

We can see from the print() command that data is a nested list (i.e., a list of lists) made up of strings. Our current implementation of csv.reader() casts all data as str objects, which may not be desirable since we are expecting our values to be float objects. There are a few different ways we can fix this. The modified code below shows how a simple post-processing step after reading the CSV file can remove the header row and casts the rest of the entries as float objects:

data = []

with open("./tutorial_txt.csv", "r", newline = "") as f:
    reader = csv.reader(f, delimiter = ",")
    for i in reader:
        data.append(i)

headers = data.pop(0)
data = [[float(i[0]), float(i[1])] for i in data]
print("Data from file:", data)
Data from file: [[0.0, 8.752231021598503], [1.0, 10.44377105259593], [2.0, 9.774241368892826], [3.0, 16.16364942783288], [4.0, 10.337361596182236], [5.0, 7.607600277776483], [6.0, 7.060097702321116], [7.0, 11.73182015799266], [8.0, 5.076299345672038], [9.0, 8.133984123827288], [10.0, 13.577547142365455], [11.0, 8.687951112429399], [12.0, 1.8212427912863518], [13.0, 8.620258848069469], [14.0, 7.8440773748233115], [15.0, 12.636184221177668], [16.0, 9.56996571490079], [17.0, 9.813435529607816], [18.0, 9.182642778463114], [19.0, 12.970390973691483]]

After reading our CSV file, we “pop” out the first element with the .pop() built-in method for lists. The command .pop(0) will pop out the passed index (i.e., 0) and remove it from the list. Then we use list comprehension to loop through the data, cast the first and second elements of each sublist as a float object, and reassign the whole thing back to data.

Another solution is to change when writer objects generate quotes for data (i.e., making them str objects). According to the csv documentation, we can pass the formatting argument quoting = csv.QUOTE_NONNUMERIC to our csv.writer() and csv.reader() calls to add quotes to all data EXCEPT numerical-based data, which instead will be represented as float objects. This modification is done in the code below:

data = []

# generate array of random gaussian distribution (mean 10, std 3) in format of
# [ increment, random_float ]
for i in range(20):
    data.append([i, gauss(mu = 10, sigma = 3)])

# write data to "./tutorial_txt.csv" in current directory
with open("./tutorial_txt.csv", "w", newline = "") as f:
    writer = csv.writer(f, delimiter = ",", quoting = csv.QUOTE_NONNUMERIC)
    writer.writerow(["Timestamp", "Data"])
    for i in data:
        writer.writerow(i)

data = []

with open("./tutorial_txt.csv", "r", newline = "") as f:
    reader = csv.reader(f, delimiter = ",", quoting = csv.QUOTE_NONNUMERIC)
    for i in reader:
        data.append(i)

print("Data from file:", data)
Data from file: [['Timestamp', 'Data'], [0.0, 12.174863823786264], [1.0, 8.329739925527115], [2.0, 4.00039748716941], [3.0, 11.901080993775455], [4.0, 9.219681978472881], [5.0, 6.7469067964017215], [6.0, 16.573850134436814], [7.0, 10.94091444263089], [8.0, 12.252527074507826], [9.0, 7.078531246520325], [10.0, 5.364872216646133], [11.0, 13.02571898988545], [12.0, 12.236918776356195], [13.0, 12.449399391420256], [14.0, 7.957243008825316], [15.0, 4.320703748030059], [16.0, 6.849109500988111], [17.0, 12.478602677986562], [18.0, 12.33658456801999], [19.0, 14.241707920111716]]

8.6.3. Reading a CSV file with the numpy library#

Yet another solution in regard to casting data in the proper class utilizes the function numpy.load_txt() from the NumPy library to read our text file, skip the header, and cast the entries as floats. Even though we have briefly discussed loading the NumPy library earlier in this lesson and will hold off discussing this library in much more detail in a later lesson, let us use the numpy.loadtxt() function from the numpy library to load our CSV file:

from numpy import loadtxt

data = loadtxt("./tutorial_txt.csv", delimiter = ",", skiprows = 1)

print("Data from file:", data)
Data from file: [[ 0.         12.17486382]
 [ 1.          8.32973993]
 [ 2.          4.00039749]
 [ 3.         11.90108099]
 [ 4.          9.21968198]
 [ 5.          6.7469068 ]
 [ 6.         16.57385013]
 [ 7.         10.94091444]
 [ 8.         12.25252707]
 [ 9.          7.07853125]
 [10.          5.36487222]
 [11.         13.02571899]
 [12.         12.23691878]
 [13.         12.44939939]
 [14.          7.95724301]
 [15.          4.32070375]
 [16.          6.8491095 ]
 [17.         12.47860268]
 [18.         12.33658457]
 [19.         14.24170792]]

This call is remarkably short compared to the other examples. The only required argument for numpy.loadtxt() is the path to the file that will be read. The numpy.loadtxt() function is used to load any type of text file, not just CSV files, so the default delimiter is a space character " ". The delimiter = "," arguments sets the single comma character as the delimiter in our CSV file, and the skiprows = 1 argument skips over the first row that contains our column headers of "Timestamp", "Data".

8.7. Conclusion#

Extending on what we learned when writing functions, we can now import our Python code from one file to another as a Python module. By developing a code base of Python files / modules, we can import common functionality throughout our programs and JupyterLab notebooks. This can cut down on development time and make a more manageable centralized code base. We can also import many built-in and external libraries in Python. We looked at the random, math, time, and datetime built-in libraries, and briefly introduced the external library NumPy. We worked on how to write and read files with Python, how to write and read CSV files with the csv library, and how to read text files with the numpy.loadtxt() function.

There are a lot of Python libraries out there, and it can be difficult to figure out which library is the best to use or how we can go about writing our own library. But we can always run the command import this to remind ourselves of how to write beautiful Python code.

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

8.8. Want to learn more?#

Python Software Foundation - The Random Library
Python Software Foundation - The Math Library
Python Software Foundation - The Time Library
Python Software Foundation - The Datetime Library
NumPy - Documentation
NumPy - GitHub Repository