CptS 481 - Python Software Construction

Unit 17: System Programming

In [2]:
from IPython.display import HTML
HTML(open("../include/notes.css", "r").read())
Out[2]:

Like any other program, your program needs to interact with the operating system. We've already seen one aspect of this: I/O. There are others.

The argparse Module

All programs have a user interface (UI). Those with a command line interface (CLI) need ways to specify options, input files, and the like. One way to do this, taught to novice programmers, is to prompt the user, but this gets tedious after a while and makes scripting difficult. Definitely unpythonic, if not uncouth.

You can always parse sys.argv[] yourself, but less time consuming and more consistent to use a Python module to do it:

  • In the beginning, there was getopt, very much like the POSIX getopt(3) (with GNU extensions, cf. getopt_longer(3)).

  • getopt begat optparse, which parsed "options" (but left what came after up to the user). It also did cool stuff like generate consistent help messages automatically.

  • Nowadays, there's argparse, which, as the name sugggests, does it all.

Simple Example

Here's a simple program using argparse:

In [3]:
%%file demos/d00_argparse_simple.py
import argparse

argumentParser = argparse.ArgumentParser()
argumentParser.parse_args()
Overwriting demos/d00_argparse_simple.py

If we run it by itself, it does nothing.

In [4]:
!python3 demos/d00_argparse_simple.py

but you see there's already a self-referential help message available with both a short ...

In [5]:
!python3 demos/d00_argparse_simple.py -h
usage: d00_argparse_simple.py [-h]

optional arguments:
  -h, --help  show this help message and exit

... and a long form:

In [6]:
!python3 demos/d00_argparse_simple.py --help
usage: d00_argparse_simple.py [-h]

optional arguments:
  -h, --help  show this help message and exit

And if we try a command line that contains an argument:

In [7]:
!python3 demos/d00_argparse_simple.py spam
usage: d00_argparse_simple.py [-h]
d00_argparse_simple.py: error: unrecognized arguments: spam

Note that the error message went to standard error. (We can demonstrate this by using redirection above.)

Adding a Single Argument

Let's add an argument -- a positional one:

In [8]:
%%file demos/d01_argparse_only_arg.py
import argparse

argumentParser = argparse.ArgumentParser()
argumentParser.add_argument("onlyArg")
args = argumentParser.parse_args()
print("args.onlyArg:", args.onlyArg)
Overwriting demos/d01_argparse_only_arg.py
In [9]:
!python3 demos/d01_argparse_only_arg.py foo
args.onlyArg: foo

So the argument is now legal and the name you give it in add_argument() is used as an attribute of the return from parse_args(). The value of that attribute is the (str) argument.

Now, if we don't pass an argument, we get an error:

In [10]:
!python3 demos/d01_argparse_only_arg.py
usage: d01_argparse_only_arg.py [-h] onlyArg
d01_argparse_only_arg.py: error: the following arguments are required: onlyArg

The argument is automatically added to the help message as well:

In [11]:
!python3 demos/d01_argparse_only_arg.py --help
usage: d01_argparse_only_arg.py [-h] onlyArg

positional arguments:
  onlyArg

optional arguments:
  -h, --help  show this help message and exit

Improving the Help Message

But we can make this help message more -- uh -- helpful by attaching text to the argument.

In [12]:
%%file demos/d02_argparse_only_arg_helpful.py
import argparse

argumentParser = argparse.ArgumentParser()
argumentParser.add_argument("onlyArg",
        help="the only argument (add description as needed)")
args = argumentParser.parse_args()
print("args.onlyArg:", args.onlyArg)
Overwriting demos/d02_argparse_only_arg_helpful.py
In [13]:
%%sh
python3 demos/d02_argparse_only_arg_helpful.py --help
usage: d02_argparse_only_arg_helpful.py [-h] onlyArg

positional arguments:
  onlyArg     the only argument (add description as needed)

optional arguments:
  -h, --help  show this help message and exit

Enforcing Argument Types

By default, the argument is a string, but we can make parse_args() convert it to another type. Let's write a program to do something useful with this. In addition to "help" keywords for each argument, we'll add an overall "description".

In [14]:
%%file demos/d03_argparse_quad_soln.py
import argparse

