diff --git a/example.py b/example.py
new file mode 100755
index 0000000000000000000000000000000000000000..a89de19a1539442aa2c7824a90ca1f83848a1fd0
--- /dev/null
+++ b/example.py
@@ -0,0 +1,19 @@
+#! /usr/bin/env python
+
+import random
+import sys
+
+count = int(sys.argv[1])
+min = int(sys.argv[2])
+max = int(sys.argv[3])
+
+r = random.Random()
+
+nums = [ r.uniform(min,max) for _ in range(count) ]
+
+print('Generated {n} random numbers between {a} and {b}'.format(
+   n=count,
+   a=min,
+   b=max ))
+print('Values: {}'.format(nums))
+
diff --git a/python.pdf b/python.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..5c7704a8ac94533464b8130c88733fb2aa282617
Binary files /dev/null and b/python.pdf differ
diff --git a/python.txt b/python.txt
new file mode 100644
index 0000000000000000000000000000000000000000..173ba97505f46f1bfd3af898369cf836d14db419
--- /dev/null
+++ b/python.txt
@@ -0,0 +1,540 @@
+Python Quick-Reference
+======================
+
+Python has two major version in current use: 2.7 and 3. In many
+cases, these behave identially, but there are some differences. Try
+things out in the interactive interpreter, which you can start by
+typing `python` with no arguments.
+
+For functions, classes, and modules, there's a built-in help system.
+Simply type `help(item)` for the documentation on *item*.
+
+**An important note about python:** *scope is indentation-based!*
+A change of indentation is a change of scope, and you cannot mix
+spaces and tabs. You should probably ensure that your editor uses
+spaces for indentation in python, and be careful of reflexively
+using the tab key for indentation unless you're sure your editor
+will replace tabs with spaces. You can break up long statements on
+multiple lines, but only if it's unambiguous that you're in the
+middle of a statement (such as within parenetheses or braces) or
+you use a line continuation character. I'm not going to tell you
+what the continuation character is, since it makes code ugly.
+
+
+Terminal Output
+---------------
+
+`print` is a statement in python2.7, and a function in python3. You
+can treat it like a function in both, and it will generally do what
+you expect:
+
+    print('Hello world')
+
+This will print a string to STDOUT.
+
+Printing to STDERR, which is what you should do for debug messages,
+is more complex. There are two portable ways to do this:
+
+    import sys
+    sys.stderr.write('Hello world\n')
+
+This uses the filehandle directly.
+
+    import sys
+    from __future__ import print_function
+    print('Hello world', file=sys.stderr)
+
+This will ensure that the python3-style print function is present,
+even in python2.7.
+
+
+Terminal Input
+--------------
+
+A simple way to read from STDIN is
+
+    import sys
+
+    while True:
+        line = sys.stdin.readline()
+        if '' == line:
+            break
+        print(line)
+
+You can also use the `raw_input` or `input` functions, but this is
+where things get messy. In python2.7, `raw_input` reads a line from
+STDIN, while `input` calls `raw_input` and then evaluates the result
+as a python expression. In python3, `input` behaves like python2.7's
+`raw_input`, and there is no `raw_input` function. We can hack our
+way around this, though:
+
+    if 'raw_input' not in dir(__builtins__):
+        raw_input = input
+
+    try:
+        while True:
+            line = raw_input()
+            print(line)
+    except:
+        pass
+
+Note that both of these require some special way to handle the end
+of file: either testing against an empty string or handling an
+exception. A third way is:
+
+    import sys
+
+    for line in sys.stdin:
+        print(line)
+
+Note how much simpler this is! This is, in general, how we would
+read from any file.
+
+
+Files
+-----
+
+Python has an `open` function, that opens files for reading or
+writing, potentially in binary mode, and possibly (for writing) in
+append mode.  See `help(open)` for details. We generally want to
+use these with the `with` keyword, which provides automatic file
+closing and other cleanup:
+
+    with open('input_file.txt') as in_file:
+        for line in in_file:
+            pass # This is a no-op
+
+    with open('output_file.txt', 'w') as out_file:
+        out_file.write('Hello world!\n')
+
+    with open('output_file.txt', 'w') as out_file:
+        print('Hello world!', file=out_file)
+
+
+Scalar Types
+------------
+
+Python has integers and floating-point numbers. Unlike many languages,
+all integers are arbitrary-length, as long as you have enough memory
+to represent them.
+
+Python2.7 has strings, which do double-duty as byte arrays. In
+python3, there is a separate bytes type, and strings are utf-8
+encoded by default.  Most of the time, you can ignore these
+differences. Strings can be single-quoted or double-quoted. There
+isn't much reason to prefer one over the other, though if you want
+to use the quote character in the string, you'll have to escape it:
+
+    s1 = "How's it goin'?"
+    s2 = 'How\'s it goin\'?'
+
+
+Iterable Types
+--------------
+
+Python also has lists and tuples. The difference is that a list can be
+modified, a tuple cannot:
+
+    list1 = [ 1, 2, 3, 4 ]  # Initialize a list with elements
+    list2 = []              # Create an empty list
+    list2.append(1)         # Append an item to the list
+    list3 = list()          # Another way to create an empty list
+    list3.extend([1,2,3])   # Add multiple items to the list
+
+    tuple1 = ( 1, 2, 3, 4)  # Create a tuple with explicit entries
+    tuple2 = tuple(list1)   # Create a tuple from another iterable
+
+There are other iterable types, defined by particular methods they have.
+
+Lists and tuples can be accessed by indexes:
+
+    list1[0]   # first element
+    list1[-1]  # last element
+
+They also support *slicing*:
+
+    list1[1:3]    # returns [ list1[1], list1[2] ]
+    list1[2:]     # returns [ list1[2], ..., list1[-1] ]
+    list1[:3]     # returns [ list1[0], list1[1], list1[2] ]
+    list1[0:3:2]  # returns [ list1[0], list1[2] ]
+    list1[0::2]   # returns all even-indexed entries
+    list1[::2]    # same
+
+You can iterate over the elements of a list or tuple:
+
+    for v in list1:
+        print(v)
+
+Note that a string (or byte string) can also be indexed like a list
+and iterated over.
+
+If you want to create an iterable of integers, you can use the `range`
+function:
+
+    stop_val  = 10
+    start_val = 1
+    step_val  = 2
+    range(stop_val)                     # range of ints from 0 through 9
+    range(start_val,stop_val)           # range of ints from 1 through 9
+    range(start_val,stop_val,step_val)  # odd ints from 1 through 9
+
+
+Dictionaries
+------------
+
+A python dictionary, or dict, is a map type.
+
+    d = {}                  # Create an empty dict
+    d = dict()              # Create an empty dict
+    d = { 'a': 1, 'b': 2 }  # Create a dict with initial values
+
+Keys and values can be of any type, and python does not require
+keys or values to be uniform in type:
+
+    d = dict()
+    d[1] = 'a'
+    d['a'] = 2
+
+Dictionaries are also iterable, though the iterator will be the
+keys, in some order:
+
+    for k in d:
+        print(d[k])
+
+dict has a number of useful methods (see help(dict) for more):
+
+| *Method*   | *Returns*                                 |
+| --------   | ---------                                 |
+| `keys()`   | an iterable containing the keys           |
+| `values()` | an iterable containing the values         |
+| `items()`  | an iterable containing (key,value) tuples |
+| `get(k)`   | value for key k, or None if not present   |
+| `get(k,x)` | value for key k, or x if not present      |
+| `pop(k)`   | value for key k, removing entry from dict |
+
+
+None
+----
+
+Python has a special type called `NoneType`, which has a single
+instance, named `None`. This is roughly python's equivalent of null.
+
+
+List Comprehensions
+-------------------
+
+Python has some functional programming elements, one of which is
+list comprehensions. Here's a simple example:
+
+    [ x**2 for x in xs ]
+
+This takes a list of values named `xs` and returns a list of the
+values squared. These can be combined extensively:
+
+    [ x*y for x in xs for y in ys ]
+
+The order can matter:
+
+    d = dict()
+    d['a'] = [ 1,2,3 ]
+    d['b'] = [ 4,5,6 ]
+    d['c'] = [ 7,8,9 ]
+    
+    print([ x for k in d for x in d[k] ])
+
+
+Formatted Strings
+-----------------
+
+The simple way to construct a formatted string is to use the string
+class's format function:
+
+    s1 = 'This is {} test'.format('a')
+    s2 = 'The square of {} is {}'.format(2,4)
+    s3 = '{1} is the square of {0}'.format(2,4)
+    s4 = 'The first 5 powers of {x} are {pows}'.format(
+             x=2,
+             pows=[2**e for e in range(1,6)]
+            )
+
+
+Control Flow
+------------
+
+Like any good language, python has a number of control flow
+expressions.  Here are a few:
+
+    if boolean_expression:
+        do_something
+    elif boolean_expression_2:
+        do_something_else
+    else:
+        do_default_thing
+
+Both `elif` and `else` are optional.
+
+    for x in xs:
+        do_something
+
+We've seen this before; it's a simple for loop
+
+    while boolean_expression:
+        do_something
+
+In all of these, `boolean_expression` is just something that evaluates
+to True or False (python's boolean constants). Python will coerce things:
+
+ * 0 is False
+ * any other number is True
+ * '' (empty string) is False
+ * any other string in True
+ * None is False
+ * [] (empty list) is False
+ * any other list is True
+
+
+Combining Lists
+---------------
+
+We've already seen `list.extend()` as a way to append an entire
+list to another. Sometimes we want to do other things, though.  Say
+we have a list of items, and we'd like to do something that involves
+their list index. We could do this:
+
+    for i in len(xs):
+        print('item {} of xs is {}'.format(i,xs[i]))
+
+There's another way we can do this, though:
+
+    for (i,v) in zip(range(len(xs)), xs):
+        print('item {} of xs is {}'.format(i,v))
+
+In this case, it doesn't seem to buy us much, but if we've read in
+two sequences from two different sources, but we know they should
+correlated, then we could use:
+
+    for (a,b) in zip(a_list, b_list):
+        do_something
+
+The `zip` function can take multiple sequences, and will truncate
+the resulting tuple to the length of the shortest sequence.
+
+
+Functions
+---------
+
+Python has functions. It even has anonymous functions. Let's start
+with normal functions:
+
+    def my_func():
+        pass
+
+This defines a function named `my_func` that takes no arguments and
+does nothing, returning None.
+
+    def my_func(a):
+        pass
+
+Now we've added an argument to our function. Arguments can be passed
+based on position or name:
+
+    my_func(1)
+    my_func(a=1)
+
+The latter is nice, because you don't have to worry about argument
+order:
+
+    def my_func(a,b):
+        pass
+    my_func(b=1,a=2)
+
+A common python idiom is to define very flexible functions like:
+
+    def my_func(a,b, *args, **kwargs):
+        pass
+
+This means we can provide additional positional parameters, which
+are then captured by `*args`, as well as named (keyword) arguments,
+which are captured by `**kwargs`. In this case, `args` is a tuple,
+and `kwargs` is a dict.
+
+A function can return a value. If there is no return statement, the
+return value is None:
+
+    def my_func():
+        return 'a'
+
+What about anonymous functions? We define these with the `lambda`
+keyword:
+
+    f = lambda x: x**2
+    f(2)
+
+This doesn't look like it gives us a lot of advantages over named
+functions, but it can be extremely handy:
+
+    num_output = map(lambda x: int(x,16), output)
+
+    def my_func(a,b):
+        return a*b
+    f = lambda a: my_func(a,2)
+
+
+Classes
+-------
+
+Without going into a lot of detail, python has a rich type system.
+Here's a simple class:
+
+    class Foo(object):
+        def __init__(self,a):
+            self.a = a
+
+This defines a type `Foo` and a constructor that takes two values.
+Here's how we create an instance:
+
+    foo = Foo(1)
+
+By calling the class name as a function, python automatically makes
+this a call to the `__init__` method, with the newly allocated
+instance as the first argument, named `self` by convention.
+
+Other methods can be defined similarly. Any instance method should
+have `self` as the first argument. Methods are otherwise almost
+identical to normal functions.
+
+Python is duck-typed. That is, if it looks like a duck and acts
+like a duck, it's a duck. When you use a value, if it conforms to
+the expected interface, you're good.
+
+You can query an object for its methods and data elements:
+
+    dir(foo)
+    foo.__dict__
+
+
+Modules
+-------
+
+A module is a python library. We've already used the sys and
+__future__ modules. To use a module foo, you need to import it:
+
+    import foo
+
+Now anything defined in foo, say a method "bar", can be accessed
+through foo's namespace:
+
+    foo.bar
+
+We can also import things into the current namespace:
+
+    from foo import bar
+    from foo import *
+
+The first line means we can reference "bar" without "foo.", but the
+second means we can reference *everything* in foo without the
+namespace. This is generally a bad idea, because it makes it less
+clear where a function or class comes from. Some packages work much
+better with this type of import, however, like scapy:
+
+    from scapy.all import *
+
+How do we create a module? We'll keep it easy, and only consider single-file
+modules. Feel free to look up more complex modules. If you want to create
+a module named foo, you would simply create a file named "foo.py", and define
+functions, classes, and variables in it as normal. Now, when you import foo,
+all of those will exist within foo's namespace.
+
+foo.py:
+
+    def bar(a):
+        print('foo: {}'.format(a))
+
+top-level script:
+
+    import foo
+    
+    foo.bar(3)
+
+
+Useful Modules
+--------------
+
+The sys module is the one you're most likely to import. It has a
+lot of functions, but one of its most useful elements is the
+`sys.argv` list. This contains the positional parameters to the
+script, in the order provided on the command line. The first element
+is the script name.
+
+    import sys
+    for arg in sys.argv:
+        print('We were called with argument {}'.format(arg))
+
+The random module is also very useful; it provides random numbers:
+
+    import random
+    r = random.Random()
+    r.choice(['a','b','c']) # choose a random element from the list
+    r.sample(range(100),5)  # choose 5 unique elements from [0,100)
+    r.randint(5,10)         # choose an integer in the range [5,10]
+    r.uniform(5,10)         # choose a float in the range [5,10)
+    r.gauss(75,10)          # choose a gaussian-distributed float with mean
+                            # 75 and standard deviation 10
+
+The subprocess module lets you call other processes, potentially
+capturing their output. See
+https://docs.python.org/2/library/subprocess.html or
+https://docs.python.org/3/library/subprocess.html, depending on
+which version of python you're using.
+
+Finally, the argparse module is a great way to process command-line arguments,
+if you need something fancier than just `sys.argv`. Here's an example to
+illustrate:
+
+    from argparse import ArgumentParser
+    
+    parser = ArgumentParser()
+    parser.add_argument('-s', '--students',
+                        dest='students',
+                        default='enrollments.json',
+                        help='JSON file containing the student enrollments'
+                        )
+    parser.add_argument('-g', '--groups',
+                        dest='groups',
+                        default='teams.json',
+                        help='JSON file containing the student groups'
+                        )
+    args = parser.parse_args()
+    
+    students = list()
+    with open(args.students) as f:
+        students = json.load(f)
+
+See the documentation for details; there's a lot to see here.
+
+
+A Complete Script
+-----------------
+
+This doesn't do much useful, but it should work, when saved to a
+file and made executable (see the bash and filesystem quick-refs):
+
+    #! /usr/bin/env python
+    
+    import random
+    import sys
+    
+    count = int(sys.argv[1])
+    min = int(sys.argv[2])
+    max = int(sys.argv[3])
+    
+    r = random.Random()
+    
+    nums = [ r.uniform(min,max) for _ in range(count) ]
+    
+    print('Generated {n} random numbers between {a} and {b}'.format(
+       n=count,
+       a=min,
+       b=max ))
+    print('Values: {}'.format(nums))
+