argumentParser = argparse.ArgumentParser(description=r"""
prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0
""")
argumentParser.add_argument("a", help="quadratic coeffcient", type=float)
argumentParser.add_argument("b", help="linear coeffcient", type=float)
argumentParser.add_argument("c", help="constant", type=float)
args = argumentParser.parse_args()
discr = args.b**2 - 4*args.a*args.c
root1 = (-args.b + discr**0.5)/(2*args.a)
root2 = (-args.b - discr**0.5)/(2*args.a)
print("roots: {}, {}".format(root1, root2))
Overwriting demos/d03_argparse_quad_soln.py

This works:

In [15]:
!python3 demos/d03_argparse_quad_soln.py 1 -7 12
roots: 4.0, 3.0

If the argument can't be parsed as a float, we get an error:

In [16]:
!python3 demos/d03_argparse_quad_soln.py 1 -7 spam
usage: d03_argparse_quad_soln.py [-h] a b c
d03_argparse_quad_soln.py: error: argument c: invalid float value: 'spam'

And here's the help message:

In [17]:
!python3 demos/d03_argparse_quad_soln.py --help
usage: d03_argparse_quad_soln.py [-h] a b c

prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0

positional arguments:
  a           quadratic coeffcient
  b           linear coeffcient
  c           constant

optional arguments:
  -h, --help  show this help message and exit

Options (optional arguments)

We have one optional argument (the automatically-added "--help"). How can we add more? For starters, we indicate them with a leading "--".

In [18]:
%%file demos/d04_argparse_quad_soln_verbose.py
import argparse
argumentParser = argparse.ArgumentParser(description=r"""
prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0
""")
argumentParser.add_argument("a",
        help="quadratic coeffcient", type=float)
argumentParser.add_argument("b",
        help="linear coeffcient", type=float)
argumentParser.add_argument("c",
        help="constant coeffcient", type=float)
argumentParser.add_argument("--verbosity", # <- like this
                            help="degree of verbosity", type=int)
args = argumentParser.parse_args()
discr = args.b**2 - 4*args.a*args.c
root1 = (-args.b + discr**0.5)/(2*args.a)
root2 = (-args.b - discr**0.5)/(2*args.a)
if args.verbosity:
    print("{:g}x**2{:+g}x{:+g} = (x{:+g})(x{:+g})".format(
            args.a, args.b, args.c, -root1, -root2))
else:
    print("roots: {}, {}".format(root1, root2))
Overwriting demos/d04_argparse_quad_soln_verbose.py
In [19]:
%%sh
python3 demos/d04_argparse_quad_soln_verbose.py --verbosity 1 1 -7 12
1x**2-7x+12 = (x-4)(x-3)
In [20]:
%%sh
python3 demos/d04_argparse_quad_soln_verbose.py --help
usage: d04_argparse_quad_soln_verbose.py [-h] [--verbosity VERBOSITY] a b c

prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0

positional arguments:
  a                     quadratic coeffcient
  b                     linear coeffcient
  c                     constant coeffcient

optional arguments:
  -h, --help            show this help message and exit
  --verbosity VERBOSITY
                        degree of verbosity

This requires us to provide a value ("VERBOSITY") after the option flag. What if we don't want to do that?

Setting the Argument "action"

If we want the presence of the option to act like a boolean flag, we only need to make a few small changes, including setting the "action" to "store_true" so that the result can be used as a bool.

In [21]:
%%file demos/d05_argparse_quad_soln_verbose_flag.py
import argparse
argumentParser = argparse.ArgumentParser(description=r"""
prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0
""")
argumentParser.add_argument("a",
        help="quadratic coeffcient", type=float)
argumentParser.add_argument("b",
        help="linear coeffcient", type=float)
argumentParser.add_argument("c",
        help="constant coeffcient", type=float)
argumentParser.add_argument("--verbose", # <- name change
                            action='store_true',  # <- added
                            help="verbose output") # <- minor change
args = argumentParser.parse_args()
discr = args.b**2 - 4*args.a*args.c
root1 = (-args.b + discr**0.5)/(2*args.a)
root2 = (-args.b - discr**0.5)/(2*args.a)
if args.verbose:
    print("{:g}x**2{:+g}x{:+g} = (x{:+g})(x{:+g})".format(
            args.a, args.b, args.c, -root1, -root2))
else:
    print("roots: {}, {}".format(root1, root2))
Overwriting demos/d05_argparse_quad_soln_verbose_flag.py

And so "--verbose" no longer needs an argument:

In [22]:
!python3 demos/d05_argparse_quad_soln_verbose_flag.py --verbose 1 -7 12
1x**2-7x+12 = (x-4)(x-3)

The help message reflects this:

In [23]:
!python3 demos/d05_argparse_quad_soln_verbose_flag.py --help
usage: d05_argparse_quad_soln_verbose_flag.py [-h] [--verbose] a b c

prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0

positional arguments:
  a           quadratic coeffcient
  b           linear coeffcient
  c           constant coeffcient

optional arguments:
  -h, --help  show this help message and exit
  --verbose   verbose output

Short Options

Long keyword "--"-prefixed options are recommended for scripting purposes, but short, single-character options are quicker to type and there are often conventions that make them easy to remember: "-h" is equivalent to "--help":

In [24]:
%%file demos/d06_argparse_quad_soln_verbose_short.py
import argparse

argumentParser = argparse.ArgumentParser(description=r"""
prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0
""")
argumentParser.add_argument("a",
        help="quadratic coeffcient", type=float)
argumentParser.add_argument("b",
        help="linear coeffcient", type=float)
argumentParser.add_argument("c",
        help="constant coeffcient", type=float)
argumentParser.add_argument("-v", "--verbose", # note new 1st argument
                            action='store_true',
                            help="verbose output")
args = argumentParser.parse_args()
discr = args.b**2 - 4*args.a*args.c
root1 = (-args.b + discr**0.5)/(2*args.a)
root2 = (-args.b - discr**0.5)/(2*args.a)
if args.verbose:
    print("{:g}x**2{:+g}x{:+g} = (x{:+g})(x{:+g})".format(
            args.a, args.b, args.c, -root1, -root2))
else:
    print("roots: {}, {}".format(root1, root2))
Overwriting demos/d06_argparse_quad_soln_verbose_short.py

The result is quicker to type:

In [25]:
%%sh
python3 demos/d06_argparse_quad_soln_verbose_short.py -v 1 -7 12
1x**2-7x+12 = (x-4)(x-3)

The "--verbose" flag continues to work, as shown in the help message:

In [26]:
%%sh
python3 demos/d06_argparse_quad_soln_verbose_short.py -h
usage: d06_argparse_quad_soln_verbose_short.py [-h] [-v] a b c

prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0

positional arguments:
  a              quadratic coeffcient
  b              linear coeffcient
  c              constant coeffcient

optional arguments:
  -h, --help     show this help message and exit
  -v, --verbose  verbose output

Hiding Arguments

How about adding a "secret" argument? One that works, but doesn't appear in the help message? Easy: Just make the "help" keyword "argparse.SUPPRESS". Let's do this for the "--verbose" argument:

In [27]:
%%file demos/d07_argparse_quad_soln_hidden_argument.py
import argparse

argumentParser = argparse.ArgumentParser(description=r"""
prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0
""")
argumentParser.add_argument("a",
        help="quadratic coeffcient", type=float)
argumentParser.add_argument("b",
        help="linear coeffcient", type=float)
argumentParser.add_argument("c",
        help="constant coeffcient", type=float)
argumentParser.add_argument("-v", "--verbose",
                            action='store_true',
                            help=argparse.SUPPRESS) # <- omitted from help
args = argumentParser.parse_args()
discr = args.b**2 - 4*args.a*args.c
root1 = (-args.b + discr**0.5)/(2*args.a)
root2 = (-args.b - discr**0.5)/(2*args.a)
if args.verbose:
    print("{:g}x**2{:+g}x{:+g} = (x{:+g})(x{:+g})".format(
            args.a, args.b, args.c, -root1, -root2))
else:
    print("roots: {}, {}".format(root1, root2))
Overwriting demos/d07_argparse_quad_soln_hidden_argument.py

Sure enough, the "--verbose" doesn't appear in the help message:

In [28]:
!python3 demos/d07_argparse_quad_soln_hidden_argument.py -h
usage: d07_argparse_quad_soln_hidden_argument.py [-h] a b c

prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0

positional arguments:
  a           quadratic coeffcient
  b           linear coeffcient
  c           constant coeffcient

optional arguments:
  -h, --help  show this help message and exit

But it's still parsed:

In [29]:
!python3 demos/d07_argparse_quad_soln_hidden_argument.py -v 1 -7 12
1x**2-7x+12 = (x-4)(x-3)

When Options Conflict

Sometimes options can be mutually exclusive. Suppose there was a "--quiet" (or "-q" for short) option that made no sense to select at the same time as a "--verbose" flag. Here's how we do that:

In [30]:
%%file demos/d08_argparse_quad_soln_verbose_quiet.py
import argparse
argumentParser = argparse.ArgumentParser(description=r"""
prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0
""")
argumentParser.add_argument("a",
        help="quadratic coeffcient", type=float)
argumentParser.add_argument("b",
        help="linear coeffcient", type=float)
argumentParser.add_argument("c",
        help="constant coeffcient", type=float)

mutexGroup = argumentParser.add_mutually_exclusive_group()
mutexGroup.add_argument("-q", "--quiet", # here's the new argument
                            action='store_true',
                            help="silent output")
mutexGroup.add_argument("-v", "--verbose",
                            action='store_true',
                            help="verbose output")

args = argumentParser.parse_args()
discr = args.b**2 - 4*args.a*args.c
root1 = (-args.b + discr**0.5)/(2*args.a)
root2 = (-args.b - discr**0.5)/(2*args.a)
if args.verbose:
    print("{:g}x**2{:+g}x{:+g} = (x{:+g})(x{:+g})".format(
            args.a, args.b, args.c, -root1, -root2))
elif args.quiet:
    pass
else:
    print("roots: {}, {}".format(root1, root2))
Overwriting demos/d08_argparse_quad_soln_verbose_quiet.py

The "--quiet" flag is nonsensical: No output, so why run the program? Nevertheless, it works:

In [31]:
!python3 demos/d08_argparse_quad_soln_verbose_quiet.py -q 1 -7 12

argparse will enforce the exclusivity:

In [32]:
!python3 demos/d08_argparse_quad_soln_verbose_quiet.py -q -v 1 -7 12
usage: d08_argparse_quad_soln_verbose_quiet.py [-h] [-q | -v] a b c
d08_argparse_quad_soln_verbose_quiet.py: error: argument -v/--verbose: not allowed with argument -q/--quiet

Note the change to the help message:

In [33]:
!python3 demos/d08_argparse_quad_soln_verbose_quiet.py -h
usage: d08_argparse_quad_soln_verbose_quiet.py [-h] [-q | -v] a b c

prints solutions (roots) of the quadratic equation a x**2 + b x + c = 0

positional arguments:
  a              quadratic coeffcient
  b              linear coeffcient
  c              constant coeffcient

optional arguments:
  -h, --help     show this help message and exit
  -q, --quiet    silent output
  -v, --verbose  verbose output

The os Module

The sys module contains system-independent Python internals like sys.argv[] and sys.path. The os module contains support functions and system dependencies from the operating system. Here are some useful attributes:

In [34]:
import os

def printSys(name):
    print("{:>15}: {}".format(name, repr(eval(name))))

printSys("os.name")
printSys("os.curdir")
printSys("os.pardir")  # parent directory
printSys("os.sep")     # pathname separator: "/" on POSIX, "\" on Windows
printSys("os.extsep")  # extension separator (always ".")
printSys("os.altsep")
printSys("os.pathsep") # separator in $PATH, etc.
printSys("os.linesep")
printSys("os.defpath") # default path for executables
        os.name: 'posix'
      os.curdir: '.'
      os.pardir: '..'
         os.sep: '/'
      os.extsep: '.'
      os.altsep: None
     os.pathsep: ':'
     os.linesep: '\n'
     os.defpath: '/bin:/usr/bin'

Path related functions are in the os.path submodule:

In [35]:
dir(os.path)
Out[35]:
['__all__',
 '__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_get_sep',
 '_joinrealpath',
 '_varprog',
 '_varprogb',
 'abspath',
 'altsep',
 'basename',
 'commonpath',
 'commonprefix',
 'curdir',
 'defpath',
 'devnull',
 'dirname',
 'exists',
 'expanduser',
 'expandvars',
 'extsep',
 'genericpath',
 'getatime',
 'getctime',
 'getmtime',
 'getsize',
 'isabs',
 'isdir',
 'isfile',
 'islink',
 'ismount',
 'join',
 'lexists',
 'normcase',
 'normpath',
 'os',
 'pardir',
 'pathsep',
 'realpath',
 'relpath',
 'samefile',
 'sameopenfile',
 'samestat',
 'sep',
 'split',
 'splitdrive',
 'splitext',
 'stat',
 'supports_unicode_filenames',
 'sys']
In [36]:
os.path.exists("xyzzy")
Out[36]:
False
In [37]:
?os.path

The os module itself contains functions that act very much like POSIX (UNIX manual section (2)) functions, with some modifications for Python such as taking the Pythonic form of functions:

outputs = f(inputs)

and not having to pass a buffer as a pointer and length.

In [38]:
?os.open

Remember these are low level I/O routines. Don't use them when normal Python I/O does what you want. Think of normal Python I/O like standard I/O in C or iostreams in C++.

Low-Level POSIX-style I/O

Recall how we do conventional "stream" based I/O. Let's create a temporary file:

In [39]:
%%file temp.txt
This is a file that contains a single line of text.
Overwriting temp.txt

We read the first character from that file with the read() built-in function:

In [40]:
f = open("temp.txt", "r")
c = f.read(1)
c
Out[40]:
'T'

Sure enough, it's type is a str because we opened it as a text file:

In [41]:
type(c)
Out[41]:
str

This is because the read() encoded it into the default UTF-8 encoding. But if we open it as a "binary" file, we get a guaranteed 8-bit bytes:

In [42]:
f = open("temp.txt", "rb")
c = f.read(1)
c
Out[42]:
b'T'
In [43]:
type(c)
Out[43]:
bytes

The os module also allows us to read it with the Python equivalent of the POSIX open(2) system call.

In [44]:
g = os.open("temp.txt", os.O_RDONLY)
r = os.read(g, 1)
r
Out[44]:
b'T'

which is also a bytes because low-level I/O doesn't know anything about encoding.

So os.read() works just like built-in read() of a stream that's open for binary reading.

How about something that stream I/O can't do?

Making a stat(2) Call

The os module gives us pretty much all the POSIX functions. One of the most important of these is os.stat(), which gives us "metadata" about any filesystem object:

In [45]:
s = os.stat("temp.txt")
s
Out[45]:
os.stat_result(st_mode=33188, st_ino=2884794, st_dev=2053, st_nlink=1, st_uid=3772, st_gid=3772, st_size=52, st_atime=1605643264, st_mtime=1605643264, st_ctime=1605643264)
In [46]:
type(s)
Out[46]:
os.stat_result

The return of os.stat() is a custom-designed object that can be accessed by attribute:

In [47]:
f'{s.st_mode:o}'
Out[47]:
'100644'

or by index:

In [48]:
s[0], s[1], s[2]
Out[48]:
(33188, 2884794, 2053)

It is also immutable:

In [49]:
s.st_mode = 0
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-49-4591197ab3cb> in <module>
----> 1 s.st_mode = 0

AttributeError: readonly attribute

See how this is done?

Forking Processes

Among the POSIX routines are the usual fork() .. exec() routines you learned about in CptS 360.

In [63]:
%%file demos/d09_fork_test.py
import os
import sys
import time

def usefulFun(sibId):
    pid = os.getpid()
    for ct in range(3):
        print("In process {} (child {}), the count is {}.".format(
                pid, sibId, ct))
        sys.stdout.flush() # shows output in more chronological order
        time.sleep((sibId+1)*2 + 10)
    print('child {} exiting', sibId)

nChild = 3
pList = []

for sibId in range(nChild):
    pid = os.fork()
    if pid != 0:
        # we are in the parent process
        pList.append(pid)
    else:
        # we are in the child process
        usefulFun(sibId)
        quit()

time.sleep(20) # in parent
print("Process list: ", pList)
print("The parent is exiting")
Overwriting demos/d09_fork_test.py
In [64]:
!python3 demos/d09_fork_test.py
In process 73161 (child 1), the count is 0.
In process 73162 (child 2), the count is 0.
In process 73160 (child 0), the count is 0.
In process 73160 (child 0), the count is 1.
In process 73161 (child 1), the count is 1.
In process 73162 (child 2), the count is 1.
Process list:  [73160, 73161, 73162]
The parent is exiting

Note that the processes operate asynchronously. This is fine, except that the parent exits before the children. If the children were producing output the parent was using, the parent might want to wait() for them. We can fix that:

In [65]:
%%file demos/d10_fork_wait.py
import os
import sys
import time

def usefulFun(sibId):
    for ct in range(2, 0, -1):
        print("In process {}, the count is {}.".format(sibId, ct))
        time.sleep(sibId)
        sys.stdout.flush()

nChild = 3
pList = []

for sibId in range(nChild):
    pid = os.fork()
    if pid != 0:
        # we are in the parent process
        pList.append(pid)
    else:
        # we are in the child process
        usefulFun(sibId)
        sys.exit()

for sibId in range(nChild):
    os.wait()
print("Process list: ", pList)
print("The parent is exiting")
Overwriting demos/d10_fork_wait.py
In [66]:
!python3 demos/d10_fork_wait.py
In process 0, the count is 0.
In process 0, the count is 1.
In process 0, the count is 2.
In process 2, the count is 0.
In process 1, the count is 0.
In process 1, the count is 1.
In process 2, the count is 1.
In process 1, the count is 2.
In process 2, the count is 2.
Process list:  [73234, 73235, 73236]
The parent is exiting

Threads

Python supports POSIX threads. Here's a simple application to compute Fibonacci numbers. Each thread takes the last two numbers on the queue, adds them, and appends the result.

Instead of placing a limit on the number of Fibonacci numbers created, we place a time limit on the amount of time used.

In [76]:
%%file demos/d11_thread_fibonacci.py
import threading
import queue
import time
import random

class FiboWorker(threading.Thread):
    count = 0

    def __init__(self, theQueue):
        threading.Thread.__init__(self)
        self.theQueue = theQueue
        self.delay = random.random()*4
        self.id = FiboWorker.count
        FiboWorker.count += 1

    def run(self):
        while 1:
            data = self.theQueue.get()
            if data == (0,0):
                self.theQueue.put(data)
                print("at {}, Thread {} exits".format(time.ctime(), self))
                break
            
            result = (data[1], data[0]+data[1])
            print("at {}, Thread {} computes {} -> {}".format(
                    time.ctime(), self, data, result))
            self.theQueue.put(result)
            time.sleep(self.delay) # give other threads a chance

    def __str__(self):
        return str(self.id)


class FiboWorkerPool:

    def __init__(self, nWorkers):
        self.theQueue = queue.Queue(10)
        # setup pool
        self.workerPool = []
        for iWorker in range(nWorkers):
            self.workerPool.append(FiboWorker(self.theQueue))

    def startPool(self):
        """
        initializes the queue by putting the initial tuple on it
        """
        self.theQueue.put( (0,1) )
        for x in self.workerPool:
            x.start()

    def killPool(self):
        """
        terminates the queue by putting (0,0) on it
        """
        self.theQueue.put( (0,0) )

if __name__ == "__main__": 
    thePool = FiboWorkerPool(5)
    thePool.startPool()
    time.sleep(10)
    thePool.killPool()
Overwriting demos/d11_thread_fibonacci.py
In [77]:
!python3 demos/d11_thread_fibonacci.py
at Tue Nov 17 12:36:54 2020, Thread 0 computes (0, 1) -> (1, 1)
at Tue Nov 17 12:36:54 2020, Thread 1 computes (1, 1) -> (1, 2)
at Tue Nov 17 12:36:54 2020, Thread 2 computes (1, 2) -> (2, 3)
at Tue Nov 17 12:36:54 2020, Thread 3 computes (2, 3) -> (3, 5)
at Tue Nov 17 12:36:54 2020, Thread 4 computes (3, 5) -> (5, 8)
at Tue Nov 17 12:36:56 2020, Thread 3 computes (5, 8) -> (8, 13)
at Tue Nov 17 12:36:56 2020, Thread 2 computes (8, 13) -> (13, 21)
at Tue Nov 17 12:36:56 2020, Thread 0 computes (13, 21) -> (21, 34)
at Tue Nov 17 12:36:57 2020, Thread 1 computes (21, 34) -> (34, 55)
at Tue Nov 17 12:36:58 2020, Thread 3 computes (34, 55) -> (55, 89)
at Tue Nov 17 12:36:58 2020, Thread 4 computes (55, 89) -> (89, 144)
at Tue Nov 17 12:36:59 2020, Thread 2 computes (89, 144) -> (144, 233)
at Tue Nov 17 12:36:59 2020, Thread 0 computes (144, 233) -> (233, 377)
at Tue Nov 17 12:36:59 2020, Thread 3 computes (233, 377) -> (377, 610)
at Tue Nov 17 12:37:00 2020, Thread 1 computes (377, 610) -> (610, 987)
at Tue Nov 17 12:37:01 2020, Thread 2 computes (610, 987) -> (987, 1597)
at Tue Nov 17 12:37:01 2020, Thread 3 computes (987, 1597) -> (1597, 2584)
at Tue Nov 17 12:37:01 2020, Thread 0 computes (1597, 2584) -> (2584, 4181)
at Tue Nov 17 12:37:01 2020, Thread 4 computes (2584, 4181) -> (4181, 6765)
at Tue Nov 17 12:37:03 2020, Thread 1 computes (4181, 6765) -> (6765, 10946)
at Tue Nov 17 12:37:03 2020, Thread 3 computes (6765, 10946) -> (10946, 17711)
at Tue Nov 17 12:37:03 2020, Thread 2 computes (10946, 17711) -> (17711, 28657)
at Tue Nov 17 12:37:04 2020, Thread 0 computes (17711, 28657) -> (28657, 46368)
at Tue Nov 17 12:37:05 2020, Thread 3 computes (28657, 46368) -> (46368, 75025)
at Tue Nov 17 12:37:05 2020, Thread 4 exits
at Tue Nov 17 12:37:06 2020, Thread 2 computes (46368, 75025) -> (75025, 121393)
at Tue Nov 17 12:37:06 2020, Thread 1 exits
at Tue Nov 17 12:37:06 2020, Thread 0 computes (75025, 121393) -> (121393, 196418)
at Tue Nov 17 12:37:06 2020, Thread 3 exits
at Tue Nov 17 12:37:08 2020, Thread 2 computes (121393, 196418) -> (196418, 317811)
at Tue Nov 17 12:37:08 2020, Thread 0 exits
at Tue Nov 17 12:37:10 2020, Thread 2 computes (196418, 317811) -> (317811, 514229)
at Tue Nov 17 12:37:13 2020, Thread 2 exits

Here's another example: a threaded simulator of Brownian motion (of molecules):

In [ ]:
%%file demos/d12_thread_brownian.py
# Brownian motion -- an example of a multi-threaded Tkinter program.

# developed by Guido van Rossum

from tkinter import *
import random
import threading
import time
import sys

WIDTH = 600
HEIGHT = 600
SIGMA = 10
BUZZ = 2
RADIUS = 4
LAMBDA = 10
GREEN = 'green'

def particle(canvas):
    r = RADIUS
    x = random.gauss(WIDTH/2.0, SIGMA)
    y = random.gauss(HEIGHT/2.0, SIGMA)
    p = canvas.create_oval(x-r, y-r, x+r, y+r, fill=GREEN)
    while True:
        dx = random.gauss(0, BUZZ)
        dy = random.gauss(0, BUZZ)
        dt = random.expovariate(LAMBDA)
        try:
            canvas.move(p, dx, dy)
        except RuntimeError:
            break
        time.sleep(dt)


def bye(event, widget):
    threading.abort()
    widget.quit()


def main():
    root = Tk()
    canvas = Canvas(root, width=WIDTH, height=HEIGHT)
    canvas.bind('<Destroy>', lambda event, widget=canvas: bye)
    canvas.grid()
    np = 10
    if sys.argv[1:]:
        np = int(sys.argv[1])
    for n in range(np):
        t = threading.Thread(target=particle, args=(canvas,))
        try:
            t.start()
        except:
            print("""
unable to create {} threads -- continuing with {} threads
"""[1:-1].format(np, n))
            break
    root.mainloop()

if __name__ == '__main__':
    main()

Start this in another shell window:

top -H -u $USER

In [82]:
!python3 demos/d12_thread_brownian.py 1000
Killed
In [ ]: