')
-
-    # we want the text, too:
-    assert 'any string I like' in contents
-    assert 'even more random text' in contents
-    assert 'and this is a different string' in contents
-
-    # you could, of course, test much more..but hopefully other things are tested, too.
-
-
-def test_indent():
-    """
-    Tests that the indentation gets passed through to the renderer
-    """
-    html = Html("some content")
-    cur_ind = 6 * " "
-    file_contents = render_element(html, cur_ind=cur_ind)
-
-    print(file_contents)
-    lines = file_contents.split("\n")
-
-    assert lines[0].startswith(cur_ind + "<")
-    assert lines[1].startswith(cur_ind + Element.indent + "som")
-    assert lines[-1].startswith(cur_ind + "<")
-
-
-def test_indent_contents():
-    """
-    The contents in a element should be indented more than the tag
-    by the amount in the indent class attribute
-    """
-    html = Html("some content")
-    file_contents = render_element(html, cur_ind="")
-
-    print(file_contents)
-    lines = file_contents.split("\n")
-    assert lines[1].startswith(Element.indent)
-
-
-def test_multiple_indent():
-    """
-    make sure multiple levels get indented properly
-    """
-    body = Body()
-    body.append(P("some text"))
-    body.append(P("even more text"))
-    html = Html(body)
-
-    file_contents = render_element(html)
-
-    print(file_contents)
-    lines = file_contents.split("\n")
-    for i in range(3):
-        assert lines[i].startswith(i * Element.indent + "<")
-    assert lines[3].startswith(3 * Element.indent + "some")
-    assert lines[4].startswith(2 * Element.indent + "
")
-    assert lines[5].startswith(2 * Element.indent + "")
-    assert lines[6].startswith(3 * Element.indent + "even ")
-    for i in range(3):
-        assert lines[-(i + 1)].startswith(i * Element.indent + "<")
-
-
-def test_title():
-    """
-    This will implicitly test the OneLineTag element
-    """
-    t = Title("Isn't this a nice title?")
-
-    # making sure indentation still works
-    file_contents = render_element(t, cur_ind="     ")
-
-    print(file_contents)
-    # no "strip()" -- making sure there are no extra newlines
-    assert file_contents.startswith("     
-    #             Here is a Pgraph of text -- there could be more of them, but this is enough  to show that we can do some text
-    #         
-    p = P("Here is a paragraph of text", style="text-align: center; font-style: oblique;")
-
-    results = render_element(p)
-
-    assert results.startswith('
-    #             Here is a paragraph of text -- there could be more of them, but this is enough  to show that we can do some text
-    #         
-    p = P("Here is a paragraph of text",
-             id="fred",
-             color="red",
-             size="12px",
-             )
-
-    results = render_element(p)
-    print(results)
-
-    lines = results.split('\n')
-    assert lines[0].startswith(' ')
-    assert 'id="fred"' in lines[0]
-    assert 'color="red"' in lines[0]
-    assert 'size="12px"' in lines[0]
-
-def test_multiple_attributes_title():
-    t = Title("Here is a paragraph of text",
-              id="fred",
-              color="red",
-              size="12px",
-              )
-
-    results = render_element(t)
-    print(results)
-
-    lines = results.split('\n')
-    assert lines[0].startswith('
')
-    assert 'id="fred"' in lines[0]
-    assert 'color="red"' in lines[0]
-    assert 'size="12px"' in lines[0]
-
-
-# test class attribute
-def test_class_attribute():
-    atts = {"id": "fred",
-            "class": "special",
-            "size": "12px",
-            }
-    p = P("Here is a paragraph of text",
-              **atts)
-
-    results = render_element(p)
-    print(results)
-
-    lines = results.split('\n')
-    assert lines[0].startswith('')
-    assert 'id="fred"' in lines[0]
-    assert 'class="special"' in lines[0]
-    assert 'size="12px"' in lines[0]
-
-
-
-
From 50a64313ec2214cece70f4227c24e21620f51332 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Mon, 3 Dec 2018 19:32:28 -0800
Subject: [PATCH 39/87] removed redundant sparse array solution
---
 source/solutions/sparse_array/slice_sparse.py | 139 ------------------
 source/solutions/sparse_array/sparse_array.py |  75 ----------
 .../sparse_array/test_slice_sparse.py         | 129 ----------------
 .../sparse_array/test_sparse_array.py         | 113 --------------
 4 files changed, 456 deletions(-)
 delete mode 100644 source/solutions/sparse_array/slice_sparse.py
 delete mode 100644 source/solutions/sparse_array/sparse_array.py
 delete mode 100644 source/solutions/sparse_array/test_slice_sparse.py
 delete mode 100644 source/solutions/sparse_array/test_sparse_array.py
diff --git a/source/solutions/sparse_array/slice_sparse.py b/source/solutions/sparse_array/slice_sparse.py
deleted file mode 100644
index c40fb76a..00000000
--- a/source/solutions/sparse_array/slice_sparse.py
+++ /dev/null
@@ -1,139 +0,0 @@
-
-"""
-example of emulating a sequence using slices
-"""
-
-
-class SparseArray(object):
-
-    def __init__(self, my_array=()):
-        self.length = len(my_array)
-        self.sparse_array = self._convert_to_sparse(my_array)
-
-    def _convert_to_sparse(self, my_array):
-        sparse_array = {}
-        for index, number in enumerate(my_array):
-            if number:
-                sparse_array[index] = number
-        return sparse_array
-
-    def __len__(self):
-        return self.length
-
-    def __str__(self):
-        msg = ['SparseArray: [']
-        for i in range(self.length):
-            msg.append("{} ".format(self[i]))
-        msg.append(']')
-        return "".join(msg)
-
-    def __getitem__(self, index):
-        # this version supports slicing -- far more complicated
-        mini_array = []
-        if isinstance(index, slice):
-            start, stop, step = index.indices(len(self))
-            if step is None:
-                step = 1
-            key = start
-            mini_array = []
-            while key < stop:
-                mini_array.append(self[key])
-                key += step
-            return mini_array
-        else:
-            # makes it an int, even if it's some other
-            # type that supports being used as an index
-            index = index.__index__()
-            return self._get_single_value(index)
-
-    def _get_single_value(self, key):
-        if key >= self.length:
-            raise IndexError('array index out of range')
-        else:
-            return self.sparse_array.get(key, 0)
-
-    def __setitem__(self, index, value):
-        if isinstance(index, slice):
-            start, stop, step = index.indices(len(self))
-            if step is None:
-                step = 1
-            key = start
-            new_values = []
-            new_keys = []
-            for each in value:
-                if key < stop:
-                    self[key] = each
-                else:
-                    # now instead of replacing values, we need to add (a) value(s) in the center,
-                    # and move stuff over, probably want to collect all of the changes,
-                    # and then make a new dictionary
-                    new_values.append(each)
-                    new_keys.append(key)
-                key += step
-            if new_keys:
-                self._add_in_slice(new_keys, new_values)
-        else:
-            index = index.__index__()
-            self._set_single_value(index, value)
-
-    def _set_single_value(self, key, value):
-        if key > self.length:
-            raise IndexError('array assignment index out of range')
-        if value != 0:
-            self.sparse_array[key] = value
-        else:
-            # if the value is being set to zero, we probably need to
-            # remove a key from our dictionary.
-            self.sparse_array.pop(key, None)
-
-    def _add_in_slice(self, new_keys, new_values):
-        # sometimes we need to add in extra values
-        # any existing values
-        # greater than the last key of the new data
-        # will be increased by how many
-        new_dict = {}
-        slice_length = len(new_keys)
-        for k, v in self.sparse_array.items():
-            if k >= new_keys[-1]:
-                # print('change keys')
-                # if greater than slice, change key
-                new_dict[k + slice_length] = v
-            elif k in new_keys:
-                # if this is a key we are changing, change it,
-                # unless we are changing to a zero...
-                new_value = values[new_keys.index(k)]
-                if new_value != 0:
-                    new_dict[k] = new_value
-            else:
-                new_dict[k] = v
-        # what if our new key was not previously in the dictionary?
-        # stick it in now
-        for k in new_keys:
-            if k not in new_dict.keys():
-                new_dict[k] = new_values[new_keys.index(k)]
-        # note we don't want to do update, since we need to make sure we are
-        # getting rid of the old keys, when we moved the value to a new key
-        self.sparse_array = new_dict
-        # now we need to increase the length by the amount we increased our array by
-        self.length += slice_length
-
-    def __delitem__(self, key):
-        # we probably need to move the keys if we are not deleting the last
-        # number, use pop in case it was a zero
-        if key == self.length - 1:
-            self.sparse_array.pop(key, None)
-        else:
-            # since we need to adjust all of the keys after the one we are
-            # deleting, probably most efficient to create a new dictionary
-            new_dict = {}
-            for k, v in self.sparse_array.items():
-                if k >= key:
-                    new_dict[k - 1] = v
-                else:
-                    new_dict[k] = v
-            # note we don't want to do update, since we need to make sure we are
-            # getting rid of the old keys, when we moved the value to a new key
-            self.sparse_array = new_dict
-        # length is now one shorter
-        self.length -= 1
-
diff --git a/source/solutions/sparse_array/sparse_array.py b/source/solutions/sparse_array/sparse_array.py
deleted file mode 100644
index d9543cab..00000000
--- a/source/solutions/sparse_array/sparse_array.py
+++ /dev/null
@@ -1,75 +0,0 @@
-
-"""
-An example of emulating a sequence
-
-A SparseArray is like a list, but only stores the non-zero values
-
-It can be indexed, appended-to, and iterated through.
-
-This version does not support slicing.
-"""
-
-
-class SparseArray(object):
-
-    def __init__(self, my_array=()):
-        """
-        initilize a sparse array
-
-        :param my_array: an initial sequence to start with
-                         if there are zeros in it, they wil not be stored
-        """
-        self.length = len(my_array)
-        # self.sparse_array is a dict that stores only the non-zero items
-        self.sparse_array = self._convert_to_sparse(my_array)
-
-    def _convert_to_sparse(self, my_array):
-        sparse_array = {}
-        for index, number in enumerate(my_array):
-            if number:  # remember that zeros are falsey.
-                sparse_array[index] = number
-        # or the dict comprehension method:
-        # sparse_array = {index:number for index, number in enumerate(my_array) if number}
-        return sparse_array
-
-    def __len__(self):
-        return self.length
-
-    def __getitem__(self, key):
-        # fixme: doesn't handle negative indexes properly
-        try:
-            return self.sparse_array[key]
-        except KeyError:
-            if key >= self.length:
-                raise IndexError('array index out of range')
-            return 0
-
-    def __setitem__(self, key, value):
-        if key > self.length:
-            raise IndexError('array assignment index out of range')
-        if value != 0:
-            self.sparse_array[key] = value
-        else:
-            # if the value is being set to zero, we probably need to
-            # remove a key from our dictionary.
-            self.sparse_array.pop(key, None)
-
-    def __delitem__(self, key):
-        # we probably need to move the keys if we are not deleting the last
-        # number, use pop in case it was a zero
-        if key == self.length - 1:  # it's the last item -- easy.
-            self.sparse_array.pop(key, None)
-        else:
-            # since we need to adjust all of the keys after the one we are
-            # deleting, probably most efficient to create a new dictionary
-            new_dict = {}
-            for k, v in self.sparse_array.items():
-                if k >= key:
-                    new_dict[k - 1] = v
-                else:
-                    new_dict[k] = v
-            # note we don't want to do update, since we need to make sure we are
-            # getting rid of the old keys, when we moved the value to a new key
-            self.sparse_array = new_dict
-        # length is now one shorter
-        self.length -= 1
diff --git a/source/solutions/sparse_array/test_slice_sparse.py b/source/solutions/sparse_array/test_slice_sparse.py
deleted file mode 100644
index 4a7994c8..00000000
--- a/source/solutions/sparse_array/test_slice_sparse.py
+++ /dev/null
@@ -1,129 +0,0 @@
-"""
-this version tests the solution with slicing tests
-"""
-
-
-import pytest
-from slice_sparse import SparseArray
-
-
-def set_up():
-    my_array = [2, 0, 0, 0, 3, 0, 0, 0, 4, 5, 6, 0, 2, 9]
-    my_sparse = SparseArray(my_array)
-    return (my_array, my_sparse)
-
-
-def test_object_exists():
-    my_array, my_sparse = set_up()
-    assert isinstance(my_sparse, SparseArray)
-
-
-def test_get_non_zero_number():
-    my_array, my_sparse = set_up()
-    assert my_sparse[4] == 3
-
-
-def test_get_zero():
-    my_array, my_sparse = set_up()
-    assert my_sparse[1] == 0
-
-
-def test_get_element_not_in_array():
-    my_array, my_sparse = set_up()
-    with pytest.raises(IndexError):
-        my_sparse[14]
-
-
-def test_str():
-    my_array, my_sparse = set_up()
-
-
-def test_get_slice():
-    my_array, my_sparse = set_up()
-    assert my_sparse[2:4] == [0, 0]
-
-
-def test_set_slice():
-    my_array, my_sparse = set_up()
-    my_sparse[2:4] = [2, 3, 4]
-    assert my_sparse[:] == [2, 0, 2, 3, 4, 3, 0, 0, 0, 4, 5, 6, 0, 2, 9]
-
-
-def test_set_slice_over_end():
-    # this slice goes over the end
-    my_array, my_sparse = set_up()
-    print(my_sparse)
-    my_sparse[2:4] = [2, 3, 4]
-    assert my_sparse[:] == [2, 0, 2, 3, 4, 3, 0, 0, 0, 4, 5, 6, 0, 2, 9]
-
-
-def test_get_length():
-    my_array, my_sparse = set_up()
-    assert len(my_sparse) == 14
-
-
-def test_change_number_in_array():
-    my_array, my_sparse = set_up()
-    my_sparse[0] = 3
-    assert my_sparse[0] == 3
-    # make sure others aren't changed
-    assert my_sparse[1] == 0
-    # make sure still same length
-    assert len(my_sparse) == 14
-
-
-def test_change_number_in_array_to_zero():
-    my_array, my_sparse = set_up()
-    my_sparse[4] = 0
-    assert my_sparse[4] == 0
-    # make sure still same length
-    assert len(my_sparse) == 14
-
-
-def test_change_number_in_array_from_zero():
-    my_array, my_sparse = set_up()
-    my_sparse[1] = 4
-    assert my_sparse[1] == 4
-    # make sure still same length
-    assert len(my_sparse) == 14
-
-
-def test_change_slice():
-    my_array, my_sparse = set_up()
-    my_sparse[1:3] = [2, 3]
-    assert my_sparse[1:3] == [2, 3]
-
-
-def test_delete_number():
-    my_array, my_sparse = set_up()
-    del(my_sparse[4])
-    # if we delete the 4 position, should now be zero
-    assert my_sparse[4] == 0
-    # should have smaller length
-    assert len(my_sparse) == 13
-
-
-def test_delete_zero():
-    my_array, my_sparse = set_up()
-    del my_sparse[5]
-    # should still be zero, but should have shorter length
-    assert my_sparse[5] == 0
-    assert len(my_sparse) == 13
-
-
-def test_delete_last_number():
-    my_array, my_sparse = set_up()
-    del(my_sparse[13])
-    # should get an error
-    with pytest.raises(IndexError):
-        my_sparse[13]
-    assert len(my_sparse) == 13
-
-
-def test_indices_change():
-    my_array, my_sparse = set_up()
-    del(my_sparse[3])
-    # next index should have changed
-    # my_sparse[4] was 3 now
-    # my_sparse[3] should be 3
-    assert (my_sparse[3] == 3)
diff --git a/source/solutions/sparse_array/test_sparse_array.py b/source/solutions/sparse_array/test_sparse_array.py
deleted file mode 100644
index 06862292..00000000
--- a/source/solutions/sparse_array/test_sparse_array.py
+++ /dev/null
@@ -1,113 +0,0 @@
-"""
-This tests the solution that does not support slicing
-"""
-
-import pytest
-from sparse_array import SparseArray
-
-
-def set_up():
-    my_array = [2, 0, 0, 0, 3, 0, 0, 0, 4, 5, 6, 0, 2, 9]
-    my_sparse = SparseArray(my_array)
-    return (my_array, my_sparse)
-
-
-def test_object_exists():
-    my_array, my_sparse = set_up()
-    assert isinstance(my_sparse, SparseArray)
-
-
-def test_get_non_zero_number():
-    my_array, my_sparse = set_up()
-    assert my_sparse[4] == 3
-
-
-def test_get_zero():
-    my_array, my_sparse = set_up()
-    assert my_sparse[1] == 0
-
-
-def test_get_element_not_in_array():
-    my_array, my_sparse = set_up()
-    with pytest.raises(IndexError):
-        my_sparse[14]
-
-
-def test_get_length():
-    my_array, my_sparse = set_up()
-    assert len(my_sparse) == 14
-
-
-def test_change_number_in_array():
-    my_array, my_sparse = set_up()
-    my_sparse[0] = 3
-    assert my_sparse[0] == 3
-    # make sure others aren't changed
-    assert my_sparse[1] == 0
-    # make sure still same length
-    assert len(my_sparse) == 14
-
-
-def test_change_number_in_array_to_zero():
-    my_array, my_sparse = set_up()
-    my_sparse[4] = 0
-    assert my_sparse[4] == 0
-    # make sure still same length
-    assert len(my_sparse) == 14
-
-
-def test_change_number_in_array_from_zero():
-    my_array, my_sparse = set_up()
-    my_sparse[1] = 4
-    assert my_sparse[1] == 4
-    # make sure still same length
-    assert len(my_sparse) == 14
-
-
-def test_delete_number():
-    my_array, my_sparse = set_up()
-    del(my_sparse[4])
-    # if we delete the 4 position, should now be zero
-    assert my_sparse[4] == 0
-    # should have smaller length
-    assert len(my_sparse) == 13
-
-
-def test_delete_zero():
-    my_array, my_sparse = set_up()
-    del(my_sparse[5])
-    # should still be zero, but should have shorter length
-    assert my_sparse[5] == 0
-    assert len(my_sparse) == 13
-
-
-def test_delete_last_number():
-    my_array, my_sparse = set_up()
-    del(my_sparse[13])
-    # should get an error?
-    with pytest.raises(IndexError):
-        my_sparse[13]
-    assert len(my_sparse) == 13
-
-
-def test_indices_change():
-    my_array, my_sparse = set_up()
-    del(my_sparse[3])
-    # next index should have changed
-    # my_sparse[4] was 3 now
-    # my_sparse[3] should be 3
-    assert (my_sparse[3] == 3)
-
-
-# this is a way to tell pytest that you expect this test to fail
-@pytest.mark.xfail
-def test_get_slice():
-    my_array, my_sparse = set_up()
-    assert my_sparse[2:4] == [0, 0]
-
-
-@pytest.mark.xfail
-def test_set_slice():
-    my_array, my_sparse = set_up()
-    my_sparse[2:4] = [2, 3, 4]
-    assert my_sparse[:] == [2, 0, 2, 3, 4, 3, 0, 0, 0, 4, 5, 6, 0, 2, 9]
From 753dac30161f5a6771810bf263a353f98f5bc4ac Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Tue, 4 Dec 2018 17:22:06 -0800
Subject: [PATCH 40/87] a bit of copy editing
---
 source/exercises/mailroom-oo.rst | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/source/exercises/mailroom-oo.rst b/source/exercises/mailroom-oo.rst
index 74d61e02..0d98b1b5 100644
--- a/source/exercises/mailroom-oo.rst
+++ b/source/exercises/mailroom-oo.rst
@@ -15,7 +15,7 @@ But this time, we want to use an OO approach to better structure the code to mak
 
 It was quite reasonable to build the simple mailroom program using a
 single module, a simple data structure, and functions that manipulate
-that data structure.
+that data structure. In fact, you've already done that :-)
 
 But if one were to expand the program with additional functionality, it
 would start to get a bit unwieldy and hard to maintain. So it's a pretty good candidate for an object-oriented approach.
@@ -23,7 +23,7 @@ would start to get a bit unwieldy and hard to maintain. So it's a pretty good ca
 As you design appropriate classes, keep in mind these three guidelines for good code structure:
 
 
-1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that they that work on that data are within one unit. The rest of the code doesn't need to know about the data structure you are using.
+1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that everyting that works with that data structure are within one unit. The rest of the code doesn't need to know about the data structure you are using.
 
 2) Separation of Concerns: The user-interaction code should be cleanly separated from the data handling code.
 
From f138cce7dd10ecaa7069d3f3cab77ef9ab404ee0 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sat, 8 Dec 2018 14:33:31 -0800
Subject: [PATCH 41/87] some extra notes on OO mailroom
---
 source/exercises/mailroom-oo.rst | 87 ++++++++++++++++++++++++++++++--
 1 file changed, 83 insertions(+), 4 deletions(-)
diff --git a/source/exercises/mailroom-oo.rst b/source/exercises/mailroom-oo.rst
index 0d98b1b5..4cc2b1a5 100644
--- a/source/exercises/mailroom-oo.rst
+++ b/source/exercises/mailroom-oo.rst
@@ -1,11 +1,12 @@
 .. _exercise_mailroom_oo:
 
+##########################
 Mailroom - Object Oriented
-==========================
+##########################
 
 Making Mailroom Object Oriented.
 
-Goal: Refactor the mailroom program using classes to help organize the code.
+**Goal:** Refactor the mailroom program using classes to help organize the code.
 
 The functionality is the same as the earlier mailroom:
 
@@ -35,7 +36,7 @@ As you design appropriate classes, keep in mind these three guidelines for good
 
 
 The Program
------------
+===========
 
 See: :ref:`exercise_mailroom_part1` to remind yourself what the program needs to do.
 
@@ -131,7 +132,85 @@ At this point we have done a great job refactoring the more complex code out of
 
 The ``Donor`` and ``DonorCollection`` classes should now have close to 100 percent code coverage.
 
-For the moment, don't worry about testing most of the command line interface code. That requires simulating use input, which is an advanced testing topic. But you can (hopefully) see some of the benefits of separating the user-interaction code from the logic code; your logic code is much easier to test with no user-interaction involved.
+For the moment, don't worry about testing most of the command line interface code. That requires simulating user input, which is an advanced testing topic. But you can (hopefully) see some of the benefits of separating the user-interaction code from the logic code; your logic code is much easier to test with no user-interaction involved.
+
+Exercise Guidelines
+===================
+
+OO mailroom is the final project for the class.
+
+So this is your chance to really do things "right". Strive to make this code as good, by every definition, as you can.
+
+With that in mind:
+
+Functionality
+-------------
+
+* The logic is correct -- i.e. the program works :-)
+
+* The logic is robust -- you are handling obvious expected errors reasonably:
+
+  - User inputting a non-number as a donation
+
+  - Trying to make a negative donation
+
+  - User getting capitalization or spacing or ??? wrong with a name.
+
+    - maybe add logic where you tell them that the name is not in the DB, and do they want to create it, rather than simply creating a new record for a typo in a donor name.
+
+Code structure:
+---------------
+
+* Classes should have clear purpose and encapsulation: only the code within a class should know exactly how the data are stored, for instance.
+
+* Anything that only needs to know about one donor should be in the ``Donor`` class
+
+* Anything that needs to know about the collection should be in a ``DonorCollection`` class.
+
+* Any user interaction should be outside the "logic" code. (Sometimes called the "Model", or "Business logic")
+
+  - You should be able to re-use all the logic code with a different UI -- Web App, GUI, etc.
+
+  - There should be no ``input()`` or ``print`` functions in the logic code.
+
+  - The logic code should be 100% testable (without mocking input() or any fancy stuff like that)
+
+Testing:
+
+* All logic code should be tested.
+
+* Tests should be isolated to test one thing each
+
+* Tests should (reasonably) check for handling of weird input.
+
+* Tests should be isolated -- that is, they will work if run by themselves, and in any order.
+
+  - This means they should not rely on any global state.
+
+  - you'll probably find this easier with a well structured OO approach -- that is, you can test an individual donor functionality without knowing about the rest of the donors.
+
+
+Now the "soft" stuff:
+---------------------
+
+* Style: conform to PEP8! (or another consistent style)
+
+  - You can use 95 or some other reasonable number for line length
+
+* docstrings: functions and classes should all have good docstrings. They can very very short if the function does something simple.
+
+* Naming: all classes, functions, methods, attributes, variables should have appropriate names: meaningful, but not too detailed.
+
+Extra ideas:
+------------
+
+In case, you are bored -- what features can you add?
+
+* How about an html report using your html_render code?
+
+* Fancier reporting
+
+* The sky's the limit
 
 
 
From cc6b92877f0c7961157a0d6d2e2a98ef16f48bb4 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Wed, 26 Dec 2018 15:28:24 -0800
Subject: [PATCH 42/87] updated for_instructors notes.
---
 source/for_instructors/code_review.rst | 14 ++++++------
 source/for_instructors/github.rst      | 30 ++++++++++++++++++++++++++
 source/for_instructors/solutions.rst   |  2 +-
 3 files changed, 39 insertions(+), 7 deletions(-)
diff --git a/source/for_instructors/code_review.rst b/source/for_instructors/code_review.rst
index 776f9bea..ff1f1e32 100644
--- a/source/for_instructors/code_review.rst
+++ b/source/for_instructors/code_review.rst
@@ -2,7 +2,7 @@
 Reviewing Students' Code
 ########################
 
-One of the most important services the program provides is code review. While for teh most part the students will know if they have solved the problem (i.e. their code works) there is al ot they can learn about clean style, robust logic, appropriate data structures, pythonicity, etc.
+One of the most important services the program provides is code review. While for the most part the students will know if they have solved the problem (i.e. their code works) there is a lot they can learn about clean style, robust logic, appropriate data structures, Pythonicity, etc.
 
 And they can only learn that if their code is reviewed and critiqued.
 
@@ -11,7 +11,7 @@ Scoring Assignments
 
 Exactly how to score the exercises is up to the individual instructor. But we do need to provide some score, so there is a clear requirement to passing the class.
 
-As the course is pass/fail, I don't think it's important to distinguish between decent, good, and excellent work. So I tend to give full credit for a good-faith effort at an exercise, and let me review be the feedback they need.
+As the course is pass/fail, I don't think it's important to distinguish between decent, good, and excellent work. So I tend to give full credit for a good-faith effort at an exercise, and let the review be the feedback they need.
 
 Reviewing Rubric
 ================
@@ -38,16 +38,18 @@ Is the code Pythonic?
 
  - looping through a sequence  rather than indexing
  - EAFTP Exception handling
- - ...
+ - iterating rather that making copies
+ - no mutables in default arguments
 
 Style Issues:
 -------------
 
  - PEP 8 compliance
  - variable names:
+    - meaningful names -- not "key", "value", "foo"
     - one letter names only for index variables etc.
-    - verbs for functions. methods
+    - verbs for functions and methods
     - nouns for objects
-    - plural for sequences of objects.
-    - ...
+    - plural for sequences of objects / singular for single items
+
 
diff --git a/source/for_instructors/github.rst b/source/for_instructors/github.rst
index b46fde41..b5bc4bbb 100644
--- a/source/for_instructors/github.rst
+++ b/source/for_instructors/github.rst
@@ -9,6 +9,36 @@ gitHub provides a good platform for code review, and it also provides the studen
 The class repo
 ==============
 
+There is a gitHub org for the class repos here:
+
+https://github.com/UWPCE-PythonCert-ClassRepos
+
+And a template here:
+
+https://github.com/UWPCE-PythonCert-ClassRepos/ClassRepoTemplate
+
+Before the class starts, the instructor should make a ocpy of that TEmplate repo, and crate a new one labeled something like:
+
+``Fall2018-PY210A`` or ``Sp2018-Online``
+
+Make sure your students know which repo to fork and use for your class -- probably by updating Canvas or EdX for your instance.
+
+Class Repo Structure
+--------------------
+
+The class repo has three directories in it::
+
+     students
+     examples
+     solutions
+
+The ``students`` dir is where each student should create a directory for their own work. They can then submit a gitHub PR to add their dir to the class repo, and to have their work reviewed when it is ready to be turned in.
+
+The ``examples`` dir can be used to put examples of various sorts -- either from the main PythonCertDevel repo -- or anything you work up for class. It is a good place to put code developed during class as well, so everyone can have access in the future.
+
+The ``solutions`` dir can be used to post solutions to the exercises. How and when (and if) you do that is up the the individual instructor, and may be different depending on the modality. i.e. for self-paced inline there may no appropriate time to post solutions.
+
+
 Example Code
 ============
 
diff --git a/source/for_instructors/solutions.rst b/source/for_instructors/solutions.rst
index b98b888f..86a15b64 100644
--- a/source/for_instructors/solutions.rst
+++ b/source/for_instructors/solutions.rst
@@ -4,7 +4,7 @@ Exercise Solutions
 
 A complete set of solutions is available in the PythonCertDevel Repo.
 
-I've found that students really like to see clean, nicely written solutions to the exercises -- when I started teaching, I would go over the students' code in class, and correct and critique it. But I got a lot of requests for how **I** would have written the code. So I ended up developing a full set of solutions to share with the class.
+I've found that students really like to see clean, nicely written solutions to the exercises -- when I started teaching, I would go over the students' code in class, and correct and critique it. But I got a lot of requests for how *I* would have written the code. So I ended up developing a full set of solutions to share with the class.
 
 When / How to Share Solutions
 =============================
From d92444a119ece763cd960e723a92f05f00544f05 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Fri, 28 Dec 2018 16:27:08 -0800
Subject: [PATCH 43/87] fleshing out the OO mailroom exercise a bit more.
---
 source/exercises/mailroom-oo.rst | 93 +++++++++++++++++++-------------
 1 file changed, 57 insertions(+), 36 deletions(-)
diff --git a/source/exercises/mailroom-oo.rst b/source/exercises/mailroom-oo.rst
index 4cc2b1a5..9df501ad 100644
--- a/source/exercises/mailroom-oo.rst
+++ b/source/exercises/mailroom-oo.rst
@@ -24,13 +24,13 @@ would start to get a bit unwieldy and hard to maintain. So it's a pretty good ca
 As you design appropriate classes, keep in mind these three guidelines for good code structure:
 
 
-1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that everyting that works with that data structure are within one unit. The rest of the code doesn't need to know about the data structure you are using.
+1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that everything that works with that data structure are within one unit. The rest of the code doesn't need to know about the data structure you are using.
 
 2) Separation of Concerns: The user-interaction code should be cleanly separated from the data handling code.
 
    https://en.wikipedia.org/wiki/Separation_of_concerns
 
-   There should be no ``input`` functions in the classes that hold the data!
+   There should be no use of the ``input()`` function in the classes that hold the data!  Nor any use of ``print()`` -- these are for user interaction, and you want the data handling code to be potentially usable with totally different user interaction -- such as a desktop GUI or Web interface.
 
 3) As always: **DRY** (Don't Repeat Yourself): Anywhere you see repeated code; refactor it!
 
@@ -48,23 +48,24 @@ One of the hardest parts of OO design (particularly in Python) is to know how "l
 
 There are no hard and fast rules, but here are some guidelines:
 
-For this assignment it's OK to go with simple tuples. However, in order for the code to be more flexible in the future, for example, if new "fields" were added to the donor object, it's probably better to use a more structured data type, so you don't have to worry about changing the order or number of fields.
+For this simple problem, simple tuples could work fine. However, in order for the code to be more flexible in the future: for example, if new "fields" were added to the donor object, it's probably better to use a more structured data type, so you don't have to worry about changing the order or number of fields.
 
-So now you have to think about using a dict or class. Again for flexibility, I think a dict is a bit easier; you can add fields to it very easily. However, with a class, you can build some functionality in there, too. This is where encapsulation comes in. For instance, one thing you might want to do is get the total of all donations a donor has made in the past. If you add a method to compute that (or a property!), then the rest of the code doesn't need to know how the donations are stored.
+So now you have to think about using a dict or class. Again for flexibility, a dict is a bit easier; you can add fields to it very easily. However, with a class, you can build some functionality in there, too. This is where encapsulation comes in. For instance, one thing you might want to do is get the total of all donations a donor has made in the past. If you add a method to compute that (or a property!), then the rest of the code doesn't need to know how the donations are stored.
 
-Consider ``data[0]`` vs ``data["first_name"]`` vs ``data.first_name``. Which one is more readable? Keep in mind that another benefit of using OO for data encapsulation is ability of modern IDE to provide auto-completion, which reduces number of bugs and helps to produce code faster.
+Consider ``data[0]`` vs ``data["name"]`` vs ``data.name``. Which one is more readable? Keep in mind that another benefit of using OO for data encapsulation is ability of modern IDEs to provide auto-completion, which reduces the number of bugs and helps to produce code faster.
 
 Below are more detailed suggestions on breaking down your existing code into multiple modules that will be part of a single mailroom program.
 
 
-Modules vs. Classes
+Modules and Classes
 ...................
 
 You may organize your code to your preference and keep it simple by having all of the code in a single file.
 
 Optionally, you could organize your code into modules, which helps to keep code organized and re-usable.
 
-What is a module? A module is a python file that can be imported in other files.
+What is a module? A module is a python file with a collection of code that can be imported into other python files.
+
 Modules can contain functions, classes, and even variables (constants).
 
 Here is an example file structure for ``mailroom_oo`` package that contains 3 modules:
@@ -72,28 +73,25 @@ Here is an example file structure for ``mailroom_oo`` package that contains 3 mo
 .. code-block:: bash
 
   └── mailroom_oo
-     ├── __init__.py
      ├── cli_main.py
      ├── donor_models.py
      └── test_mailroom_oo.py
 
-
 The module ``donor_models.py`` can contain the ``Donor`` and ``DonorCollection`` classes.
 
 The module ``cli_main.py`` would include all of your user interaction functions and main program flow.
 
-Note that ``__init__.py`` is an empty file that tells Python that this directory should be treated as a package so that you can import modules.
-
-Donor Class
-...........
+``Donor`` Class
+...............
 
 **Class responsible for donor data encapsulation**
 
-This class will hold all the information about a single donor, and have attributes, properties, and methods to provide access to the donor-specific information that is needed.
-Remember, if you are writing code that only accesses information about a single donor, then it should most likely live in this class.
+This class will hold all the information about a *single* donor, and have attributes, properties, and methods to provide access to the donor-specific information that is needed.
+Any code that only accesses information about a single donor should be part of this class.
+
 
-DonorCollection Class
-.....................
+``DonorCollection`` Class
+.........................
 
 **Class responsible for donor collection data encapsulation**
 
@@ -101,24 +99,35 @@ This class will hold all of the donor objects, as well as methods to add a new d
 
 Your class for the collection of donors will also hold the code that generates reports about multiple donors.
 
+In short: if the functionality involves more than one donor -- it belongs in this class.
+
+Note that the ``DonorCollection`` class should be holding, and working with, ``Donor`` objects -- it should NOT work directly with a list of donations, etc.
+
+**Examples:**
+
+Generating a thank you letter to a donor only requires knowledge of that one donor -- so that code belongs in the ``Donor`` class.
+
+Generating a report about all the donors requires knowledge of all the donors, so that code belongs in the ``DonorCollection`` class.
+
 
 Command Line Interface
 .......................
 
 **Module responsible for main program flow (CLI - Command Line Interface)**
 
-Let's call this module ``cli_main.py`` to represent the entry point for the mailroom program. This module will be using the classes we defined: ``Donor`` and ``DonorCollection``. It will also handle interaction with the user via the ``input`` function calls that gather user input and to provide the output to the console.
+Let's call this module ``cli_main.py`` to represent the entry point for the mailroom program.
+This module will be using the classes we defined: ``Donor`` and ``DonorCollection``.
+It will also handle interaction with the user via the ``input`` function calls that gather user input and to provide the output to the console.
 
 What should go into this module?
 
-* main "switch dictionary" to map user selection to the program features; in general, you will have a method for each of the mailroom functions.
-* ``input`` function calls to gather user input
-* ``print`` statements to print to console
+A set of user-interaction menu functions -- to handle each of the modes of the program.
 
-.. note::  Technically, console print statements don't belong in your data classes. However, for some features of this program, such as "send letters," we are simply printing instead of "sending," so it is ok for this feature to reside in the data class. But do keep integration of console print statements with data classes to a minimum. Ideally, the data class methods return a string, and the UI code does the printing.
+These will include ``input()`` function calls to gather user input, and ``print()`` functions to print results to console.
 
+.. note:: Console print statements don't belong in your data classes. So for features such as "send letters," in which we are simply printing instead of "sending", the data class methods should return a string, and let the UI code do the printing. This will mean there may be very simple functions in the UI code that simply call a method and print the results -- but that does keep flexibility for other ways of handling user interaction.
 
-Why is this separation of data and method so important?
+.. rubric:: Why is this separation of data and method so important?
 
 The idea here is that we should be able to fairly easy replace this CLI program with a different type of interface,
 such as a GUI (Graphical User Interface), without having to make any changes to our data classes.
@@ -130,10 +139,21 @@ Test-Driven Development
 
 At this point we have done a great job refactoring the more complex code out of data-holding classes and we are left with simple classes that are more straightforward to unit test. As you build your classes, update the tests you already have to the logic code to the new API. Ideally, update the tests first, then the code.
 
-The ``Donor`` and ``DonorCollection`` classes should now have close to 100 percent code coverage.
+The ``Donor`` and ``DonorCollection`` classes should now have 100 percent code coverage, which means that every line of code in your ``donor_models.py`` file will be run at least once when your tests are run.
 
 For the moment, don't worry about testing most of the command line interface code. That requires simulating user input, which is an advanced testing topic. But you can (hopefully) see some of the benefits of separating the user-interaction code from the logic code; your logic code is much easier to test with no user-interaction involved.
 
+.. rubric:: refactoring non-OO code
+
+In this case, you already have working code without an OO structure. You should be able to re-use a fair bit of your existing code.
+However, you should still start with the OO structure/design.
+That is, rather than take a non-OO function and try to make it a method of a class, decide what method you need, and what it's API should be, and then see if you have code you can use to fill in that function.
+
+You should expect to re-use a lot of the command line interface code, while refactoring most of the logic code.
+
+If you are not sure at the start what functionality you data classes will need, you can start with the CLI code, and as you find the need for a function, add it to your data classes (after writing a test first, of course).
+
+
 Exercise Guidelines
 ===================
 
@@ -156,10 +176,9 @@ Functionality
 
   - User getting capitalization or spacing or ??? wrong with a name.
 
-    - maybe add logic where you tell them that the name is not in the DB, and do they want to create it, rather than simply creating a new record for a typo in a donor name.
+    - Maybe add logic where you tell them that the name is not in the DB, and do they want to create it, rather than simply creating a new record for a typo in a donor name.
 
-Code structure:
----------------
+.. rubric:: Code structure
 
 * Classes should have clear purpose and encapsulation: only the code within a class should know exactly how the data are stored, for instance.
 
@@ -175,7 +194,7 @@ Code structure:
 
   - The logic code should be 100% testable (without mocking input() or any fancy stuff like that)
 
-Testing:
+.. rubric:: Testing
 
 * All logic code should be tested.
 
@@ -190,21 +209,23 @@ Testing:
   - you'll probably find this easier with a well structured OO approach -- that is, you can test an individual donor functionality without knowing about the rest of the donors.
 
 
-Now the "soft" stuff:
----------------------
+.. rubric:: The "soft" stuff:
 
-* Style: conform to PEP8! (or another consistent style)
+Style:
+    - conform to PEP8! (or another consistent style)
 
-  - You can use 95 or some other reasonable number for line length
+    - You can use 95 or some other reasonable number for line length
 
-* docstrings: functions and classes should all have good docstrings. They can very very short if the function does something simple.
+Docstrings:
+    Functions and classes should all have good docstrings. They can be very short if the function does something simple.
 
-* Naming: all classes, functions, methods, attributes, variables should have appropriate names: meaningful, but not too detailed.
+Naming:
+    All classes, functions, methods, attributes, variables should have appropriate names: meaningful, but not too detailed.
 
-Extra ideas:
+Extra Ideas:
 ------------
 
-In case, you are bored -- what features can you add?
+In case you are bored -- what features can you add?
 
 * How about an html report using your html_render code?
 
From 8b1e84a9593f7c31b9032562d90b36c7628e1187 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Tue, 8 Jan 2019 19:31:22 -0800
Subject: [PATCH 44/87] a bit more clarification in the Subclassing page.
---
 source/modules/SubclassingAndInheritance.rst | 119 +++++++++++++++----
 1 file changed, 95 insertions(+), 24 deletions(-)
diff --git a/source/modules/SubclassingAndInheritance.rst b/source/modules/SubclassingAndInheritance.rst
index edc7d154..220b19a9 100644
--- a/source/modules/SubclassingAndInheritance.rst
+++ b/source/modules/SubclassingAndInheritance.rst
@@ -22,11 +22,11 @@ The resulting classes are known as derived classes or subclasses.
 Subclassing
 -----------
 
-A subclass "inherits" all the attributes (methods, etc) of the parent class.
+A subclass "inherits" all the attributes (methods, etc) of the parent class. This means that a subclass will have everything that its "parents" have.
 
-You can then change ("override") some or all of the attributes to change the behavior.
+You can then change ("override") some or all of the attributes to change the behavior.  You can also add new attributes to extend the behavior.
 
-You can also add new attributes to extend the behavior.  You create a subclass by passing the superclass to the class statement.
+You create a subclass by passing the superclass to the ``class`` statement.
 
 The simplest subclass in Python:
 
@@ -35,33 +35,74 @@ The simplest subclass in Python:
     class A_subclass(The_superclass):
         pass
 
-``A_subclass``  now has exactly the same behavior as ``The_superclass``
+``A_subclass``  now has exactly the same behavior as ``The_superclass`` -- all the same attributes and methods.
 
 Overriding attributes
 ---------------------
 
 Overriding is as simple as creating a new attribute with the same name:
 
-.. code-block:: python
+.. code-block:: ipython
 
-    class Circle:
-        color = "red"
+  In [1]:     class Circle:
+     ...:         color = "red"
+     ...:
 
-    ...
+We now have a class with a class attribute, ``color``, with the value: "red". All instances of ``Circle`` will be red:
 
-    class NewCircle(Circle):
-        color = "blue"
-    >>> nc = NewCircle
-    >>> print(nc.color)
-    blue
+.. code-block:: ipython
+
+  In [2]: c = Circle()
+
+  In [3]: c.color
+  Out[3]: 'red'
+
+If we create a subclass of Circle, and set that same class attribute:
+
+.. code-block:: ipython
+
+  In [4]:     class NewCircle(Circle):
+     ...:         color = "blue"
+     ...:
+
+  In [5]: nc = NewCircle()
+
+  In [6]: nc.color
+  Out[6]: 'blue'
+
+We now have a class that is all the same, except that its instances have the color blue.
+
+Note that any methods that refer to that attribute, will get the new value, even if the methods themselves have not changed:
+
+.. code-block:: ipython
+
+    In [10]: class Circle:
+        ...:     color = "red"
+        ...:
+        ...:     def describe(self):
+        ...:         return f"I am a {self.color} circle"
+        ...:
+
+    In [11]: class NewCircle(Circle):
+        ...:     color = "blue"
+        ...:
+
+    In [12]: c = Circle()
 
+    In [13]: c.describe()
+    Out[13]: 'I am a red circle'
 
-all the ``self``  instances will have the new attribute.
+    In [14]: nc = NewCircle()
+
+    In [15]: nc.describe()
+    Out[15]: 'I am a blue circle'
+
+Note that this is *why* self is passed in to every method -- when you write the method, you don't know exactly what class ``self`` will be -- it is an instance of the class at the time the method is called.
 
 Overriding methods
 ------------------
 
-Same thing, but with methods (remember, a method *is* an attribute in Python)
+Overriding methods is exactly the same thing, but with methods (remember, a method *is* an attribute in Python -- one that happens to be a function)
 
 .. code-block:: python
 
@@ -79,7 +120,7 @@ Same thing, but with methods (remember, a method *is* an attribute in Python)
             self.diameter = self.diameter * math.sqrt(2)
 
 
-all the instances will have the new method.
+all the instances of the new class will have the new method -- similar, but different, behavior.  Note that both these methods are requiring that the class instance has a ``diameter`` attribute.
 
 
 **Here's a program design suggestion:**
@@ -88,12 +129,15 @@ all the instances will have the new method.
 
   If you obey this rule, you will find that any function designed to work with an instance of a superclass, like a Deck, will also work with instances of subclasses like a Hand or PokerHand.  If you violate this rule, your code will collapse like (sorry) a house of cards.
 
+-- from *Think Python*
+
+
 Overriding ``__init__``
 -----------------------
 
 ``__init__`` is a common method to override.
 
-You often need to call the super class ``__init__``  as well.
+You often need to call the super class ``__init__``  as well, so that any initialization required is performed:
 
 .. code-block:: python
 
@@ -110,11 +154,32 @@ You often need to call the super class ``__init__``  as well.
 
 Exception to: "don't change the method signature" rule.
 
+Often when you override ``__init__``, the new class may take an extra parameter or two.  In this case, you will want to keep the signature as similar as possible, and cleanly define what is part of the subclass. A common idiom in this case is this:
+
+.. code-block:: python
+
+    class A_Subclass(A_Superclass):
+
+        def __init__(self, param1, param2, *args, **kwargs):
+            self.param1 = param1
+            self.init_something(param2)
+            super().__init__(*args, **kwargs)
+
+That is:
 
-Using the superclasses' methods
+ * Put the extra parameters in the beginning of the list -- usually as required positional parameters.
+
+ * Accept ``*args`` and ``**kwargs``
+
+ * Pass everything else on to the superclass' __init__
+
+Using ``*args`` and ``**kwargs`` is a way to make it clear that the rest is simply the signature of the superclass.  It is also flexible if the superclass (or others up in the hierarchy) changes -- it could completely change its signature, and this subclass would still work.
+
+
+Using the superclass' methods
 -------------------------------
 
-You can also call the superclass' other methods:
+In a subclass, you can access everything in the superclass: all attributes and other methods:
 
 .. code-block:: python
 
@@ -130,13 +195,13 @@ You can also call the superclass' other methods:
             return Circle.get_area(self, self.radius*2)
 
 
-Note that there is nothing special about ``__init__``  except that it gets called automatically when you instantiate an instance. Otherwise, it is the same as any other method -- it gets ``self`` as the first argument, it can or can not call the superclasses methods, etc.
+Note that there is nothing special about ``__init__``  except that it gets called automatically when you instantiate an instance. Otherwise, it is the same as any other method -- it gets ``self`` as the first argument, it can or can not call the superclass' methods, etc.
 
 
 "Favor Object Composition Over Class Inheritance"
 -------------------------------------------------
 
-That is a quotation from the "Design Patterns" book -- kind of one of the gospels of OO programming.
+That is a quotation from the "Design Patterns" book -- one of the gospels of OO programming.
 
 But what does it mean?
 
@@ -178,6 +243,8 @@ You only want to subclass list if your class could be used anywhere a list can b
 Attribute Resolution Order
 --------------------------
 
+Once there is a potentially large hierarchy of subclasses, how do you know which one will be used?
+
 When you access an attribute:
 
 ``an_instance.something``
@@ -192,6 +259,8 @@ Python looks for it in this order:
 
 It can get more complicated, particularly when there are multiple superclasses (multiple inheritance), but when there is a simple inheritance structure (the usual case) -- it's fairly straightforward.
 
+This is often referred to as "method resolution order" (MRO), because it's more complicated with methods, and in some languages, methods and attributes are more distinct than in Python. In Python, it can be thought of as "name resolution" -- everything in Python is about names and namespaces.
+
 If you want to know more of the gory details -- here's some reading:
 
 https://www.python.org/download/releases/2.3/mro/
@@ -222,7 +291,7 @@ That's about it -- really!
 Type-Based Dispatch
 -------------------
 
-You'll see code that looks like this:
+Occasionally you'll see code that looks like this:
 
 .. code-block:: python
 
@@ -231,12 +300,12 @@ You'll see code that looks like this:
       else:
           Do_something_else
 
-When it's called for, Python provides these utilties:
+When it's called for, Python provides these utilities:
 
     * ``isinstance()``
     * ``issubclass()``
 
-But it is very rarely called for! Between Duck typing, polymorphism, and EAFP, you rarely need to check for type directly.
+But it is *very* rarely called for! Between Duck Typing, polymorphism, and EAFP, you rarely need to check for type directly.
 
 Wrap Up
 -------
@@ -254,3 +323,5 @@ OO can be a very powerful approach, but don't be a slave to what OO is *supposed
 
 Let OO work for you, not *create* work for you.
 
+And the biggest way to do that is to support code re-use.
+
From c8c7b7b74a4959f677524b6cca2566611ead9573 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Tue, 8 Jan 2019 19:37:46 -0800
Subject: [PATCH 45/87] more .gitignore for output files
---
 source/solutions/Lesson07/.gitignore          |  2 ++
 source/solutions/Lesson07/sample_output.html  | 27 +++++++++++++++++++
 .../solutions/Lesson09/mailroom_oo/.gitignore |  2 ++
 3 files changed, 31 insertions(+)
 create mode 100644 source/solutions/Lesson07/.gitignore
 create mode 100644 source/solutions/Lesson07/sample_output.html
 create mode 100644 source/solutions/Lesson09/mailroom_oo/.gitignore
diff --git a/source/solutions/Lesson07/.gitignore b/source/solutions/Lesson07/.gitignore
new file mode 100644
index 00000000..dfec25c4
--- /dev/null
+++ b/source/solutions/Lesson07/.gitignore
@@ -0,0 +1,2 @@
+test_html_output?.html
+
diff --git a/source/solutions/Lesson07/sample_output.html b/source/solutions/Lesson07/sample_output.html
new file mode 100644
index 00000000..9c2e675d
--- /dev/null
+++ b/source/solutions/Lesson07/sample_output.html
@@ -0,0 +1,27 @@
+
+
+    
+        
+        Python Class Sample page
+    
+    
+        Python Class - Html rendering example
+        
+            Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text
+        
+        
+        
+            - 
+                The first item in a list
+            +
- 
+                This is the second item
+            +
- 
+                And this is a 
+                link
+                to google
+            +
+    
+
\ No newline at end of file
diff --git a/source/solutions/Lesson09/mailroom_oo/.gitignore b/source/solutions/Lesson09/mailroom_oo/.gitignore
new file mode 100644
index 00000000..960fe795
--- /dev/null
+++ b/source/solutions/Lesson09/mailroom_oo/.gitignore
@@ -0,0 +1,2 @@
+*.txt
+
From 22ce3a29284dcc796ddde4bf501c1cf130fd8b22 Mon Sep 17 00:00:00 2001
From: Hosung Song 
Date: Sat, 19 Jan 2019 22:06:20 -0800
Subject: [PATCH 46/87] Minor fixes in IteratorsAndGenerators.rst
---
 source/modules/IteratorsAndGenerators.rst | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/source/modules/IteratorsAndGenerators.rst b/source/modules/IteratorsAndGenerators.rst
index 61b0cb2d..f0298ea0 100644
--- a/source/modules/IteratorsAndGenerators.rst
+++ b/source/modules/IteratorsAndGenerators.rst
@@ -115,7 +115,7 @@ https://docs.python.org/3/library/stdtypes.html#iterator-types
 Iterables
 ---------
 
-To make an object iterable, you simply have to implement the __getitem__ method.
+To make an object iterable, you simply have to implement the ``__getitem__`` method.
 
 .. code-block:: python
 
@@ -131,8 +131,8 @@ To make an object iterable, you simply have to implement the __getitem__ method.
 
 How do you get the iterator object from an "iterable"?
 
-The iter function will make any iterable an iterator. It first looks for the __iter__
-method, and if none is found, uses get_item to create the iterator.
+The ``iter`` function will make any iterable an iterator. It first looks for the ``__iter__``
+method, and if none is found, uses ``__getitem__`` to create the iterator.
 
 The ``iter()`` function:
 
@@ -199,7 +199,7 @@ It works, and is fairly efficient, but what about:
     for triple in zip(words[:-2], words[1:-1], words[2:]):
 
 
-zip() returns an iterable -- it does not build up the whole list.
+``zip()`` returns an iterable -- it does not build up the whole list.
 So this is quite efficient.
 
 but we are still slicing: ([1:]), which produces a copy -- so we are creating three copies of
@@ -323,7 +323,7 @@ An "iterator" is anything that conforms to the "iterator protocol":
  - Has a ``__next__()`` method that returns objects.
  - Raises ``StopIteration`` when their are no more objects to be returned.
  - Has a ``__iter__()`` method that returns an iterator -- usually itself.
-   - sometimes the __iter__() method re-sets the iteration...
+   - sometimes the ``__iter__()`` method re-sets the iteration...
 
 https://docs.python.org/3/glossary.html#term-iterator
 
@@ -430,7 +430,7 @@ Really just a shorthand for an iterator class that does the book keeping for you
 To master yield, you must understand that when you call the function,
 the code you have written in the function body does not run. The function
 only returns the generator object. The actual code in the function is run
-when next() is called on the generator itself.
+when ``next()`` is called on the generator itself.
 
 And note that each time you call the "generator function" you get a new
 instance of a generator object that saves state separately from other instances.
@@ -473,7 +473,7 @@ Note: A generator function can also be a method in a class
 In fact, this is a nice way to provide different ways to iterate over
 the data in a class in multiple ways.
 
-This is done by the dict protocol with dict.keys() and dict.values().
+This is done by the dict protocol with ``dict.keys()`` and ``dict.values()``.
 
 More about iterators and generators:
 
@@ -509,11 +509,11 @@ Keep in mind -- if all you need to do with the results is loop over it
 Other uses for ``yield``
 ------------------------
 
-The yield keyword and generator functions were designed with classic "generators" in mind.
+The ``yield`` keyword and generator functions were designed with classic "generators" in mind.
 
 That is -- objects that generate values on the fly.
 
-But, as we alluded to earlier, yield can be used for other things as well.
+But, as we alluded to earlier, ``yield`` can be used for other things as well.
 
 Anytime you want to return a value, and then hold state until later,
 ``yield`` can be used.
@@ -530,7 +530,7 @@ Anytime you want to return a value, and then hold state until later,
         # do the teardown
         something_with(value)
 
-In this case, the yield isn't in any sort of loop or anything.
+In this case, the ``yield`` isn't in any sort of loop or anything.
 It will only get run once. But the generator will maintain state,
 so the value can be used after the yield to do the teardown.
 
From 97a8a01cda193e3366d20af2728183103261d860 Mon Sep 17 00:00:00 2001
From: Hosung Song 
Date: Sun, 20 Jan 2019 11:46:16 -0800
Subject: [PATCH 47/87] Minor formatting fixes in Closures.rst
---
 source/modules/Closures.rst | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/source/modules/Closures.rst b/source/modules/Closures.rst
index 74ef6359..2e15e161 100644
--- a/source/modules/Closures.rst
+++ b/source/modules/Closures.rst
@@ -162,9 +162,9 @@ So there are multiple scopes in play at any point -- the local scope, and all th
     this is in outer
     this is in inner
 
-Look carefully to see where each of those names came from. All the print statements are in the inner function, so its local scope is searched first, and then the outer function's scope, and then the global scope. name1 is only defined in the global scope, so that one is found.
+Look carefully to see where each of those names came from. All the print statements are in the inner function, so its local scope is searched first, and then the outer function's scope, and then the global scope. ``name1`` is only defined in the global scope, so that one is found.
 
-The global keyword
+The ``global`` keyword
 ------------------
 
 global names can be accessed from within functions, but not if that same name is created in the local scope. So you can't change an immutable object that is outside the local scope:
@@ -192,7 +192,7 @@ global names can be accessed from within functions, but not if that same name is
 
 The problem here is that ``x += 5`` is the same as ``x = x + 5``, so it is creating a local name, but it can't be incremented, because it hasn't had a value set yet.
 
-The global keyword tells python that you want to use the global name, rather than create a new, local name:
+The ``global`` keyword tells python that you want to use the global name, rather than create a new, local name:
 
 .. code-block:: ipython
 
@@ -207,9 +207,9 @@ The global keyword tells python that you want to use the global name, rather tha
     In [42]: x
     Out[42]: 10
 
-**NOTE:** the use of global is frowned upon -- having global variables manipulated in arbitrary other scopes makes for buggy, hard to maintain code!
+**NOTE:** The use of ``global`` is frowned upon -- having global variables manipulated in arbitrary other scopes makes for buggy, hard to maintain code!
 
-nonlocal keyword
+``nonlocal`` keyword
 ----------------
 
 The other limitation with ``global`` is that there is only one global namespace, so what if you are in a nested scope, and want to get at the value outside the current scope, but not all the way up at the global scope:
@@ -229,7 +229,7 @@ That's not going to work as the inner x hasn't been initialized:
 
 ``UnboundLocalError: local variable 'x' referenced before assignment``
 
-But if we use ``global``, we'll get the global x:
+But if we use ``global``, we'll get the global ``x``:
 
 .. code-block:: ipython
 
@@ -257,7 +257,7 @@ But if we use ``global``, we'll get the global x:
     In [9]: x
     Out[9]: 15
 
-so the global x is getting changed, but not the one in the outer scope.
+so the global ``x`` is getting changed, but not the one in the ``outer`` scope.
 
 This is enough of a limitation that Python 3 added a new keyword: ``nonlocal``. What it means is that the name should be looked for outside the local scope, but only as far as you need to go to find it:
 
@@ -275,9 +275,9 @@ This is enough of a limitation that Python 3 added a new keyword: ``nonlocal``.
     In [11]: outer()
     x in outer is: 15
 
-So the x in the outer function scope is the one being changed.
+So the ``x`` in the ``outer`` function scope is the one being changed.
 
-While using ``global`` is discouraged, ``nonlocal`` is safer -- as long as it is referring to a name in a scope that is closely defined like the above example. In fact, nonlocal will not go all the way up to the global scope to find a name:
+While using ``global`` is discouraged, ``nonlocal`` is safer -- as long as it is referring to a name in a scope that is closely defined like the above example. In fact, ``nonlocal`` will not go all the way up to the global scope to find a name:
 
 .. code-block:: ipython
 
@@ -404,9 +404,9 @@ But what happens if we call ``counter()`` multiple times?
     In [44]: c2()
     Out[44]: 11
 
-So each time ``counter()`` is called, a new ``incr`` function is created. But also, a new namespace is created, that holds the count name. So the new ``incr`` function is holding a reference to that new count name.
+So each time ``counter()`` is called, a new ``incr`` function is created. But also, a new namespace is created, that holds the ``count`` name. So the new ``incr`` function is holding a reference to that new ``count`` name.
 
-This is what makes in a "closure" -- it carries with it the scope in which it was created.
+This is what makes it a "closure" -- it carries with it the scope in which it was created.
 
 The returned ``incr`` function is a "curried" function -- a function with some parameters pre-specified.
 
@@ -451,7 +451,7 @@ So to compute the scale, I could pass that half-life in each time I called the f
     def scale(time, half_life):
         return 0.5 ** (time / (half_life))
 
-But this is a bit klunky -- I need to keep passing that half_life around, even though it isn't changing. And there are places, like ``map`` that require a function that takes only one argument!
+But this is a bit klunky -- I need to keep passing that ``half_life`` around, even though it isn't changing. And there are places, like ``map`` that require a function that takes only one argument!
 
 What if I could create a function, on the fly, that had a particular half-life "baked in"?
 
@@ -527,7 +527,7 @@ https://docs.python.org/3.5/library/functools.html
 
 Creating a curried function turns out to be common enough that the ``functools.partial`` function provides an optimized way to do it:
 
-What functools.partial does is:
+What ``functools.partial`` does is:
 
  * Makes a new version of a function with one or more arguments already filled in.
  * The new version of a function documents itself.
From 37b13e2d9e5bd654a781d0cc443e6534c44e4796 Mon Sep 17 00:00:00 2001
From: Hosung Song 
Date: Tue, 22 Jan 2019 20:05:24 -0800
Subject: [PATCH 48/87] Minor formatting fixes in Decorators.rst
---
 source/modules/Decorators.rst | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/source/modules/Decorators.rst b/source/modules/Decorators.rst
index 60299383..4234f114 100644
--- a/source/modules/Decorators.rst
+++ b/source/modules/Decorators.rst
@@ -186,7 +186,7 @@ And we can apply it with the regular calling and rebinding syntax:
     In [6]: other_func
     Out[6]: .inner>
 
-Notice that other_func is now the "inner" function, which lives in the "my_decorator" namespace...
+Notice that ``other_func`` is now the "inner" function, which lives in the "my_decorator" namespace...
 
 And this is the same with the decoration syntax:
 
@@ -203,7 +203,7 @@ And this is the same with the decoration syntax:
     In [9]: other_func
     Out[9]: .inner>
 
-Notice that other_func is the "inner" function here as well.
+Notice that ``other_func`` is the "inner" function here as well.
 
 Decorators have the power to replace the decorated function with a different one!
 
@@ -411,7 +411,7 @@ The ``classmethod()`` builtin can do the same thing:
 property()
 -----------
 
-Remember the property() built in?
+Remember the ``property()`` builtin?
 
 Perhaps most commonly, you'll see the ``property()`` builtin used this way.
 
@@ -502,8 +502,8 @@ A decorator that wraps an html `` tag around the output of any decorated func
 
 
     @p_decorate
-        def get_fullname(first_name, last_name):
-            return f"{first_name} {last_name}"
+    def get_fullname(first_name, last_name):
+        return f"{first_name} {last_name}"
 
     In [124]: get_fullname('Chris', 'Barker')
     Out[124]: '
Chris Barker
'
@@ -532,8 +532,8 @@ Can you make a version that will wrap any other tag -- specified as a parameter
 .. code-block:: ipython
 
     @add_tag('p')
-        def get_fullname(first_name, last_name):
-            return f"{first_name} {last_name}"
+    def get_fullname(first_name, last_name):
+        return f"{first_name} {last_name}"
 
     In [124]: get_fullname('Chris', 'Barker')
     Out[124]: 'Chris Barker
'
@@ -545,21 +545,21 @@ But:
 .. code-block:: ipython
 
     @add_tag('div')
-        def get_fullname(first_name, last_name):
-            return f"{first_name} {last_name}"
+    def get_fullname(first_name, last_name):
+        return f"{first_name} {last_name}"
 
     In [124]: get_fullname('Chris', 'Barker')
     Out[124]: 'Chris Barker
'
 
 and you could pass any tag in.
 
-This can be ackomplished either with a closure --nesting antoher level of functions in the decorator, or with a callable class, like the memoize example. Maybe try both, and decide which you like better.
+This can be accomplished either with a closure --nesting another level of functions in the decorator, or with a callable class, like the memoize example. Maybe try both, and decide which you like better.
 
 
 Further Reading:
 ----------------
 
-*Fluent Python* by Luciano Ramalho, chapter 7.
+*Fluent Python* by Luciano Ramalho, Chapter 7.
 
 Another good overview:
 
From 4ba89106ed67626af56fc04a56d93f1b5b6fddeb Mon Sep 17 00:00:00 2001
From: Hosung Song 
Date: Tue, 22 Jan 2019 20:33:52 -0800
Subject: [PATCH 49/87] Minor fixes in ContextManager.rst
---
 .gitignore                         |  6 +++++-
 source/modules/ContextManagers.rst | 14 ++++++--------
 2 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/.gitignore b/.gitignore
index 053ae06c..e6c77b9d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,6 +66,7 @@ instance/
 
 # Sphinx documentation
 docs/_build/
+source/_build/
 
 # PyBuilder
 target/
@@ -104,4 +105,7 @@ ENV/
 .mypy_cache/
 
 # emacs
-*.*~
\ No newline at end of file
+*.*~
+
+# vscode
+.vscode/
diff --git a/source/modules/ContextManagers.rst b/source/modules/ContextManagers.rst
index d0784ba7..60e5d8c0 100644
--- a/source/modules/ContextManagers.rst
+++ b/source/modules/ContextManagers.rst
@@ -104,9 +104,9 @@ If the resource in questions has a ``.close()`` method, then you can simply use
     # and here, it will be closed automatically
 
 But what if the thing doesn't have a ``close()`` method, or you're creating
-the thing and it shouldn't have a close() method?
+the thing and it shouldn't have a ``close()`` method?
 
-(full confession: urlib.request was not a context manager in py2 -- but it is in py3 -- but the issue still comes up with third-party packages and your own code!)
+(full confession: ``urlib.request`` was not a context manager in py2 -- but it is in py3 -- but the issue still comes up with third-party packages and your own code!)
 
 Do It Yourself
 --------------
@@ -158,12 +158,10 @@ clarify the order in which things happen:
 
 .. code-block:: ipython
 
-    In [2]: %paste
-        In [46]: with Context(True) as foo:
-           ....:     print('This is in the context')
-           ....:     raise RuntimeError('this is the error message')
-
-    ## -- End pasted text --
+    In [46]: with Context(True) as foo:
+        ....:     print('This is in the context')
+        ....:     raise RuntimeError('this is the error message')
+        ....:
     __init__(True)
     __enter__()
     This is in the context
From a455b0993c40ed0a9e921dacf11c2ec40abf5cb7 Mon Sep 17 00:00:00 2001
From: Hosung Song 
Date: Tue, 22 Jan 2019 20:42:42 -0800
Subject: [PATCH 50/87] Minor fixes in context-managers-exercise.rst
---
 source/exercises/context-managers-exercise.rst | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/source/exercises/context-managers-exercise.rst b/source/exercises/context-managers-exercise.rst
index 2c826466..67e2b505 100644
--- a/source/exercises/context-managers-exercise.rst
+++ b/source/exercises/context-managers-exercise.rst
@@ -22,7 +22,7 @@ run all the code inside the context:
        ...:
     This code took 0.206805 seconds
 
-NOTE: the time module has what you need:
+NOTE: the ``time`` module has what you need:
 
 .. code-block:: python
 
@@ -30,7 +30,7 @@ NOTE: the time module has what you need:
 
     start = time.clock()
     # some code here
-    elapsed = time.clock() = start
+    elapsed = time.clock() - start
 
 ``time.clock()`` returns the number of seconds that this process has been running.  You can also use ``time.time()``, which gives the "wall time", rather than the process time. ``time()`` will vary more depending on how busy the system is. But you may want to use it if you want to measure how long it takes to download something, for instance.
 
@@ -38,13 +38,13 @@ Extra Credit
 ------------
 
 Allow the ``Timer`` context manager to take a file-like
-object as an argument (the default should be sys.stdout). The results of the
+object as an argument (the default should be ``sys.stdout``). The results of the
 timing should be printed to the file-like object. You could also pass in a name for this particular context, so the message in the file-like object is labeled -- kind of a poor man's logging system.
 
 Extra Extra Credit
 ------------------
 
-Implement this a a generator, wrapped by the:
+Implement this as a generator, wrapped by the:
 
 ``contextlib.contextmanager``
 
@@ -101,7 +101,7 @@ tests fail when an assert fails:
 
     assert some_expression, "a message"
 
-you get a failure when some_expression evaluates as false.
+you get a failure when ``some_expression`` evaluates as false.
 
 This is more-or-less the same as this code:
 
@@ -110,7 +110,7 @@ This is more-or-less the same as this code:
     if some_expression:
         raise AssertionError("a message")
 
-The reason it exists is not so much to save a bit of typing (though that's nice), but that assertions are designed for tests, ans thus can be turned of for an entire python process -- and, indeed are turned off when you turn on optimization.
+The reason it exists is not so much to save a bit of typing (though that's nice), but that assertions are designed for tests, and thus can be turned off for an entire python process -- and, indeed are turned off when you turn on optimization.
 
 So in your context manager, you can raise an AssertionError, or force one with an assert:
 
From f1b2c1c7a4cb64ff24b558b131bcab1d8e8ae4db Mon Sep 17 00:00:00 2001
From: Marcus Kazmierczak 
Date: Tue, 29 Jan 2019 17:03:19 -0800
Subject: [PATCH 51/87] Update string_formatting.rst (#181)
Update domain for string format cookbook, a redirect is already in place, but the old domain will be expiring
---
 source/exercises/string_formatting.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/source/exercises/string_formatting.rst b/source/exercises/string_formatting.rst
index d480f3dd..9879cb58 100644
--- a/source/exercises/string_formatting.rst
+++ b/source/exercises/string_formatting.rst
@@ -222,7 +222,7 @@ https://pyformat.info/
 
 A nice "Cookbook":
 
-https://mkaz.tech/python-string-format.html
+https://mkaz.blog/code/python-string-format-cookbook/
 
 
 Submitting Your Work
From 33e0ccc7a1d7b48726c8409a01029ebec2a91554 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Mon, 18 Feb 2019 09:31:01 -0800
Subject: [PATCH 52/87] some formatting and copy editing.
---
 source/modules/Closures.rst | 84 +++++++++++++++++++++----------------
 1 file changed, 48 insertions(+), 36 deletions(-)
diff --git a/source/modules/Closures.rst b/source/modules/Closures.rst
index 2e15e161..17e70703 100644
--- a/source/modules/Closures.rst
+++ b/source/modules/Closures.rst
@@ -14,14 +14,16 @@ In order to get a handle on all this, it's important to understand variable scop
 
 "Scope" is the word for where the names in your code are accessible. Another word for a scope is namespace.
 
-global
-------
+Global Scope
+------------
 
-The simplest is the global scope. This is where all the names defined right in your code file are (or in the interpreter).
+The simplest is the global scope. This is where all the names defined right in your code file (module) are. When running in an interactavive interpreter,  it is in the global namespace as well.
 
 You can get the global namespace with the ``globals()`` function, but ...
 
-The Python interpreter defines a handful of names when it starts up, and iPython defines a whole bunch more.  Most of those start with an underscore, so you can filter them out for a more reasonable result:
+The Python interpreter defines a handful of names when it starts up, and iPython defines a whole bunch more.
+Recall that a convention in Python is that names that start with an underscore are "special" in some way -- double underscore names have a special meaning to Python, and single underscore names are considered "private".
+Most of the extra names defined by the Python interpreter or iPython that are designed for internal use start with an underscore. These can really "clutter up" the namespace, but they can be filtered out for a more reasonable result:
 
 .. code-block:: python
 
@@ -31,7 +33,7 @@ The Python interpreter defines a handful of names when it starts up, and iPython
             if not (name.startswith("_") or name in ipy_names):
                 print(name)
 
-And run that in a raw interpreter:
+Try running that in a newly started interpreter:
 
 .. code-block:: ipython
 
@@ -47,6 +49,8 @@ And run that in a raw interpreter:
 
 The only name left is "print_globals" itself -- created when we defined the function.
 
+.. note:: Try running ``globals()`` by itself to see all the cruft iPython adds. Also note that ``globals`` returns not just the names, but a dictionary, where the keys are the names, and the items are the values bound to those names.
+
 If we add a name or two, they show up in the global scope:
 
 .. code-block:: ipython
@@ -60,7 +64,7 @@ If we add a name or two, they show up in the global scope:
     x
     this
 
-names are created by assignment, and by ``def`` and ``class`` statements. we already saw a ``def``.
+names are created by assignment, and by ``def`` and ``class`` statements. We already saw a ``def``, here is a ``class`` definition.
 
 .. code-block:: ipython
 
@@ -75,14 +79,14 @@ names are created by assignment, and by ``def`` and ``class`` statements. we alr
     test
     TestClass
 
-local
------
+Local Scope
+-----------
 
 So that's the global scope -- what creates a new scope?
 
 A new, "local" scope is created by a function or class definition:
 
-And there is a built-in function to get the names in the local scope, too, so we can use it to show us the names in a function's local namespace. There isn't a lot of cruft in the local namespace, so we don't need a special function to print it.
+There is a built-in function to get the names in the local scope, too, so we can use it to show us the names in a function's local namespace. There isn't a lot of cruft in the local namespace, so we don't need a special function to print it.
 
 Note that ``locals()`` and ``globals()`` returns a dict of the names and the objects they are bound to, so we can print the keys to get the names:
 
@@ -136,10 +140,32 @@ Turns out that this holds true for functions defined within functions also:
     outer scope: dict_keys(['inner', 'y', 'x'])
     inner scope: dict_keys(['z', 'w'])
 
+Function Parameters
+-------------------
+
+The other way you can define names in a function's local namespace is with function parameters:
+
+
+.. code-block:: ipython
+
+    In [14]: def fun_with_parameters(a, b=0):
+        ...:     print("local names are:", locals().keys())
+        ...:
+        ...:
+
+    In [15]: fun_with_parameters(4)
+    local names are: dict_keys(['a', 'b'])
+
+Notice that no other names have been defined in the function, but both of the parameters (positional and keyword) are local names.
+
+
 Finding Names
 -------------
 
-So there are multiple scopes in play at any point -- the local scope, and all the surrounding scopes. When you use a name, python checks in the local scope first, then moves out one by one until it finds the name. So if you define a new name inside a function, it "overrides" the name in any of the outer scopes. But the outer one will be found.
+At any point, there are multiple scopes in play: the local scope, and all the surrounding scopes.
+When you use a name, python checks in the local scope first, then moves out one by one until it finds the name.
+If you define a new name inside a function, it "overrides" the name in any of the outer scopes.
+But any names not defined in an inner scope will be found by looking in the enclosing scopes.
 
 .. code-block:: ipython
 
@@ -165,9 +191,9 @@ So there are multiple scopes in play at any point -- the local scope, and all th
 Look carefully to see where each of those names came from. All the print statements are in the inner function, so its local scope is searched first, and then the outer function's scope, and then the global scope. ``name1`` is only defined in the global scope, so that one is found.
 
 The ``global`` keyword
-------------------
+----------------------
 
-global names can be accessed from within functions, but not if that same name is created in the local scope. So you can't change an immutable object that is outside the local scope:
+Global names can be accessed from within functions, but not if that same name is created in the local scope. So you can't change an immutable object that is outside the local scope:
 
 .. code-block:: ipython
 
@@ -210,7 +236,7 @@ The ``global`` keyword tells python that you want to use the global name, rather
 **NOTE:** The use of ``global`` is frowned upon -- having global variables manipulated in arbitrary other scopes makes for buggy, hard to maintain code!
 
 ``nonlocal`` keyword
-----------------
+--------------------
 
 The other limitation with ``global`` is that there is only one global namespace, so what if you are in a nested scope, and want to get at the value outside the current scope, but not all the way up at the global scope:
 
@@ -257,7 +283,7 @@ But if we use ``global``, we'll get the global ``x``:
     In [9]: x
     Out[9]: 15
 
-so the global ``x`` is getting changed, but not the one in the ``outer`` scope.
+This indicates that the global ``x`` is getting changed, but not the one in the ``outer`` scope.
 
 This is enough of a limitation that Python 3 added a new keyword: ``nonlocal``. What it means is that the name should be looked for outside the local scope, but only as far as you need to go to find it:
 
@@ -295,7 +321,7 @@ While using ``global`` is discouraged, ``nonlocal`` is safer -- as long as it is
 
 But it will go up multiple levels in nested scopes:
 
-.. code-block: ipython
+.. code-block:: ipython
 
     In [16]: def outer():
         ...:     x = 10
@@ -311,20 +337,6 @@ But it will go up multiple levels in nested scopes:
     In [17]: outer()
     x in outer is: 20
 
-function parameters
--------------------
-
-A side note: function parameters are in a function's local scope, just as though they were created there:
-
-.. code-block:: ipython
-
-    In [28]: def fun(x, y, z):
-        ...:     print(locals().keys())
-        ...:
-
-    In [29]: fun(1,2,3)
-    dict_keys(['z', 'y', 'x'])
-
 Closures
 ========
 
@@ -354,7 +366,7 @@ So after we define a function within a function, we can actually return that fun
             return count
         return incr
 
-So this looks a lot like the previous examples, but we are returning the function that was defined inside the function.
+This looks a lot like the previous examples, but we are returning the function that was defined inside the function. Which means is can be used elsewhere.
 
 What's going on here?
 .....................
@@ -404,9 +416,9 @@ But what happens if we call ``counter()`` multiple times?
     In [44]: c2()
     Out[44]: 11
 
-So each time ``counter()`` is called, a new ``incr`` function is created. But also, a new namespace is created, that holds the ``count`` name. So the new ``incr`` function is holding a reference to that new ``count`` name.
+So each time ``counter()`` is called, a new ``incr`` function is created. Along with the new function, a new namespace is created that holds the ``count`` name. So the new ``incr`` function is holding a reference to that new ``count`` name.
 
-This is what makes it a "closure" -- it carries with it the scope in which it was created.
+This is what makes it a "closure" -- it carries with it the scope in which it was created (or enclosed - I guess that's where the word closure comes from).
 
 The returned ``incr`` function is a "curried" function -- a function with some parameters pre-specified.
 
@@ -414,8 +426,6 @@ Let's experiment a bit more with these ideas:
 
 :download:`play_with_scope.py <../examples/closures_currying/play_with_scope.py>`
 
-.. :download:`capitalize.zip <../examples/packaging/capitalize.zip>`
-
 Currying
 ========
 
@@ -423,7 +433,7 @@ Currying
 
 `Currying on Wikipedia `_
 
-The idea behind currying is that you may have a function with a number of parameters, and you want to make a specialized version of that function with a couple parameters pre-set.
+The idea behind currying is that you may have a function with a number of parameters, and you want to make a specialized version of that function with a couple of parameters pre-set.
 
 
 Real world Example
@@ -442,7 +452,9 @@ So I wanted a function that would compute how much the concentration would reduc
 
 The trick is, how much the concentration would be reduced depends on both time and the half life. And for a given material, and given flow conditions in the river, that half life is pre-determined.  Once you know the half-life, the scale is given by:
 
-scale = 0.5 ** (time / (half_life))
+.. code-block:: python
+
+  scale = 0.5 ** (time / (half_life))
 
 So to compute the scale, I could pass that half-life in each time I called the function:
 
From fb727c5180db21edd27f83f3be29cfde8f453978 Mon Sep 17 00:00:00 2001
From: "Christopher H.Barker, PhD" 
Date: Tue, 19 Feb 2019 18:37:16 -0800
Subject: [PATCH 53/87] Remove cigar (#182)
* changed the "cigar_party" (potentially offensive) examples to "walnut_party"
* added a .gitignore so output files would not be tracked
* adding a bit more to the testing module
* fleshing out the unit testing module a bit
* fix indentation issue
---
 source/examples/testing/cigar_party.py        |  15 -
 source/examples/testing/test_cigar_party.py   |  61 ----
 source/examples/testing/test_random_pytest.py |  44 ++-
 .../examples/testing/test_random_unitest.py   |   4 +-
 source/examples/testing/test_walnut_party.py  |  65 +++++
 source/examples/testing/walnut_party.py       |  15 +
 source/exercises/unit_testing.rst             |  81 ++++--
 source/modules/Testing.rst                    | 272 ++++++++++++++----
 .../Lesson01/codingbat/Logic-1/cigar_party.py |  45 ---
 .../codingbat/Logic-1/walnut_party.py         |  49 ++++
 source/solutions/Lesson06/.gitignore          |   3 +
 source/solutions/Lesson06/cigar_party.py      |  15 -
 source/solutions/Lesson06/test_cigar_party.py |  64 -----
 .../solutions/Lesson06/test_walnut_party.py   |  63 ++++
 source/solutions/Lesson06/walnut_party.py     |  14 +
 .../codingbat/Logic-1/cigar_party.py          |  45 ---
 16 files changed, 513 insertions(+), 342 deletions(-)
 delete mode 100644 source/examples/testing/cigar_party.py
 delete mode 100644 source/examples/testing/test_cigar_party.py
 create mode 100644 source/examples/testing/test_walnut_party.py
 create mode 100644 source/examples/testing/walnut_party.py
 delete mode 100755 source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py
 create mode 100755 source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py
 create mode 100644 source/solutions/Lesson06/.gitignore
 delete mode 100644 source/solutions/Lesson06/cigar_party.py
 delete mode 100644 source/solutions/Lesson06/test_cigar_party.py
 create mode 100644 source/solutions/Lesson06/test_walnut_party.py
 create mode 100644 source/solutions/Lesson06/walnut_party.py
 delete mode 100755 source/solutions/codingbat/Logic-1/cigar_party.py
diff --git a/source/examples/testing/cigar_party.py b/source/examples/testing/cigar_party.py
deleted file mode 100644
index e6863f46..00000000
--- a/source/examples/testing/cigar_party.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env python
-
-"""
-When squirrels get together for a party, they like to have cigars.
-A squirrel party is successful when the number of cigars is between
-40 and 60, inclusive. Unless it is the weekend, in which case there
-is no upper bound on the number of cigars.
-
-Return True if the party with the given values is successful,
-or False otherwise.
-"""
-
-
-def cigar_party(cigars, is_weekend):
-    pass
diff --git a/source/examples/testing/test_cigar_party.py b/source/examples/testing/test_cigar_party.py
deleted file mode 100644
index 260d5f47..00000000
--- a/source/examples/testing/test_cigar_party.py
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python
-
-"""
-When squirrels get together for a party, they like to have cigars.
-A squirrel party is successful when the number of cigars is between
-40 and 60, inclusive. Unless it is the weekend, in which case there
-is no upper bound on the number of cigars.
-
-Return True if the party with the given values is successful,
-or False otherwise.
-"""
-
-
-# you can change this import to test different versions
-from cigar_party import cigar_party
-# from cigar_party import cigar_party2 as cigar_party
-# from cigar_party import cigar_party3 as cigar_party
-
-
-def test_1():
-    assert cigar_party(30, False) is False
-
-
-def test_2():
-    assert cigar_party(50, False) is True
-
-
-def test_3():
-    assert cigar_party(70, True) is True
-
-
-def test_4():
-    assert cigar_party(30, True) is False
-
-
-def test_5():
-    assert cigar_party(50, True) is True
-
-
-def test_6():
-    assert cigar_party(60, False) is True
-
-
-def test_7():
-    assert cigar_party(61, False) is False
-
-
-def test_8():
-    assert cigar_party(40, False) is True
-
-
-def test_9():
-    assert cigar_party(39, False) is False
-
-
-def test_10():
-    assert cigar_party(40, True) is True
-
-
-def test_11():
-    assert cigar_party(39, True) is False
diff --git a/source/examples/testing/test_random_pytest.py b/source/examples/testing/test_random_pytest.py
index 6250d1b4..b9a65afd 100644
--- a/source/examples/testing/test_random_pytest.py
+++ b/source/examples/testing/test_random_pytest.py
@@ -8,32 +8,46 @@
 import pytest
 
 
-seq = list(range(10))
+example_seq = list(range(10))
+
+
+def test_choice():
+    """
+    A choice selected should be in the sequence
+    """
+    element = random.choice(example_seq)
+    assert (element in example_seq)
+
+
+def test_sample():
+    """
+    All the items in a sample should be in the sequence
+    """
+    for element in random.sample(example_seq, 5):
+        assert element in example_seq
 
 
 def test_shuffle():
-    # make sure the shuffled sequence does not lose any elements
+    """
+    Make sure a shuffled sequence does not lose any elements
+    """
+    seq = list(range(10))
     random.shuffle(seq)
-    seq.sort()  # IFyou comment this out, it will fail, so you can see output
+    # seq.sort()  # If you comment this out, it will fail, so you can see output
     print("seq:", seq)  # only see output if it fails
     assert seq == list(range(10))
 
 
 def test_shuffle_immutable():
+    """
+    Trying to shuffle an immutable sequence raises an Exception
+    """
     with pytest.raises(TypeError):
         random.shuffle((1, 2, 3))
 
-
-def test_choice():
-    element = random.choice(seq)
-    assert (element in seq)
-
-
-def test_sample():
-    for element in random.sample(seq, 5):
-        assert element in seq
-
-
 def test_sample_too_large():
+    """
+    Trying to sample more than exist should raise an error
+    """
     with pytest.raises(ValueError):
-        random.sample(seq, 20)
+        random.sample(example_seq, 20)
diff --git a/source/examples/testing/test_random_unitest.py b/source/examples/testing/test_random_unitest.py
index f825be5b..b8a1b712 100644
--- a/source/examples/testing/test_random_unitest.py
+++ b/source/examples/testing/test_random_unitest.py
@@ -8,7 +8,9 @@ def setUp(self):
         self.seq = list(range(10))
 
     def test_shuffle(self):
-        # make sure the shuffled sequence does not lose any elements
+        """
+        make sure the shuffled sequence does not lose any elements
+        """
         random.shuffle(self.seq)
         self.seq.sort()
         self.assertEqual(self.seq, list(range(10)))
diff --git a/source/examples/testing/test_walnut_party.py b/source/examples/testing/test_walnut_party.py
new file mode 100644
index 00000000..62f226dc
--- /dev/null
+++ b/source/examples/testing/test_walnut_party.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+
+"""
+test code for the walnut party example
+
+Adapted from the "coding bat" site: https://codingbat.com/python
+
+When squirrels get together for a party, they like to have walnuts.
+A squirrel party is successful when the number of walnuts is between
+40 and 60, inclusive. Unless it is the weekend, in which case there
+is no upper bound on the number of walnuts.
+
+Return True if the party with the given values is successful,
+or False otherwise.
+"""
+
+
+# you can change this import to test different versions
+from walnut_party import walnut_party
+# from walnut_party import walnut_party2 as walnut_party
+# from walnut_party import walnut_party3 as walnut_party
+
+
+def test_1():
+    assert walnut_party(30, False) is False
+
+
+def test_2():
+    assert walnut_party(50, False) is True
+
+
+def test_3():
+    assert walnut_party(70, True) is True
+
+
+def test_4():
+    assert walnut_party(30, True) is False
+
+
+def test_5():
+    assert walnut_party(50, True) is True
+
+
+def test_6():
+    assert walnut_party(60, False) is True
+
+
+def test_7():
+    assert walnut_party(61, False) is False
+
+
+def test_8():
+    assert walnut_party(40, False) is True
+
+
+def test_9():
+    assert walnut_party(39, False) is False
+
+
+def test_10():
+    assert walnut_party(40, True) is True
+
+
+def test_11():
+    assert walnut_party(39, True) is False
diff --git a/source/examples/testing/walnut_party.py b/source/examples/testing/walnut_party.py
new file mode 100644
index 00000000..9b195b28
--- /dev/null
+++ b/source/examples/testing/walnut_party.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+
+"""
+When squirrels get together for a party, they like to have walnuts.
+A squirrel party is successful when the number of walnuts is between
+40 and 60, inclusive. Unless it is the weekend, in which case there
+is no upper bound on the number of walnuts.
+
+Return True if the party with the given values is successful,
+or False otherwise.
+"""
+
+
+def walnut_party(walnuts, is_weekend):
+    pass
diff --git a/source/exercises/unit_testing.rst b/source/exercises/unit_testing.rst
index 72ba4057..b71653dc 100644
--- a/source/exercises/unit_testing.rst
+++ b/source/exercises/unit_testing.rst
@@ -8,57 +8,89 @@ Preparation
 -----------
 
 In order to do unit testing, you need a framework in which to write and run your tests.
-Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block.  These are, in fact a kind of unit test.
+Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block.  These are, in fact, a kind of unit test.
 But as you build larger systems, you'll want a more structured way to write and run your tests.
 
+We will use the pytest testing system for this class.
 
+If you have not already done so -- install pytest like so:
+
+.. code-block:: bash
+
+    $ python3 -m pip install pytest
+
+Once this is complete, you should have a ``pytest`` command you can run
+at the command line:
+
+.. code-block:: bash
+
+    $ pytest
+    ============================= test session starts ==============================
+    platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1
+    rootdir: /Users/Chris/temp/DrMartins, inifile:
+    plugins: cov-2.6.0
+    collected 0 items
+
+    ========================= no tests ran in 0.01 seconds =========================
+
+If you already HAVE some tests -- you may see something different!
 
 
 Test Driven Development
 -----------------------
 
-Download this module:
-
-:download:`cigar_party.py `
+Download these files, and save them in your own students directory in the class repo:
 
-(This is the `"cigar party" `_ problem from the codingbat site)
+:download:`test_walnut_party.py <../examples/testing/test_walnut_party.py>`
 
-and this test file:
+and:
 
-:download:`test_cigar_party.py `
+:download:`walnut_party.py <../examples/testing/walnut_party.py>`
 
-Put them in the same directory, and make that directory your working directory.
+(This is the adapted from the codingbat site: http://codingbat.com/prob/p195669)
 
-Then try running the test file with pytest:
+In the directory where you put the files, run:
 
 .. code-block:: bash
 
-  $ pytest test_cigar_party
+  $ pytest test_walnut_party.py
+
+You will get a LOT of test failures!
 
 What you've done here is the first step in what is called:
 
-  **Test Driven Development**.
+  **Test Driven Development**
 
 A bunch of tests exist, but the code to make them pass does not yet exist.
 
 The red you see in the terminal when we run our tests is a goad to us to write the code that fixes these tests.
 
-Let's do that next!
+The tests all failed  because currently ``walnut_party()`` looks like:
 
-Test Driven development
------------------------
+.. code-block:: python
+
+  def walnut_party(walnuts, is_weekend):
+      pass
+
+A totally do nothing function.
+
+
+Making tests pass
+-----------------
 
 Open:
 
-``test_cigar_party.py``
+``test_walnut_party.py``
 
 and:
 
-``cigar_party.py``
+``walnut_party.py``
 
 In your editor.
 
-Now edit ``cigar_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass.
+Now edit the function in ``walnut_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass.
+
+When the tests pass -- you are done! That's the beauty of test-driven development.
 
 Doing your own:
 ---------------
@@ -69,16 +101,23 @@ Pick another example from codingbat:
 
 Do a bit of test-driven development on it:
 
-  * run something on the web site.
-  * write a few tests using the examples from the site.
+* Run something on the web site.
+* Write a few tests using the examples from the site.
+* Then write the function, and fix it 'till it passes the tests.
 
-These tests should be in a file names ``test_something.py`` -- I usually name the test file the same as the module it tests,
+These tests should be in a file named ``test_something.py`` -- I usually name the test file the same as the module it tests,
 with ``test_`` prepended.
 
-  * then write the function, and fix it 'till it passes the tests.
+.. note::
+  Technically, you can name your test files anything you want. But there are two reasons to use standard naming conventions.
+  One is that it is clear to anyone looking at the code what is and isn't a test module. The other is that pytest, and other testing systems, use `naming conventions `_ to find your test files.
+  If you name your test files: ``test_something.py`` then pytest will find them for you. And if you use the name of the module being tested:
+  ``test_name_of_tested_module.py`` then it will be clear which test files belong to which modules.
+
 
 Do at least two of these to get the hang of the process.
 
 Also -- once you have the tests passing, look at your solution -- is there a way it could be refactored to be cleaner?
+
 Give it a shot -- you'll know if it still works if the tests still pass!
 
diff --git a/source/modules/Testing.rst b/source/modules/Testing.rst
index 7a37ae3b..f9baa7e6 100644
--- a/source/modules/Testing.rst
+++ b/source/modules/Testing.rst
@@ -26,9 +26,9 @@ block.
 * You can't do anything else when the file is executed without running tests.
 
 
-    This is not optimal.
+This is not optimal.
 
-    Python provides testing systems to help.
+Python provides testing systems to help.
 
 
 Standard Library: ``unittest``
@@ -38,11 +38,9 @@ The original testing system in Python.
 
 ``import unittest``
 
-More or less a port of ``JUnit`` from Java
+More or less a port of `JUnit `_ from Java
 
-A bit verbose: you have to write classes & methods
-
-(And we haven't covered that yet!)
+A bit verbose: you have to write classes & methods (And we haven't covered that yet!)
 
 But here's a bit of an introduction, as you will see this in others' code.
 
@@ -108,32 +106,31 @@ in ``test_my_mod.py``:
 Advantages of ``unittest``
 --------------------------
 
+The ``unittest`` module is pretty full featured
 
-    The ``unittest`` module is pretty full featured
-
-    It comes with the standard Python distribution, no installation required.
+It comes with the standard Python distribution, no installation required.
 
-    It provides a wide variety of assertions for testing all sorts of situations.
+It provides a wide variety of assertions for testing all sorts of situations.
 
-    It allows for a setup and tear down workflow both before and after all tests and before and after each test.
+It allows for a setup and tear down workflow both before and after all tests and before and after each test.
 
-    It's well known and well understood.
+It's well known and well understood.
 
 
 Disadvantages of ``unittest``
 -----------------------------
 
-    It's Object Oriented, and quite "heavyweight".
+It's Object Oriented, and quite "heavyweight".
 
-      - modeled after Java's ``JUnit`` and it shows...
+  - modeled after Java's ``JUnit`` and it shows...
 
-    It uses the framework design pattern, so knowing how to use the features means learning what to override.
+It uses the framework design pattern, so knowing how to use the features means learning what to override.
 
-    Needing to override means you have to be cautious.
+Needing to override means you have to be cautious.
 
-    Test discovery is both inflexible and brittle.
+Test discovery is both inflexible and brittle.
 
-    And there is no built-in parameterized testing.
+And there is no built-in parameterized testing.
 
 
 Other Options
@@ -141,22 +138,24 @@ Other Options
 
 There are several other options for running tests in Python.
 
-* `Nose`: https://nose.readthedocs.org/
+* **Nose2**: https://github.com/nose-devs/nose2
 
-* `pytest`: http://pytest.org/latest/
+* **pytest**: http://pytest.org/latest/
 
 * ... (many frameworks supply their own test runners: e.g. django)
 
-Nose was the most common test runner when I first started learning testing, but it has been in maintaince mode for a while.
+Nose was the most common test runner when I first started learning testing, but it has been in maintenance mode for a while. Even the nose2 site recommends that you consider pytest.
 
 pytest has become the defacto standard test runner for those that want a more "pythonic" test framework.
 
-It is very capable and widely used.
+pytest is very capable and widely used.
 
 For a great description of the strengths of pytest, see:
 
 `The Cleaning Hand of Pytest `_
 
+So we will use pytest for the rest of this class.
+
 Installing ``pytest``
 ---------------------
 
@@ -173,7 +172,7 @@ at the command line:
 
     $ pytest
 
-If you have any tests in your repository, that will find and run them.
+If you have any tests in your repository, that command will find and run them (If you have followed the proper naming conventions).
 
     **Do you?**
 
@@ -182,12 +181,18 @@ Pre-existing Tests
 
 Let's take a look at some examples.
 
-in ``/examples/testing``
+Create a directory to try this out, and download:
+
+:download:`test_random_unitest.py <../examples/testing/test_random_unitest.py>`
+
+In the directory you created for that file, run:
 
 .. code-block:: bash
 
   $ pytest
 
+It should find that test file and run it.
+
 You can also run pytest on a particular test file:
 
 .. code-block:: bash
@@ -205,17 +210,20 @@ Take a few minutes to look these files over.
 What is Happening Here?
 -----------------------
 
-You should have gotten results that look something like this::
+You should have gotten results that look something like this:
+
+.. code-block:: bash
 
-    MacBook-Pro:Session06 Chris$ pytest test_random_unitest.py
+    $ pytest
     ============================= test session starts ==============================
-    platform darwin -- Python 3.6.2, pytest-3.2.3, py-1.4.34, pluggy-0.4.0
-    rootdir: /Users/Chris/PythonStuff/UWPCE/IntroPython-2017/examples/Session06, inifile:
+    platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1
+    rootdir: /Users/Chris/temp/test_temp, inifile:
+    plugins: cov-2.6.0
     collected 3 items
 
-    test_random_unitest.py ...
+    test_random_unitest.py ...                                               [100%]
 
-    =========================== 3 passed in 0.02 seconds ===========================
+    =========================== 3 passed in 0.06 seconds ===========================
 
 
 When you run the ``pytest`` command, ``pytest`` starts in your current
@@ -246,60 +254,204 @@ It will run ``unittest`` tests for you, so can be used as a test runner.
 
 But in addition to finding and running tests, it makes writing tests simple, and provides a bunch of nifty utilities to support more complex testing.
 
+Now download this file:
 
-Test Driven Development
------------------------
+:download:`test_random_pytest.py <../examples/testing/test_random_pytest.py>`
 
-Download these files, and save them in your own students directory in the class repo:
+And run pytest again:
 
-:download:`test_cigar_party.py <../examples/testing/test_cigar_party.py>`
-and:
-:download:`cigar_party.py <../examples/testing/cigar_party.py>`
+.. code-block:: bash
 
-then, in dir where you put the files, run::
+    $ pytest
+    ============================= test session starts ==============================
+    platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1
+    rootdir: /Users/Chris/temp/test_temp, inifile:
+    plugins: cov-2.6.0
+    collected 8 items
 
-  $ pytest test_cigar_party.py
+    test_random_pytest.py .....                                              [ 62%]
+    test_random_unitest.py ...                                               [100%]
 
-You will get a LOT of test failures!
+    =========================== 8 passed in 0.07 seconds ===========================
 
-What we've just done here is the first step in what is called:
+Note that it ran the tests in both the test files.
 
-  **Test Driven Development**.
+Take a look at ``test_random_pytest.py`` -- It is essentially the same tests -- but written in native pytest style -- simple test functions.
 
-The idea is that you write the tests first, and then write the code that passes the tests. In this case, the writing the tests part has been done for you:
+pytest tests
+------------
 
-A bunch of tests exist, but the code to make them pass does not yet exist.
+The beauty of pytest is that it takes advantage of Python's dynamic nature -- you don't need to use any particular structure to write tests.
 
-The red you see in the terminal when we run the tests is a goad to you to write the code that fixes these tests.
+Any function named appropriately is a test.
 
-The tests all failed  because ``cigar_party()`` looks like:
+If the function doesn't raise an error or an assertion, the test passes. It's that simple.
+
+Let's take a look at ``test_random_pytest.py`` to see how this works.
 
 .. code-block:: python
 
-  def cigar_party(cigars, is_weekend):
-      pass
+    import random
+    import pytest
+
+The ``random`` module is imported becasue that's what we are testing.
+``pytest`` only needs to be imported if you are using its utilities -- more on this in a moment.
 
-A totally do nothing function!
+.. code-block:: python
 
-Put real code in  ``cigar_party.py`` until all the tests pass.
+    seq = list(range(10))
 
-When the tests pass -- you are done! That's the beauty of test-driven development.
+Here we create a simple little sequence to use for testing. We put it in the global namespace so other functions can access it.
 
-Trying it yourself
-------------------
+Now the first tests -- simply by naming it ``test_something``, pytest will run it as a test:
+
+.. code-block:: python
+
+    def test_choice():
+        """
+        A choice selected should be in the sequence
+        """
+        element = random.choice(example_seq)
+        assert (element in example_seq)
+
+This is pretty straightforward. We make a random choice from the sequence,
+and then assert that the selected element is, indeed, in the original sequence.
+
+.. code-block:: python
+
+    def test_sample():
+        """
+        All the items in a sample should be in the sequence
+        """
+        for element in random.sample(example_seq, 5):
+            assert element in example_seq
+
+And this is pretty much the same thing, except that it loops to make sure that every item returned by ``.sample`` is in the original sequence.
+
+Note that this will result in 5 separate assertions -- that is fine, you can have as many assertions as you like in one test function. But the test will fail on the first failed assertion -- so you only want to have closely related assertions in each test function.
+
+.. code-block:: python
+
+    def test_shuffle():
+        """
+        Make sure a shuffled sequence does not lose any elements
+        """
+        seq = list(range(10))
+        random.shuffle(seq)
+        seq.sort()  # If you comment this out, it will fail, so you can see output
+        print("seq:", seq)  # only see output if it fails
+        assert seq == list(range(10))
+
+This test is designed to make sure that ``random.shuffle`` only re-arranges the items, but doesn't add or lose any.
+
+In this case, the global ``example_seq`` isn't used, because ``shuffle()`` will change the sequence -- tests should never rely on or alter global state. So a new sequence is created for the test.  This also allows the test to know exactly what the results should be at the end.
+
+Then the "real work" -- calling ``random.shuffle`` on the sequence -- this should re-arrange the elements without adding or losing any.
+
+Calling ``.sort()`` again should put the elements back in the order they started
+
+So we can then test that after shuffling and re-sorting, we have the same sequence back:
 
-Try it a bit more, writing the tests yourself:
+.. code-block:: python
+
+    assert seq == list(range(10))
+
+If that assertion passes, the test will pass.
+
+``print()`` and test failures
+.............................
+
+Try commenting out the sort line:
+
+.. code-block:: python
+
+    # seq.sort()  # If you comment this out, it will fail, so you can see output
+
+And run again to see what happens. This is what I got:
+
+.. code-block:: bash
+
+    $ pytest test_random_pytest.py
+    ============================= test session starts ==============================
+    platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1
+    rootdir: /Users/Chris/PythonStuff/UWPCE/PythonCertDevel/source/examples/testing, inifile:
+    plugins: cov-2.6.0
+    collected 5 items
+
+    test_random_pytest.py F....                                              [100%]
+
+    =================================== FAILURES ===================================
+    _________________________________ test_shuffle _________________________________
+
+        def test_shuffle():
+            """
+            Make sure a shuffled sequence does not lose any elements
+            """
+            seq = list(range(10))
+            random.shuffle(seq)
+            # seq.sort()  # If you comment this out, it will fail, so you can see output
+            print("seq:", seq)  # only see output if it fails
+    >       assert seq == list(range(10))
+    E       assert [4, 8, 9, 3, 2, 0, ...] == [0, 1, 2, 3, 4, 5, ...]
+    E         At index 0 diff: 4 != 0
+    E         Use -v to get the full diff
+
+    test_random_pytest.py:22: AssertionError
+    ----------------------------- Captured stdout call -----------------------------
+    seq: [4, 8, 9, 3, 2, 0, 7, 5, 6, 1]
+    ====================== 1 failed, 4 passed in 0.40 seconds ======================
+
+You get a lot of information when test fails.  It's usually enough to tell you what went wrong.
 
-Pick an example from codingbat:
+Note that pytest didn't print out the results of the print statement when the test passed, but when it failed, it printed it (under "Captured stdout call"). This means you can put diagnostic print calls in your tests, and they will not clutter up the output when they are not needed.
 
-  `codingbat `_
+Testing for Exceptions
+......................
 
-Do a bit of test-driven development on it:
+One of the things you might want to test about your code is that it raises an exception when it should -- and that the exception it raises is the correct one.
+
+In this example, if you try to call ``random.shuffle`` with an immutable sequence, such as a tuple, it should raise a ``TypeError``. Since raising an exception will generally stop the code (and cause a test to fail), we can't use an assertion to test for this.
+
+pytest provides a "context manager", ``pytest.raises``, that can be used to test for exceptions.  The test will pass if and only if the specified Exception is raised by the enclosed code. You use it like so:
+
+.. code-block:: python
+
+    def test_shuffle_immutable():
+        """
+        Trying to shuffle an immutable sequence raises an Exception
+        """
+        with pytest.raises(TypeError):
+            random.shuffle((1, 2, 3))
+
+The ``with`` block is how you use a context manager -- it will run the code enclosed, and perform various actions at the end of the code, or when an exception is raised.
+This is the same ``with`` as used to open files. In that case, it is used to assure that the file is properly closed when you are done with it.  In this case, the ``pytest.raises`` context manager captures any exceptions, and raises an ``AssertionError`` if no exception is raised, or if the wrong exception is raised.
+
+In this case, the test will only pass if a ``TypeError`` is raised by the call to ``random.shuffle`` with a tuple as an argument.
+
+The next test:
+
+.. code-block:: python
+
+    def test_sample_too_large():
+        """
+        Trying to sample more than exist should raise an error
+        """
+        with pytest.raises(ValueError):
+            random.sample(example_seq, 20)
+
+is very similar, except that this time, a ValueError has to be raised for the test to pass.
+
+pytest provides a number of other features for fixtures, parameterized tests, test classes, configuration, shared resources, etc.
+But simple test functions like this will get you very far.
+
+
+Test Driven Development
+-----------------------
 
-   * run something on the web site.
-   * write a few tests using the examples from the site.
-   * then write the function, and fix it 'till it passes the tests.
+Test Driven Development or "TDD", is a development process where you write tests to assure that your code works, *before* you write the actual code.
 
-Do at least two of these...
+This is a very powerful approach, as it forces you to think carefully about exactly what your code should do before you start to write it. It also means that you know when you code is working, and you can refactor it in the future with assurance that you haven't broken it.
 
+Give this exercise a try to get the idea:
 
+:ref:`exercise_unit_testing`
diff --git a/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py b/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py
deleted file mode 100755
index ad7df22c..00000000
--- a/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env python
-
-
-def cigar_party(cigars, is_weekend):
-    """
-    basic solution
-    """
-    if is_weekend and cigars >= 40:
-            return True
-    elif 40 <= cigars <= 60:
-        return True
-    return False
-
-
-def cigar_party2(cigars, is_weekend):
-    """
-    some direct return of bool result
-    """
-    if is_weekend:
-        return (cigars >= 40)
-    return (cigars >= 40 and cigars <= 60)
-
-
-def cigar_party3(cigars, is_weekend):
-    """
-    conditional expression
-    """
-    return (cigars >= 40) if is_weekend else (cigars >= 40 and cigars <= 60)
-
-if __name__ == "__main__":
-    # some tests
-
-    assert cigar_party(30, False) is False
-    assert cigar_party(50, False) is True
-    assert cigar_party(70, True) is True
-    assert cigar_party(30, True) is False
-    assert cigar_party(50, True) is True
-    assert cigar_party(60, False) is True
-    assert cigar_party(61, False) is False
-    assert cigar_party(40, False) is True
-    assert cigar_party(39, False) is False
-    assert cigar_party(40, True) is True
-    assert cigar_party(39, True) is False
-
-    print("All tests passed")
diff --git a/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py b/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py
new file mode 100755
index 00000000..3bdae1d8
--- /dev/null
+++ b/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+"""
+adapted from coding bat: https://codingbat.com/python
+"""
+
+
+def walnut_party(walnuts, is_weekend):
+    """
+    basic solution
+    """
+    if is_weekend and walnuts >= 40:
+        return True
+    elif 40 <= walnuts <= 60:
+        return True
+    return False
+
+
+def walnut_party2(walnuts, is_weekend):
+    """
+    Direct return of bool result
+    """
+    if is_weekend:
+        return (walnuts >= 40)
+    return (walnuts >= 40 and walnuts <= 60)
+
+
+def walnut_party3(walnuts, is_weekend):
+    """
+    Conditional expression
+    """
+    return (walnuts >= 40) if is_weekend else (walnuts >= 40 and walnuts <= 60)
+
+if __name__ == "__main__":
+    # some tests
+
+    assert walnut_party(30, False) is False
+    assert walnut_party(50, False) is True
+    assert walnut_party(70, True) is True
+    assert walnut_party(30, True) is False
+    assert walnut_party(50, True) is True
+    assert walnut_party(60, False) is True
+    assert walnut_party(61, False) is False
+    assert walnut_party(40, False) is True
+    assert walnut_party(39, False) is False
+    assert walnut_party(40, True) is True
+    assert walnut_party(39, True) is False
+
+    print("All tests passed")
diff --git a/source/solutions/Lesson06/.gitignore b/source/solutions/Lesson06/.gitignore
new file mode 100644
index 00000000..8035ab50
--- /dev/null
+++ b/source/solutions/Lesson06/.gitignore
@@ -0,0 +1,3 @@
+*.txt
+
+
diff --git a/source/solutions/Lesson06/cigar_party.py b/source/solutions/Lesson06/cigar_party.py
deleted file mode 100644
index 992d99bd..00000000
--- a/source/solutions/Lesson06/cigar_party.py
+++ /dev/null
@@ -1,15 +0,0 @@
-
-"""
-When squirrels get together for a party, they like to have cigars.
-A squirrel party is successful when the number of cigars is between
-40 and 60, inclusive. Unless it is the weekend, in which case there
-is no upper bound on the number of cigars.
-
-Return True if the party with the given values is successful,
-or False otherwise.
-"""
-
-
-def cigar_party(num, weekend):
-    return num >= 40  and (num <= 60 or weekend)
-
diff --git a/source/solutions/Lesson06/test_cigar_party.py b/source/solutions/Lesson06/test_cigar_party.py
deleted file mode 100644
index 80bc0920..00000000
--- a/source/solutions/Lesson06/test_cigar_party.py
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env python
-
-"""
-When squirrels get together for a party, they like to have cigars.
-A squirrel party is successful when the number of cigars is between
-40 and 60, inclusive. Unless it is the weekend, in which case there
-is no upper bound on the number of cigars.
-
-Return True if the party with the given values is successful,
-or False otherwise.
-"""
-
-
-# you can change this import to test different versions
-from cigar_party import cigar_party
-# from cigar_party import cigar_party2 as cigar_party
-# from cigar_party import cigar_party3 as cigar_party
-
-
-def test_1():
-    assert cigar_party(30, False) is False
-
-
-def test_2():
-
-    assert cigar_party(50, False) is True
-
-
-def test_3():
-
-    assert cigar_party(70, True) is True
-
-
-def test_4():
-    assert cigar_party(30, True) is False
-
-
-def test_5():
-    assert cigar_party(50, True) is True
-
-
-def test_6():
-    assert cigar_party(60, False) is True
-
-
-def test_7():
-    assert cigar_party(61, False) is False
-
-
-def test_8():
-    assert cigar_party(40, False) is True
-
-
-def test_9():
-    assert cigar_party(39, False) is False
-
-
-def test_10():
-    assert cigar_party(40, True) is True
-
-
-def test_11():
-    assert cigar_party(39, True) is False
-
diff --git a/source/solutions/Lesson06/test_walnut_party.py b/source/solutions/Lesson06/test_walnut_party.py
new file mode 100644
index 00000000..5bd7ae45
--- /dev/null
+++ b/source/solutions/Lesson06/test_walnut_party.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+
+"""
+When squirrels get together for a party, they like to have walnuts.
+A squirrel party is successful when the number of walnuts is between
+40 and 60, inclusive. Unless it is the weekend, in which case there
+is no upper bound on the number of walnuts.
+
+Return True if the party with the given values is successful,
+or False otherwise.
+"""
+
+
+# you can change this import to test different versions
+from walnut_party import walnut_party
+# from walnut_party import walnut_party2 as walnut_party
+# from walnut_party import walnut_party3 as walnut_party
+
+
+def test_1():
+    assert walnut_party(30, False) is False
+
+
+def test_2():
+
+    assert walnut_party(50, False) is True
+
+
+def test_3():
+
+    assert walnut_party(70, True) is True
+
+
+def test_4():
+    assert walnut_party(30, True) is False
+
+
+def test_5():
+    assert walnut_party(50, True) is True
+
+
+def test_6():
+    assert walnut_party(60, False) is True
+
+
+def test_7():
+    assert walnut_party(61, False) is False
+
+
+def test_8():
+    assert walnut_party(40, False) is True
+
+
+def test_9():
+    assert walnut_party(39, False) is False
+
+
+def test_10():
+    assert walnut_party(40, True) is True
+
+
+def test_11():
+    assert walnut_party(39, True) is False
diff --git a/source/solutions/Lesson06/walnut_party.py b/source/solutions/Lesson06/walnut_party.py
new file mode 100644
index 00000000..781f71be
--- /dev/null
+++ b/source/solutions/Lesson06/walnut_party.py
@@ -0,0 +1,14 @@
+
+"""
+When squirrels get together for a party, they like to have walnuts.
+A squirrel party is successful when the number of walnuts is between
+40 and 60, inclusive. Unless it is the weekend, in which case there
+is no upper bound on the number of walnuts.
+
+Return True if the party with the given values is successful,
+or False otherwise.
+"""
+
+
+def walnut_party(num, weekend):
+    return num >= 40 and (num <= 60 or weekend)
diff --git a/source/solutions/codingbat/Logic-1/cigar_party.py b/source/solutions/codingbat/Logic-1/cigar_party.py
deleted file mode 100755
index ad7df22c..00000000
--- a/source/solutions/codingbat/Logic-1/cigar_party.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env python
-
-
-def cigar_party(cigars, is_weekend):
-    """
-    basic solution
-    """
-    if is_weekend and cigars >= 40:
-            return True
-    elif 40 <= cigars <= 60:
-        return True
-    return False
-
-
-def cigar_party2(cigars, is_weekend):
-    """
-    some direct return of bool result
-    """
-    if is_weekend:
-        return (cigars >= 40)
-    return (cigars >= 40 and cigars <= 60)
-
-
-def cigar_party3(cigars, is_weekend):
-    """
-    conditional expression
-    """
-    return (cigars >= 40) if is_weekend else (cigars >= 40 and cigars <= 60)
-
-if __name__ == "__main__":
-    # some tests
-
-    assert cigar_party(30, False) is False
-    assert cigar_party(50, False) is True
-    assert cigar_party(70, True) is True
-    assert cigar_party(30, True) is False
-    assert cigar_party(50, True) is True
-    assert cigar_party(60, False) is True
-    assert cigar_party(61, False) is False
-    assert cigar_party(40, False) is True
-    assert cigar_party(39, False) is False
-    assert cigar_party(40, True) is True
-    assert cigar_party(39, True) is False
-
-    print("All tests passed")
From 736e9d5e6a50dd167e8c2e9def522fee6090d069 Mon Sep 17 00:00:00 2001
From: "Christopher H.Barker, PhD" 
Date: Mon, 4 Mar 2019 12:47:50 -0800
Subject: [PATCH 54/87] added Fluent Python to the learning page.
---
 source/references/learning.rst | 2 ++
 1 file changed, 2 insertions(+)
diff --git a/source/references/learning.rst b/source/references/learning.rst
index c50f4196..f66ebbdd 100644
--- a/source/references/learning.rst
+++ b/source/references/learning.rst
@@ -109,6 +109,8 @@ Advanced Books
   (http://chimera.labs.oreilly.com/books/1230000000393):
   The Python Cookbook provides practical solutions to various programming tasks, along with excellent explanations of how they work. Written by David Beazley -- Python author and instructor extraordinaire. Also available fully online: (http://chimera.labs.oreilly.com/books/1230000000393/index.html)
 
+* **Fluent Python**
+  (http://shop.oreilly.com/product/0636920032519.do): Fluent Python is an excellent deeper dive into the inner workings of   Python. Written for folks that already understand the basics of Python, it goes provides explainations of the more advanced topics in Python. If we were going to use a textbook for an advanced Python class, this would be it. 
 
 Evaluating Your Options
 -----------------------------
From 43302d0ff3ff6ad6f419b72b570c74d1ad77c56d Mon Sep 17 00:00:00 2001
From: Natasha 
Date: Wed, 20 Mar 2019 18:34:29 -0700
Subject: [PATCH 55/87] fix print_grid(11) to print_grid(9) (#183)
---
 source/exercises/grid_printer.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/source/exercises/grid_printer.rst b/source/exercises/grid_printer.rst
index 861869ff..87804fdc 100644
--- a/source/exercises/grid_printer.rst
+++ b/source/exercises/grid_printer.rst
@@ -128,7 +128,7 @@ One of the points of writing functions is so you can write code that does simila
 
 Write a function ``print_grid(n)`` that takes one integer argument and prints a grid just like before, *BUT* the size of the grid is given by the argument.
 
-For example, ``print_grid(11)`` prints the grid at the top of this page.
+For example, ``print_grid(9)`` prints the grid at the top of this page.
 
 ``print_grid(3)`` would print a smaller grid::
 
From ccd1695b82ab3d938336fc8b5f1182a4def9870a Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Tue, 2 Apr 2019 22:20:11 -0700
Subject: [PATCH 56/87] added a bit more about Comprehensions and a reference.
---
 source/modules/Comprehensions.rst | 45 ++++++++++++++++++++++++++++++-
 1 file changed, 44 insertions(+), 1 deletion(-)
diff --git a/source/modules/Comprehensions.rst b/source/modules/Comprehensions.rst
index a594c669..19408c2c 100644
--- a/source/modules/Comprehensions.rst
+++ b/source/modules/Comprehensions.rst
@@ -88,7 +88,40 @@ Comprehensions and map()
 
 Comprehensions are another way of expressing the "map" pattern from functional programming.
 
-Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it.
+Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. And there is nothing that can be expressed with ``map()`` that cannot be done with a comprehension. IF youare not familiar with ``map()``, you can saftly skip this, but if you are:
+
+.. code-block:: python
+
+    map(a_function, an_iterable)
+
+is the same as:
+
+.. code-block:: python
+
+    [a_function(item), for item in an_iterable]
+
+In this case, the comprehension is a tad wordier than ``map()``.  BUt comprehensions really shine when you do'nt already have a handy function to pass to map:
+
+.. code-block:: python
+
+    [x**2 for x in an_iterable]
+
+To use ``map()``, you need a function:
+
+.. code-block:: python
+
+    def square(x):
+        return x**2
+
+    map(square, an_iterable)
+
+There are shortcuts of course, including ``lambda`` (stay tuned for more about that):
+
+.. code-block:: python
+
+    map(lambda x: x**2, an_iterable)
+
+But is that easier to read or write?
 
 
 What about filter?
@@ -130,6 +163,8 @@ This is expressing the "filter" pattern and the "map" pattern at the same time -
 
 Get creative....
 
+How do I see all the built in Exceptions?
+
 .. code-block:: python
 
     [name for name in dir(__builtin__) if "Error" in name]
@@ -431,3 +466,11 @@ If you are going to immediately loop through the items created by the comprehens
 
   The "official" term is "generator expression" -- that is what you will see in the Python docs, and a lot of online discussions. I've used the term "generator comprehension" here to better make clear the association with list comprehensions.
 
+References
+----------
+
+This is a nice intro to comprehensions from Trey Hunner:
+
+https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/
+
+Trey writes a lot of good stuff -- I recommned browsing his site.
From 4cc1a1c71e4a8442ab85c8a895424ce858b44966 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Tue, 2 Apr 2019 22:25:33 -0700
Subject: [PATCH 57/87] another link about comprehensions
---
 source/modules/Comprehensions.rst | 4 ++++
 1 file changed, 4 insertions(+)
diff --git a/source/modules/Comprehensions.rst b/source/modules/Comprehensions.rst
index 19408c2c..a813b5ab 100644
--- a/source/modules/Comprehensions.rst
+++ b/source/modules/Comprehensions.rst
@@ -473,4 +473,8 @@ This is a nice intro to comprehensions from Trey Hunner:
 
 https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/
 
+Once you've got the hang of it, you may want to read this so you don't overdo it :-)
+
+https://treyhunner.com/2019/03/abusing-and-overusing-list-comprehensions-in-python/
+
 Trey writes a lot of good stuff -- I recommned browsing his site.
From 16e2806dd5fc7419d6c0becd01d93b345e404899 Mon Sep 17 00:00:00 2001
From: Bryan Davis 
Date: Mon, 8 Apr 2019 00:33:52 -0700
Subject: [PATCH 58/87] Create vsc_as_ide.rst
---
 .../dev_environment/vsc_as_ide.rst            | 69 +++++++++++++++++++
 1 file changed, 69 insertions(+)
 create mode 100644 source/supplemental/dev_environment/vsc_as_ide.rst
diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst
new file mode 100644
index 00000000..a92f7dcb
--- /dev/null
+++ b/source/supplemental/dev_environment/vsc_as_ide.rst
@@ -0,0 +1,69 @@
+.. _vsc_as_ide:
+
+##########################################
+Using Visual Studio Code as a lightweight Python IDE
+##########################################
+
+Visual Studio Code is an extensible and customizable text editor from Microsoft that provides a very minimal layout with additional tooling such as an excellent built-in terminal.
+
+
+Requirements
+============
+
+Any IDE should ease your development experience by providing the following:
+
+* It should provide excellent, configurable syntax colorization.
+* It should allow for robust tab completion.
+* It should offer the ability to jump to the definition of symbols in other files.
+* It should perform automatic code linting to help avoid silly mistakes.
+* It should be able to interact with a Python interpreter such that when debugging, the editor will follow along with the debugger.
+
+Visual Studio Code requires that you perform some setup out of the box (see below for details).
+
+
+Which Version?
+==============
+
+There's just the latest version available to download.
+
+This ensures that all recent bug fixes and updates have been made.
+
+Visual Studio Code runs on Macs, Windows, and Linux flavors like Ubuntu, Debian, Red Hat, etc.
+
+Also, Visual Studio Code performs updates on itself, so there's no need to download newer versions of the app... you should already have it.
+
+Installation
+============
+
+Check out this solid video_ that will walk you through the process of setting up Visual Studio Code for Python in detail.
+
+.. _video: https://www.youtube.com/watch?v=TILIcrrVABg/
+
+Go to the Visual Studio Code website_.
+
+.. _website: https://code.visualstudio.com/
+
+Scroll down to the bottom of the page and you'll see links for installers to all the major OS platforms.
+
+Download your flavor and run the installer.
+
+
+Basic Settings
+==============
+
+Visual Studio Code can be used out of the box with no setup as a text editor. It automatically
+recognizes file types and helpfully highlights text accordingly. To use in this manner,
+write your Python files in Visual Studio Code, then run them in your Python command prompt 
+or Visual Studio Code's own built in terminal (Ctrl + \`)
+
+
+Extending the Editor
+====================
+
+After you've install Visual Studio Code, there are many ways to extend it for working with Python.
+
+The video linked above goes into this much deeper.
+
+There is also a great tutorial for setting up Python here_.
+
+.. _here: https://code.visualstudio.com/docs/python/python-tutorial
From b697855aad57a44f3c569b4c517218164dfdcfb7 Mon Sep 17 00:00:00 2001
From: Bryan Davis 
Date: Mon, 8 Apr 2019 00:36:01 -0700
Subject: [PATCH 59/87] Update vsc_as_ide.rst
@ChrisBarker-NOAA
Here's a starter page.
I plan add more on linting and debugging soon.
Ran out of time and the video / tutorial from Microsoft is pretty solid.
Thanks
---
 source/supplemental/dev_environment/vsc_as_ide.rst | 1 +
 1 file changed, 1 insertion(+)
diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst
index a92f7dcb..5539cc33 100644
--- a/source/supplemental/dev_environment/vsc_as_ide.rst
+++ b/source/supplemental/dev_environment/vsc_as_ide.rst
@@ -67,3 +67,4 @@ The video linked above goes into this much deeper.
 There is also a great tutorial for setting up Python here_.
 
 .. _here: https://code.visualstudio.com/docs/python/python-tutorial
+
From 4c172739bf369c7f178fb49c26b5485914f390a8 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Mon, 8 Apr 2019 21:44:12 -0700
Subject: [PATCH 60/87] a bit of clean up of the editor tutorials -- and adding
 links to the visual studio code page.
---
 source/modules/Class_introduction.rst         | 12 +++++-----
 source/supplemental/dev_environment/index.rst |  8 +++++--
 .../dev_environment/sublime_as_ide.rst        | 23 +++++++++----------
 .../dev_environment/vsc_as_ide.rst            |  8 +++----
 4 files changed, 27 insertions(+), 24 deletions(-)
diff --git a/source/modules/Class_introduction.rst b/source/modules/Class_introduction.rst
index 308326bd..363e0421 100644
--- a/source/modules/Class_introduction.rst
+++ b/source/modules/Class_introduction.rst
@@ -9,7 +9,7 @@ In which you are introduced to this class, your instructors, your environment an
 
 .. image:: /_static/python.png
     :align: center
-    :width: 38%
+    :width: 80%
 
 
 `xkcd.com/353`_
@@ -74,9 +74,9 @@ Interrupt us with questions -- please!
 Homework:
 ---------
 
-* Homework will primarily be reading, a handful of videos, and links to optional external materials -- videos, blog posts, etc.
+* Homework will be reading: a handful of videos, and links to optional external materials -- videos, blog posts, etc.
 
-* Exercises will be started in class -- but you can finish them at home.
+* Exercises will be started in class -- but you can finish them at home (and you will need time to do that!)
 
 * You are adults -- it's up to you to do the homework. But if you don't code, you won't learn to code. And we can't give you a certificate if you haven't demonstrated that you've done the work.
 
@@ -87,11 +87,11 @@ There is a video about that, and we will show you in the first class as well.
 Communication
 -------------
 
-**MS Teams:**
+**Mailing List**
 
-There has been an MS Team channel set up for this class. You should have been invited to join -- if not, let your instructors know.
+There should have been a Mailing List set up for this class. You should have been invited to join -- if not, let your instructors know. Also let them know if you would prefer a different email address.
 
-Anything Python related is fair game. Keep general discussion about the class or Python in the main channel, and maybe set up separate channels for particular assignments.
+Anything Python related is fair game.  Questions and discussion about the assignments are encouraged.
 
 We highly encourage you to work together. You will learn at a much deeper level if you work together, and it gets you ready to collaborate with colleagues.
 
diff --git a/source/supplemental/dev_environment/index.rst b/source/supplemental/dev_environment/index.rst
index 8eb96522..9f98c5cf 100644
--- a/source/supplemental/dev_environment/index.rst
+++ b/source/supplemental/dev_environment/index.rst
@@ -278,13 +278,16 @@ http://www.sublimetext.com/
 
 :ref:`sublime_as_ide`
 
-
 "Atom" is another good open source option.
 
 https://atom.io/
 
 :ref:`atom_as_ide`
 
+"Visual Studio Code" is a relatively new cross platfrom offering from Microsoft -- a lot of folks seem to like it:
+
+:ref:`vsc_as_ide`
+
 And, of course, vim or Emacs on Linux, if you are familiar with those.
 
 Why No IDE?
@@ -377,7 +380,7 @@ You should be able to run git on the command line:
 .. code-block:: bash
 
     $ git --version
-    git version 2.11.0 (Apple Git-81)
+    git version 2.20.1 (Apple Git-117)
 
 It should be version >= 2
 
@@ -404,6 +407,7 @@ Specific Documentation
 
     sublime_as_ide
     atom_as_ide
+    vsc_as_ide
     command_line
     shell
     ipython
diff --git a/source/supplemental/dev_environment/sublime_as_ide.rst b/source/supplemental/dev_environment/sublime_as_ide.rst
index 65f0208e..1f96594a 100644
--- a/source/supplemental/dev_environment/sublime_as_ide.rst
+++ b/source/supplemental/dev_environment/sublime_as_ide.rst
@@ -34,6 +34,7 @@ And some more advanced features that you may want later:
 
 Which Version?
 ==============
+
 Use version 3 -- it is updated now and again, so make sure to get the latest.
 
 *Use Sublime Version 3*
@@ -42,7 +43,9 @@ Use version 3 -- it is updated now and again, so make sure to get the latest.
 Basic Settings
 ==============
 
-All configuration in Sublime Text is done via `JSON `_. It's simple to learn. go and read that link then return here. [Note that JSON is very similar to Python dict and list literals]
+All configuration in Sublime Text is done via `JSON `_. It's simple to learn. Go and read that link then return here.
+
+.. note:: JSON is very similar to Python dict and list literals. Though it has its root in Javascript, it is also used in a wide variety of applications,  and is well supported by Python. But a key difference is that it does not allow trailing commas after items in lists -- so be careful.
 
 There are a number of `different levels of configuration `_ in Sublime Text. You will most often work on settings at the user level.
 
@@ -86,7 +89,7 @@ Here's a reasonable set of preliminary settings (theme, color scheme and font ar
 
 **NOTE:** Especially important is the setting ``translate_tabs_to_spaces``, which ensures that any time you hit a tab key, the single  character is replaced by four  characters.  In Python this is **vital**!
 
-If you do nothing else, add this for your config!
+If you do nothing else, add ``translate_tabs_to_spaces`` to your config!
 
 Extending the Editor
 ====================
@@ -125,7 +128,7 @@ Anaconda
 
 There are a bunch of Python-related plugins available. However, Anaconda is a nice package that provides most of the features you want, so plan on using just that one.
 
-Not to be confused with the Scientific Python distribution -- the Anaconda sublime plugin is a full featured package to turn Sublime into a pretty full IDE:
+Not to be confused with the scientific Python distribution -- the Anaconda sublime plugin is a full featured package to turn Sublime into a pretty full IDE:
 
 http://damnwidget.github.io/anaconda/
 
@@ -144,7 +147,9 @@ A few settings you'll want to change
 
 There are a few setting you may want to change:
 
-* max line length for the linter: default is 72, which is pretty short these day. I use 95
+* max line length for the linter: default is 72, which is pretty short these day. I use 95::
+
+        "pep8_max_line_length": 95,
 
 
 White Space Management
@@ -171,13 +176,7 @@ You'll probably want to wait on this until you start using a debugger, but it's
 
 The final requirement for a reasonable IDE experience is to be able to follow a debugging session in the file where the code exists.
 
-There is no plugin for Sublime Text that supports this. But there is a Python package you can install.
-
-The package is called `PDBSublimeTextSupport `_ and its simple to install with ``pip``:
-
-.. code-block:: bash
-
-    $ python -m  pip install PDBSublimeTextSupport
 
-With that package installed in the Python that is used for your project, any breakpoint you set will automatically pop to the surface in Sublime Text.  And as you step through the code, you will see the current line in your Sublime Text file move along with you.
+This: https://packagecontrol.io/packages/Python%20Debugger
 
+Looks promising as a debugger plugin for sublime.
diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst
index 5539cc33..cd35aee0 100644
--- a/source/supplemental/dev_environment/vsc_as_ide.rst
+++ b/source/supplemental/dev_environment/vsc_as_ide.rst
@@ -1,8 +1,8 @@
 .. _vsc_as_ide:
 
-##########################################
+####################################################
 Using Visual Studio Code as a lightweight Python IDE
-##########################################
+####################################################
 
 Visual Studio Code is an extensible and customizable text editor from Microsoft that provides a very minimal layout with additional tooling such as an excellent built-in terminal.
 
@@ -53,8 +53,8 @@ Basic Settings
 
 Visual Studio Code can be used out of the box with no setup as a text editor. It automatically
 recognizes file types and helpfully highlights text accordingly. To use in this manner,
-write your Python files in Visual Studio Code, then run them in your Python command prompt 
-or Visual Studio Code's own built in terminal (Ctrl + \`)
+write your Python files in Visual Studio Code, then run them in your Python command prompt
+or Visual Studio Code's own built in terminal: Ctrl + \` (control-backtick)
 
 
 Extending the Editor
From 7da2e15ecd99c0029fa631aef8c9b4641fad8788 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Mon, 8 Apr 2019 22:13:59 -0700
Subject: [PATCH 61/87] another coding bat
---
 source/solutions/codingbat/List-2/sum13.py | 64 ++++++++++++++++++++++
 1 file changed, 64 insertions(+)
 create mode 100644 source/solutions/codingbat/List-2/sum13.py
diff --git a/source/solutions/codingbat/List-2/sum13.py b/source/solutions/codingbat/List-2/sum13.py
new file mode 100644
index 00000000..e2e77ad9
--- /dev/null
+++ b/source/solutions/codingbat/List-2/sum13.py
@@ -0,0 +1,64 @@
+def sum13(l):
+    tot = 0
+    prev = 0
+    for i in range(len(l)):
+        if l[i] != 13 and prev != 13:
+            tot += l[i]
+        prev = l[i]
+    return tot
+
+
+# def sum13(l):
+#     prev, tot = 0, 0
+#     for i in l:
+#         if i != 13 and prev != 13:
+#             tot += i
+#         prev = i
+#     return tot
+
+
+# def sum13(l):
+#     tot = 0
+#     i = 0
+#     while i < len(l):
+#         if l[i] != 13:
+#             tot += l[i]
+#             i += 1
+#         else:
+#             i += 2
+#     return tot
+
+
+# def sum13(l):
+#     tot = 0
+#     l_iter = iter(l)
+#     for i in l_iter:
+#         if i == 13:
+#             try:
+#                 next(l_iter)
+#             except StopIteration:
+#                 break
+#         else:
+#             tot += i
+#     return tot
+
+
+if __name__ == "__main__":
+
+    assert sum13([1, 2, 2, 1]) == 6
+    assert sum13([1, 1]) == 2
+    assert sum13([1, 2, 2, 1, 13]) == 6
+    assert sum13([1, 2, 13, 2, 1, 13]) == 4
+    assert sum13([13, 1, 2, 13, 2, 1, 13]) == 3
+    assert sum13([]) == 0
+    assert sum13([13]) == 0
+    assert sum13([13, 13]) == 0
+    assert sum13([13, 0, 13]) == 0
+    assert sum13([13, 1, 13]) == 0
+    assert sum13([5, 7, 2]) == 14
+    assert sum13([5, 13, 2]) == 5
+    assert sum13([0]) == 0
+    assert sum13([13, 0]) == 0
+
+    print("all asserts passed")
+
From c4226fe3f4eb58a85316633b8246db3668d6c142 Mon Sep 17 00:00:00 2001
From: Bryan Davis 
Date: Tue, 9 Apr 2019 23:47:51 -0700
Subject: [PATCH 62/87] Update
Adding info on default Git editor
---
 source/supplemental/dev_environment/vsc_as_ide.rst | 3 +++
 1 file changed, 3 insertions(+)
diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst
index cd35aee0..eb43e57d 100644
--- a/source/supplemental/dev_environment/vsc_as_ide.rst
+++ b/source/supplemental/dev_environment/vsc_as_ide.rst
@@ -68,3 +68,6 @@ There is also a great tutorial for setting up Python here_.
 
 .. _here: https://code.visualstudio.com/docs/python/python-tutorial
 
+I also recommend setting up Visual Studio Code as your default_ Git editor
+.. _default: https://stackoverflow.com/questions/30024353/how-to-use-visual-studio-code-as-default-editor-for-git
+
From ca0d4d7b75d7b2dc4a86dbf9098ae4974e2c35ae Mon Sep 17 00:00:00 2001
From: Bryan Davis 
Date: Tue, 9 Apr 2019 23:48:43 -0700
Subject: [PATCH 63/87] another update
---
 source/supplemental/dev_environment/vsc_as_ide.rst | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst
index eb43e57d..21af2f4c 100644
--- a/source/supplemental/dev_environment/vsc_as_ide.rst
+++ b/source/supplemental/dev_environment/vsc_as_ide.rst
@@ -68,6 +68,7 @@ There is also a great tutorial for setting up Python here_.
 
 .. _here: https://code.visualstudio.com/docs/python/python-tutorial
 
-I also recommend setting up Visual Studio Code as your default_ Git editor
+I also recommend setting up Visual Studio Code as your default_ Git editor.
+
 .. _default: https://stackoverflow.com/questions/30024353/how-to-use-visual-studio-code-as-default-editor-for-git
 
From c572010f1d24335a48d139d39a2eaaaaf8a75365 Mon Sep 17 00:00:00 2001
From: Bryan Davis 
Date: Tue, 9 Apr 2019 23:50:19 -0700
Subject: [PATCH 64/87] Update vsc_as_ide.rst
---
 source/supplemental/dev_environment/vsc_as_ide.rst | 4 ++++
 1 file changed, 4 insertions(+)
diff --git a/source/supplemental/dev_environment/vsc_as_ide.rst b/source/supplemental/dev_environment/vsc_as_ide.rst
index 21af2f4c..98a0826b 100644
--- a/source/supplemental/dev_environment/vsc_as_ide.rst
+++ b/source/supplemental/dev_environment/vsc_as_ide.rst
@@ -68,6 +68,10 @@ There is also a great tutorial for setting up Python here_.
 
 .. _here: https://code.visualstudio.com/docs/python/python-tutorial
 
+If you're on a Mac, be sure to set up your path_ for easy integration with the terminal.
+
+.. _path: https://code.visualstudio.com/docs/setup/mac
+
 I also recommend setting up Visual Studio Code as your default_ Git editor.
 
 .. _default: https://stackoverflow.com/questions/30024353/how-to-use-visual-studio-code-as-default-editor-for-git
From ac442bfd64c2b7a3303a28a2236b4edee02206ec Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sat, 13 Apr 2019 12:30:28 -0700
Subject: [PATCH 65/87] changed references to "lesson**" from "session**" for
 the exercise submissions.
---
 source/exercises/dict_lab.rst        | 2 +-
 source/exercises/except_exercise.rst | 2 +-
 source/exercises/fib_and_lucas.rst   | 2 +-
 source/exercises/list_lab.rst        | 2 +-
 source/exercises/python_pushups.rst  | 9 +++++++--
 source/exercises/rot13.rst           | 2 +-
 6 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/source/exercises/dict_lab.rst b/source/exercises/dict_lab.rst
index c887fa33..e048e032 100644
--- a/source/exercises/dict_lab.rst
+++ b/source/exercises/dict_lab.rst
@@ -15,7 +15,7 @@ Learn the basic ins and outs of Python dictionaries and sets.
 Procedure
 ---------
 
-In your student dir in the class repo, create a ``session04`` dir and put in a new ``dict_lab.py`` file.
+In your student dir in the class repo, create a ``lesson04`` dir and put in a new ``dict_lab.py`` file.
 
 The file should be an executable Python script. That is to say that one
 should be able to run the script directly like so:
diff --git a/source/exercises/except_exercise.rst b/source/exercises/except_exercise.rst
index 62104ccf..c4b0fdc1 100644
--- a/source/exercises/except_exercise.rst
+++ b/source/exercises/except_exercise.rst
@@ -9,7 +9,7 @@ This is a little exercise that shows you how to handle exceptions in a way that
 Procedure
 =========
 
-Here are two files that you should put in your ``session05`` directory in the class repo.
+Here are two files that you should put in your ``lesson05`` directory in the class repo.
 
 :download:`except_exercise.py`
 
diff --git a/source/exercises/fib_and_lucas.rst b/source/exercises/fib_and_lucas.rst
index 189aaa29..ba0e5c21 100644
--- a/source/exercises/fib_and_lucas.rst
+++ b/source/exercises/fib_and_lucas.rst
@@ -25,7 +25,7 @@ We will write a function that computes this series -- then generalize it.
 Step 1
 ------
 
-* Create a new module ``series.py`` in the ``session02`` folder in your student folder.
+* Create a new module ``series.py`` in the ``lesson02`` folder in your student folder.
 
   - In it, add a function called ``fibonacci``.
 
diff --git a/source/exercises/list_lab.rst b/source/exercises/list_lab.rst
index 205acf9d..f4c78606 100644
--- a/source/exercises/list_lab.rst
+++ b/source/exercises/list_lab.rst
@@ -29,7 +29,7 @@ to query the user for info at the command line, you use:
 Procedure
 ---------
 
-In your student dir in the class repo, create a ``session03`` dir and put in a new ``list_lab.py`` file.
+In your student dir in the class repo, create a ``lesson03`` dir and put in a new ``list_lab.py`` file.
 
 The file should be an executable Python script. That is to say that one
 should be able to run the script directly like so:
diff --git a/source/exercises/python_pushups.rst b/source/exercises/python_pushups.rst
index 146ef766..7586521b 100644
--- a/source/exercises/python_pushups.rst
+++ b/source/exercises/python_pushups.rst
@@ -11,8 +11,8 @@ Task 1: Explore Errors
 
 * Create a new directory in your working dir for the class::
 
-  $ mkdir session01
-  $ cd session01
+  $ mkdir lesson01
+  $ cd lesson01
 
 * Add a new file to it called ``break_me.py``
 
@@ -38,3 +38,8 @@ exercises at "Coding Bat": http://codingbat.com/python
 There are 8 sets of puzzles. Do as many as you can, but try to get to at least
 the "Warmups".
 
+While the codingbat site has a great interface for submitting your solution and see if it works, we suggest you write your code in your regular text editor and get it to run on your machine first.
+
+Put at least one solution in the ``lesson1`` folder you created in git and submit it via pull request.
+
+
diff --git a/source/exercises/rot13.rst b/source/exercises/rot13.rst
index 8000d19b..26dbcc47 100644
--- a/source/exercises/rot13.rst
+++ b/source/exercises/rot13.rst
@@ -22,7 +22,7 @@ circle, so it wraps around).
 The task
 --------
 
-Add a python module named ``rot13.py`` to the session03 dir in your student dir. This module should provide at least one function called ``rot13`` that takes any amount of text and returns that same text encrypted by ROT13.
+Add a python module named ``rot13.py`` to the lesson03 dir in your student dir. This module should provide at least one function called ``rot13`` that takes any amount of text and returns that same text encrypted by ROT13.
 
 This function should preserve whitespace, punctuation and capitalization.
 
From b0264b51db2aa6514436e54edfa42f995a3ef38f Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sat, 13 Apr 2019 13:57:39 -0700
Subject: [PATCH 66/87] updated gitbash instructions to use .bash_profile
---
 .../supplemental/installing/python_for_windows.rst | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/source/supplemental/installing/python_for_windows.rst b/source/supplemental/installing/python_for_windows.rst
index b35f77c5..e2579c2e 100644
--- a/source/supplemental/installing/python_for_windows.rst
+++ b/source/supplemental/installing/python_for_windows.rst
@@ -40,7 +40,7 @@ Terminal
 
 If you are confident in your use of the "DOS Box" or "powershell", command lines, feel free to use one of those. However, your life may be easier if you install "Git Bash", as then you can follow unix-style terminal instructions exactly, and do not have to translate. Also, your instructors are more experienced with Bash.
 
-From now on, if you hear the terms "bash", "shell" or "terminal", or "commandline" know that this is the application that is being referred to. We will use those terms interchangably to mean ANY command line.
+From now on, if you hear the terms "bash", "shell" or "terminal", or "command line" know that this is the application that is being referred to. We will use those terms interchangeably to mean ANY command line.
 
 When you install Git Bash, you are installing git (and a git gui) as well, thus killing two birds with one stone!
 
@@ -51,7 +51,7 @@ You can go through the rest of the prompts using default values. Once you are do
 
 You can use this git with the DOS box or Powershell as well.
 
-This is also a good bet for running Python -- If you use the Git Bash shell, you can use the same commands as Linux and OS-X users. Regardless of which shell you choose, you will need to add Python to your environment. It is possible that this was done during the installation of Python. If you type 'which python' into your terminal, and get back the answer '/c/python34/python', then you are good to go, otherwise (which shouldn't happen if you checked the "Add Python 3.7 to PATH" checkbox when you installed Python above), follow the instructions here:
+This is also a good bet for running Python -- If you use the Git Bash shell, you can use the same commands as Linux and OS-X users. Regardless of which shell you choose, you will need to add Python to your environment. It is possible that this was done during the installation of Python. If you type 'which python' into your terminal, and get back the answer '/c/python37/python', then you are good to go, otherwise (which shouldn't happen if you checked the "Add Python 3.7 to PATH" checkbox when you installed Python above), follow the instructions here:
 
 http://www.computerhope.com/issues/ch000549.htm
 
@@ -101,7 +101,7 @@ Note: if you have trouble running ``python`` command in your gitbash (it hangs),
 
 ::
 
-  $ echo "alias python='winpty python'" >> ~/.bashrc
+  $ echo "alias python='winpty python'" >> ~/.bash_profile
 
 You will need to close the current bash window and restart a new one to get this alias. Then from now on, you can just type ``python`` and it should work on git bash (no more hanging) as well.
 
@@ -128,7 +128,7 @@ It should download and install the latest ``pip``.
 
 You can now use pip to install other packages.
 
- The first thing you may want to do is update pip itself:
+The first thing you may want to do is update pip itself:
 
 .. code-block:: bash
 
@@ -171,7 +171,7 @@ You should now be able to run ``iPython`` from the git bash shell or "DOS Box" o
     IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.
     (or from the DOS box or PowerShell prompt)
 
-We will use this as our default Python interpreter.
+We will use this in class as our default Python interpreter.
 
 
 Testing it out
@@ -187,7 +187,7 @@ All available from the command line.
 
 To try it out, you should be able to run all of these commands, and get something like the following results:
 
-(recall that you can get out of the python or iPython command lines with ``ctrl+Z``)
+(recall that you can get out of the python or iPython command lines with ``ctrl+Z`` (ifthat doesn't work, try ``ctrl+D`` -- the *nix version))
 
 For Python:
 
@@ -209,7 +209,7 @@ For iPython:
   Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)]
   Type 'copyright', 'credits' or 'license' for more information
   IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help.
-  
+
   In [1]:
   Do you really want to exit ([y]/n)? y
 
From 41a68459428003b11661913222b2982315a7d2a7 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Mon, 15 Apr 2019 22:14:00 -0700
Subject: [PATCH 67/87] another coding bat solution
---
 .../codingbat/String-2/count_code.py          | 20 +++++++++++++++++++
 1 file changed, 20 insertions(+)
 create mode 100644 source/solutions/codingbat/String-2/count_code.py
diff --git a/source/solutions/codingbat/String-2/count_code.py b/source/solutions/codingbat/String-2/count_code.py
new file mode 100644
index 00000000..e58198a2
--- /dev/null
+++ b/source/solutions/codingbat/String-2/count_code.py
@@ -0,0 +1,20 @@
+def count_code(str):
+    count = 0
+    for i in range(len(str) - 3):
+        if str[i:i + 2] == "co" and str[i + 3] == "e":
+            count += 1
+    return count
+
+
+assert count_code('aaacodebbb') == 1
+assert count_code('codexxcode') == 2
+assert count_code('cozexxcope') == 2
+assert count_code('cozexxcope') == 2
+assert count_code('cozfxxcope') == 1
+assert count_code('xxcozeyycop') == 1
+assert count_code('cozcop') == 0
+
+
+
+print("all checks passed")
+
From 03a159b695dba284eadd9742f9c5d92915bd5836 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Tue, 16 Apr 2019 17:45:05 -0700
Subject: [PATCH 68/87] updated packaging page a bit
---
 source/modules/Packaging.rst                  | 34 +++++++++++--------
 .../installing/python_for_windows.rst         |  2 +-
 2 files changed, 20 insertions(+), 16 deletions(-)
diff --git a/source/modules/Packaging.rst b/source/modules/Packaging.rst
index 522ddbf5..492beb9b 100644
--- a/source/modules/Packaging.rst
+++ b/source/modules/Packaging.rst
@@ -37,25 +37,25 @@ of python files or other package directories::
        module_b.py
 
 The ``__init__.py`` can be totally empty -- or it can have arbitrary python code in it.
-The code will be run when the package is imported -- just like a module,
+The code will be run when the package is imported -- just like a module.
 
-modules inside packages are *not* automatically imported. So, with the above structure::
+Modules inside packages are *not* automatically imported. So, with the above structure::
 
   import a_package
 
-will run the code in ``a_package/__init__.py``. Any names defined in the
-``__init__.py`` will be available in::
+will run the code in ``a_package/__init__.py``. Any names defined in the ``__init__.py`` will be available in::
 
   a_package.a_name
 
-but::
+But::
 
  a_package.module_a
 
-will not exist. To get submodules, you need to explicitly import them:
+will not exist. To get submodules, you need to explicitly import them like so:
 
   import a_package.module_a
 
+
 More on Importing
 -----------------
 
@@ -77,11 +77,11 @@ or a few names from a package::
                          x,
                          y)
 
-And you can rename stuff as you import it::
+You also can optionally rename stuff as you import it::
 
   import numpy as np
 
-This is a common pattern for using large packages and not having to type a lot...
+This is a common pattern for using large packages (maybe with long names) and not having to type a lot.
 
 
 ``import *``
@@ -91,16 +91,16 @@ This is a common pattern for using large packages and not having to type a lot..
 
   from something import *
 
-means: "import all the names in the module"
+Means: "import all the names in the module, "something".
 
-You really don't want to do that! It is an old pattern that is now an anti-pattern
+You really don't want to do that! It is an old pattern that is now an anti-pattern.
 
 But if you do encounter it, it doesn't actually import all the names -- it imports the ones defined in the module's ``__all__`` variable.
 
 ``__all__`` is a list of names that you want ``import *`` to import.
-So the module author can control it, and not expect all sorts of builtins and other modules.
+So the module author can control it, and not accidentally override builtins or bring a lot of extraneous names into your namespace.
 
-But really -- don't use it!
+But really -- **don't use ``import *``**
 
 
 Relative imports
@@ -149,7 +149,7 @@ Similarly to \*nix shells:
 
 ".." means "the package above this one"
 
-Note that you have to use the "from" form when using relative imports.
+Note that you have to use the ``from`` form of import when using relative imports.
 
 **Caveats:**
 
@@ -185,8 +185,13 @@ There is debate about which is the "one way to do it" -- a bit unpythonic, but y
 sys.modules
 -----------
 
+``sys.modules`` is simply a dictionary that stores all teh already imported modules.
+The keys are the module names, and the values are the module objects themselves:
+
 .. code-block:: ipython
 
+  In [3]: import sys
+
   In [4]: type(sys.modules)
   Out[4]: dict
 
@@ -231,8 +236,7 @@ So, more or less, when you import a module, the interpreter:
 
 * Looks to see if the module is already in ``sys.modules``.
 
-* If it is, it binds a name to the existing module in the current
-  module's namespace.
+* If it is, it binds a name to the existing module in the current module's namespace.
 
 * If it isn't:
 
diff --git a/source/supplemental/installing/python_for_windows.rst b/source/supplemental/installing/python_for_windows.rst
index e2579c2e..1dca6fd8 100644
--- a/source/supplemental/installing/python_for_windows.rst
+++ b/source/supplemental/installing/python_for_windows.rst
@@ -187,7 +187,7 @@ All available from the command line.
 
 To try it out, you should be able to run all of these commands, and get something like the following results:
 
-(recall that you can get out of the python or iPython command lines with ``ctrl+Z`` (ifthat doesn't work, try ``ctrl+D`` -- the *nix version))
+(recall that you can get out of the python or iPython command lines with ``ctrl+Z`` (if that doesn't work, try ``ctrl+D`` -- the \*nix version))
 
 For Python:
 
From e1f1e7693dcd1fb7d708d5b1d760ff35689ba1e5 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sat, 20 Apr 2019 14:40:06 -0700
Subject: [PATCH 69/87] updated series template
---
 source/exercises/series_template.py           | 17 ++++-
 .../solutions/Lesson02/series_non_recusive.py | 73 +++++++++++++++++++
 2 files changed, 89 insertions(+), 1 deletion(-)
 create mode 100644 source/solutions/Lesson02/series_non_recusive.py
diff --git a/source/exercises/series_template.py b/source/exercises/series_template.py
index 0a87f509..fb161d70 100644
--- a/source/exercises/series_template.py
+++ b/source/exercises/series_template.py
@@ -21,11 +21,16 @@ def sum_series(n, n0=0, n1=1):
 
     :param n0=0: value of zeroth element in the series
     :param n1=1: value of first element in the series
-    
+
     This function should generalize the fibonacci() and the lucas(),
     so that this function works for any first two numbers for a sum series.
     Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n).
     And sum_series(n, 2, 1) should be equivalent to lucas(n).
+
+    sum_series(n, 3, 2) should generate antoehr series with no specific name
+
+    The defaults are set to 0, 1, so if you don't pass in any values, you'll
+    get the fibonacci sercies
     """
     pass
 
@@ -45,9 +50,19 @@ def sum_series(n, n0=0, n1=1):
 
     assert lucas(4) == 7
 
+    # test that sum_series matches fibonacci
     assert sum_series(5) == fibonacci(5)
+    assert sum_series(7, 0, 1) == fibonacci(7)
 
     # test if sum_series matched lucas
     assert sum_series(5, 2, 1) == lucas(5)
 
+    # test if sum_series works for arbitrary initial values
+    assert sum_series(0, 3, 2) == 3
+    assert sum_series(1, 3, 2) == 2
+    assert sum_series(2, 3, 2) == 5
+    assert sum_series(3, 3, 2) == 7
+    assert sum_series(4, 3, 2) == 12
+    assert sum_series(5, 3, 2) == 19
+
     print("tests passed")
diff --git a/source/solutions/Lesson02/series_non_recusive.py b/source/solutions/Lesson02/series_non_recusive.py
new file mode 100644
index 00000000..2ac4e070
--- /dev/null
+++ b/source/solutions/Lesson02/series_non_recusive.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+
+"""
+a template for the series assignment
+"""
+
+
+def fibonacci(n):
+    """ compute the nth Fibonacci number """
+    a, b = 0, 1
+    if n == 0:
+        return a
+    for _ in range(n - 1):
+        a, b = b, a + b
+    return b
+
+
+def lucas(n):
+    """ compute the nth Lucas number """
+    pass
+
+
+def sum_series(n, n0=0, n1=1):
+    """
+    compute the nth value of a summation series.
+
+    :param n0=0: value of zeroth element in the series
+    :param n1=1: value of first element in the series
+
+    This function should generalize the fibonacci() and the lucas(),
+    so that this function works for any first two numbers for a sum series.
+    Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n).
+    And sum_series(n, 2, 1) should be equivalent to lucas(n).
+
+    sum_series(n, 3, 2) should generate antoehr series with no specific name
+
+    The defaults are set to 0, 1, so if you don't pass in any values, you'll
+    get the fibonacci sercies
+    """
+    pass
+
+if __name__ == "__main__":
+    # run some tests
+    assert fibonacci(0) == 0
+    assert fibonacci(1) == 1
+    assert fibonacci(2) == 1
+    assert fibonacci(3) == 2
+    assert fibonacci(4) == 3
+    assert fibonacci(5) == 5
+    assert fibonacci(6) == 8
+    assert fibonacci(7) == 13
+
+    assert lucas(0) == 2
+    assert lucas(1) == 1
+
+    assert lucas(4) == 7
+
+    # test that sum_series matches fibonacci
+    assert sum_series(5) == fibonacci(5)
+    assert sum_series(7, 0, 1) == fibonacci(7)
+
+    # test if sum_series matched lucas
+    assert sum_series(5, 2, 1) == lucas(5)
+
+    # test if sum_series works for arbitrary initial values
+    assert sum_series(0, 3, 2) == 3
+    assert sum_series(1, 3, 2) == 2
+    assert sum_series(2, 3, 2) == 5
+    assert sum_series(3, 3, 2) == 7
+    assert sum_series(4, 3, 2) == 12
+    assert sum_series(5, 3, 2) == 19
+
+    print("tests passed")
From c9c3ebff08271fef1f12ac4e1d741576104abf05 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sat, 20 Apr 2019 14:42:09 -0700
Subject: [PATCH 70/87] added a non-recusive series solution
---
 source/solutions/Lesson02/series.py           | 42 ++++++++++++++++---
 .../solutions/Lesson02/series_non_recusive.py | 16 +++++--
 2 files changed, 50 insertions(+), 8 deletions(-)
diff --git a/source/solutions/Lesson02/series.py b/source/solutions/Lesson02/series.py
index 082e4c1e..3bf35665 100755
--- a/source/solutions/Lesson02/series.py
+++ b/source/solutions/Lesson02/series.py
@@ -3,14 +3,25 @@
 """
 series.py
 
-solutions to the Fibonacci Series and Lucas numbers
+solutions to the Fibonacci Series, Lucas numbers and
+generalized sum_series
+
+These solutions use recursion
+-- calling a funciton from within that function.
+
+These series are defined "recusively", so it's a
+really natural way to express the solution. However,
+recursion can be substantially less efficient.n
+
+See series_non_recursive.py for a more efficient way
+to do it.
 """
 
 
 def fibonacci(n):
     """ compute the nth Fibonacci number """
 
-    if n < 0:
+    if n < 0:  # check for negative number -- just in case.
         return None
     elif n == 0:
         return 0
@@ -35,15 +46,22 @@ def lucas(n):
 
 def sum_series(n, n0=0, n1=1):
     """
-    compute the nth value of a summation series.
+    Compute the nth value of a summation series.
 
     :param n0=0: value of zeroth element in the series
     :param n1=1: value of first element in the series
 
-    if n0 == 0 and n1 == 1, the result is the Fibbonacci series
+    This function should generalize the fibonacci() and the lucas(),
+    so that this function works for any first two numbers for a sum series.
+    Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n).
+    And sum_series(n, 2, 1) should be equivalent to lucas(n).
+
+    sum_series(n, 3, 2) should generate antoehr series with no specific name
 
-    if n0 == 2 and n1 == 1, the result is the Lucas series
+    The defaults are set to 0, 1, so if you don't pass in any values, you'll
+    get the fibonacci sercies
     """
+
     if n < 0:
         return None
     if n == 0:
@@ -55,6 +73,7 @@ def sum_series(n, n0=0, n1=1):
 
 # Can you re-define fibonacci and lucas by using sum_series?
 
+
 if __name__ == "__main__":
     # run some tests
 
@@ -94,4 +113,17 @@ def sum_series(n, n0=0, n1=1):
     for n in range(0, 10):
         assert sum_series(n, 2, 1) == lucas(n)
 
+    # test if sum_series works for negative value
+    # (it should return None)
+
+    assert sum_series(-1, 3, 2) is None
+
+    # test if sum_series works for arbitrary initial values
+    assert sum_series(0, 3, 2) == 3
+    assert sum_series(1, 3, 2) == 2
+    assert sum_series(2, 3, 2) == 5
+    assert sum_series(3, 3, 2) == 7
+    assert sum_series(4, 3, 2) == 12
+    assert sum_series(5, 3, 2) == 19
+
     print("tests passed")
diff --git a/source/solutions/Lesson02/series_non_recusive.py b/source/solutions/Lesson02/series_non_recusive.py
index 2ac4e070..b6925b92 100644
--- a/source/solutions/Lesson02/series_non_recusive.py
+++ b/source/solutions/Lesson02/series_non_recusive.py
@@ -4,7 +4,6 @@
 a template for the series assignment
 """
 
-
 def fibonacci(n):
     """ compute the nth Fibonacci number """
     a, b = 0, 1
@@ -17,7 +16,12 @@ def fibonacci(n):
 
 def lucas(n):
     """ compute the nth Lucas number """
-    pass
+    a, b = 2, 1  # notice that all I had to change from fib were these values?
+    if n == 0:
+        return a
+    for _ in range(n - 1):
+        a, b = b, a + b
+    return b
 
 
 def sum_series(n, n0=0, n1=1):
@@ -37,7 +41,13 @@ def sum_series(n, n0=0, n1=1):
     The defaults are set to 0, 1, so if you don't pass in any values, you'll
     get the fibonacci sercies
     """
-    pass
+    a, b = n0, n1  # notice that all I had to change from fib were these values?
+    if n == 0:
+        return a
+    for _ in range(n - 1):
+        a, b = b, a + b
+    return b
+
 
 if __name__ == "__main__":
     # run some tests
From 46c8594bcfda893f584fefec8472c1d8ced72abe Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sat, 20 Apr 2019 15:28:44 -0700
Subject: [PATCH 71/87] clarifications on the sum_series assignment
---
 source/exercises/fib_and_lucas.rst      | 62 ++++++++++++++-----------
 source/solutions/Lesson02/print_grid.py |  3 ++
 2 files changed, 39 insertions(+), 26 deletions(-)
diff --git a/source/exercises/fib_and_lucas.rst b/source/exercises/fib_and_lucas.rst
index ba0e5c21..b0243936 100644
--- a/source/exercises/fib_and_lucas.rst
+++ b/source/exercises/fib_and_lucas.rst
@@ -12,12 +12,15 @@ Goal:
 
 The `Fibonacci Series`_ is a numeric series starting with the integers 0 and 1.
 
-In this series, the next integer is determined by summing the previous two.
+In this series, the next integer is determined by summing the previous two
+
 
 This gives us::
 
     0, 1, 1, 2, 3, 5, 8, 13, ...
 
+.. note: 0+1 is 1; 1+1 is 2; 1+2 is 3; 2+3 is 5; 3+5 is 8; and so on forever...
+
 We will write a function that computes this series -- then generalize it.
 
 .. _Fibonacci Series: http://en.wikipedia.org/wiki/Fibbonaci_Series
@@ -31,12 +34,11 @@ Step 1
 
   - The function should have one parameter ``n``.
 
-  - The function should return the ``nth`` value in the fibonacci series
-    (starting with zero index).
+  - The function should return the ``nth`` value in the fibonacci series (starting with zero index).
 
 * Ensure that your function has a well-formed ``docstring``
 
-Note that the fibinacci series is naturally recursive -- the value is
+Note that the fibonacci series is naturally recursive -- the value is
 defined by previous values:
 
 fib(n) = fib(n-2) + fib(n-1)
@@ -58,50 +60,58 @@ In your ``series.py`` module, add a new function ``lucas`` that returns the
 
 Ensure that your function has a well-formed ``docstring``
 
+YOu should find it's *very* similar to the ``fibonacci()`` function.
+
 Generalizing
 ------------
 
-Both the *fibonacci series* and the *lucas numbers* are based on an identical
-formula.
+Both the *fibonacci series* and the *lucas numbers* are based on an identical formula:
+
+fib(n) = fib(n-2) + fib(n-1)
+
+That's why the code is so similar.
+
+This formula creates a class of series that are all related -- each with a different two starting numbers.
+
+Add a third function called ``sum_series`` that can compute all of these related series.
+
+It should have one required parameter and two optional parameters.
+The required parameter will determine which element in the
+series to print.
+The two optional parameters will have default values of 0 and 1 and will determine the first two values for the series to be produced.
+
+Calling this function with no optional parameters will produce numbers from the *fibonacci series* (because 0 and 1 are the defaults).
 
-Add a third function called ``sum_series`` with one required parameter and two
-optional parameters. The required parameter will determine which element in the
-series to print. The two optional parameters will have default values of 0 and
-1 and will determine the first two values for the series to be produced.
+Calling it with the optional arguments 2 and 1 will
+produce values from the *lucas numbers*.
 
-Calling this function with no optional parameters will produce numbers from the
-*fibonacci series*.  Calling it with the optional arguments 2 and 1 will
-produce values from the *lucas numbers*. Other values for the optional
-parameters will produce other series.
+Other values for the optional parameters will produce other series.
 
 **Note:** While you *could* check the input arguments, and then call one
 of the functions you wrote, the idea of this exercise is to make a general
-function, rather than one specialized. So you should reimplement the code
+function, rather than one specialized. So you should re-implement the code
 in this function.
 
-In fact, you could go back and reimplement your fibonacci and lucas
-functions to call this one with particular arguments.
+In fact, you could go back and re-implement your fibonacci and lucas
+functions to call ``sum-series`` with particular arguments.
 
 Ensure that your function has a well-formed ``docstring``
 
 Tests...
 --------
 
-Add a block of code to the end of your ``series.py``
-module. Use the block to write a series of ``assert`` statements that
+Add a block of code to the end of your ``series.py`` module.
+Use the block to write a series of ``assert`` statements that
 demonstrate that your three functions work properly.
 
 Use comments in this block to inform the observer what your tests do.
 
-We have created a template for you to use, to clarify what we mean by these
-tests:
+We have created a template for you to use to clarify what we mean by these asserts:
 
 :download:`series_template.py <../exercises/series_template.py>`
 
-Add your new module to your git clone and commit frequently while working on
-your implementation. Include good commit messages that explain concisely both
-*what* you are doing and *why*.
+Add your new module to your personal git repo and commit frequently while working on your implementation.
+Include good commit messages that explain concisely both *what* you are doing and *why*.
 
-When you are finished, push your changes to your fork of the class repository
-in GitHub and make a pull request.
+When you are finished, push your changes to your fork of the class repository in GitHub and make a pull request.
 
diff --git a/source/solutions/Lesson02/print_grid.py b/source/solutions/Lesson02/print_grid.py
index a270eaf7..651b3bf8 100755
--- a/source/solutions/Lesson02/print_grid.py
+++ b/source/solutions/Lesson02/print_grid.py
@@ -14,6 +14,9 @@
 def print_grid_trivial():
     """
     Did anyone come up with the most trivial possible solution?
+
+    Note that this may the the wy to go, oif this all you need to do:
+      print something specific once. Why write fancy code for that?
     """
     print("""
 + - - - - + - - - - +
From d227300b5bc743fc51c281f0ec923cc0f861800a Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sat, 20 Apr 2019 17:25:45 -0700
Subject: [PATCH 72/87] updated string notes
---
 source/modules/Strings.rst | 165 ++++++++++++++++++++++++++++---------
 source/modules/Unicode.rst |  86 +++++++++++++------
 2 files changed, 183 insertions(+), 68 deletions(-)
diff --git a/source/modules/Strings.rst b/source/modules/Strings.rst
index d476e186..ac712fea 100644
--- a/source/modules/Strings.rst
+++ b/source/modules/Strings.rst
@@ -9,7 +9,26 @@ Strings
 Strings
 =======
 
-A "String" is a computerese word for a piece of text -- a "string" of characters.
+.. admonition:: Joke
+
+  A piece of string is new in town, and looking for a drink. He sees a local bar, walks in, sits down, and orders a beer. The bartender looks at him askance, and says: "wait a minute, are you a piece of string?". "Why yes", the string replies.  The bartender growls back: "We don't serve your kind in here -- you get out!".
+
+  Disappointed, the piece of string leaves and heads down the street to the next bar, but this time, he barely gets to a seat before being yelled at -- "we don't want any string in here -- you get out!"
+
+  A third bar, and he has a similar encounter. Now he's getting pretty distraught and confused -- "what have they got against string in this town?", he asks himself. But he's also tired and thirsty, so he gets an idea.
+
+  The piece of string twists himself all up, winding himself around and around. Then he reaches up and fluffs up the top of his head.
+
+  Thus prepared, he heads into yet another bar. This time is different. He walks in, sits down, the bartender takes his order -- all good. But then just as the bartender is putting his beer down he stops, and looks hard at him: "wait a minute! you're a piece of string, aren't you?".
+
+  Full of confidence, the string replies: "Nope, I'm a frayed knot."
+
+A "String" is a computerese word for a piece of text -- a "string" of characters. Why "string"? "String" can be used to mean "(a linear sequence (as of characters, words, proteins, etc.)"
+(`definition of string `_)
+
+So a string is a sequence of individual letters or characters.
+
+In Python, each character can be a `Unicode `_ character -- that is, any character in any language in the world.  Having this built in by default in Python (3) means that you can get very far simply ignoring it -- anything you can type on your computer can be used in strings in Python. If you do need to work with non-English characters, or data encoded in non-utf-8, particularly on Python 2, here are some notes about that: :ref:`unicode`. But for the most part, in Python3 -- strings are text, and text is string, and that's that.
 
 Creating strings:
 -----------------
@@ -18,6 +37,8 @@ A string literal creates a string type.
 
 (we've seen this already...)
 
+A literal can be delineated with single or double quotes, alone or in triples.
+
 ::
 
     "this is a string"
@@ -26,45 +47,52 @@ A string literal creates a string type.
 
     """and this also"""
 
-    '''and even this'''
+    '''and even this
+    triple quotes preserve newlines
+    so this is three lines'''
 
-You can also use ``str()``
+You can also use call the string object (``str()``) to "make" a string out of other data types.
 
 .. code-block:: ipython
 
     In [256]: str(34)
     Out[256]: '34'
 
-And strings can be read from files or other sources of I/O.
+Strings can also be read from files or other sources of I/O.
 
 String Methods
 ===============
 
-String objects have a lot of methods.
+The python string object is very powerful with lots of methods for common text manipulation. "methods" are functions defined on an object itself (more on that when we get to OO programming). But it means that you have many ways to manipulate text built right into the string objects themselves.
 
-Here are just a few:
+Note that strings are "immutable" --they can not be changed once they have been created. So the string methods all return new objects, rather than change the string in place.
+
+Here are just a few of the more common string methods:
 
 Splitting and Joining Strings
 -----------------------------
 
-``split`` and ``join``:
+``split`` and ``join`` can be used to break up a string into pieces, or make one big string out of multiple smaller pieces:
 
 .. code-block:: ipython
 
-    In [167]: csv = "comma, separated, values"
-    In [168]: csv.split(', ')
+    In [167]: csv = "comma,separated,values"
+
+    In [168]: csv.split(',')
     Out[168]: ['comma', 'separated', 'values']
-    In [169]: psv = '|'.join(csv.split(', '))
+
+    In [169]: psv = '|'.join(csv.split(','))
+
     In [170]: psv
     Out[170]: 'comma|separated|values'
 
-It may seem odd at first that ``.join()`` is a string method, rather than, say, a method on lists. But, in fact it makes a lot of sense. Lists (and tuples, and other sequences) can hold any type of data -- and "joining" arbitrary data types doesn't make any sense.  Joining is strictly a string activity.
+It may seem odd at first that ``.join()`` is a string method, rather than, say, a method on lists. But in fact, it makes a lot of sense. Lists (and tuples, and other sequences) can hold any type of data -- and "joining" arbitrary data types doesn't make any sense.  Joining is strictly a string activity.
 
 And you need a string so you can join the parts -- therefore, we need a string object in there somewhere anyway.
 
-Lastly, having join() be a string method means that it can join strings in ANY iterable object -- not just the built-in sequence types.
+Lastly, having join() be a string method means that it can join strings in ANY iterable object -- not just lists or other built-in sequence types.
 
-So it does make sense -- but even if not, that's the way it is.
+So it does make sense -- but even if doesn't make sense to you, that's the way it is -- so remember that you call ``.join()`` on the string you want to join things with.
 
 So to be clear: if you have a bunch of strings in a sequence and you want to put them together, you create a string with the character (or characters) you want to join them with, and call join() on that object:
 
@@ -80,10 +108,17 @@ So to be clear: if you have a bunch of strings in a sequence and you want to put
     In [23]: "".join(["these", "are", "some", "strings"])
     Out[23]: 'thesearesomestrings'
 
-Building up a long string.
+Maybe not very common, but you can join with a longer string as well:
+
+.. code-block:: ipython
+
+    In [5]: " --#-- ".join(["these", "are", "some", "strings"])
+    Out[5]: 'these --#-- are --#-- some --#-- strings'
+
+Building up a Long String.
 --------------------------
 
-The obvious thing to do is something like:
+An obvious thing to do is something like:
 
 .. code-block:: python
 
@@ -91,7 +126,7 @@ The obvious thing to do is something like:
   for piece in list_of_stuff:
       msg += piece
 
-But: strings are immutable -- Python needs to create a new string each time you add a piece -- not efficient:
+But: strings are immutable -- Python needs to create a new string each time you add a piece, which is not very efficient.  So it's better to gather all the pieces together in a list, and then join them together:
 
 .. code-block:: python
 
@@ -100,8 +135,7 @@ But: strings are immutable -- Python needs to create a new string each time you
        msg.append(piece)
    " ".join(msg)
 
-appending to lists is efficient -- and so is the join() method of strings.
-
+appending to lists is efficient -- and so is the ``join()`` method of strings.
 
 Case Switching
 --------------
@@ -109,29 +143,38 @@ Case Switching
 .. code-block:: ipython
 
     In [171]: sample = 'A long string of words'
+
     In [172]: sample.upper()
     Out[172]: 'A LONG STRING OF WORDS'
+
     In [173]: sample.lower()
     Out[173]: 'a long string of words'
+
     In [174]: sample.swapcase()
     Out[174]: 'a LONG STRING OF WORDS'
+
     In [175]: sample.title()
     Out[175]: 'A Long String Of Words'
 
 
-Testing
---------
+Testing for certain classes of characters
+-----------------------------------------
 
 .. code-block:: ipython
 
     In [181]: number = "12345"
+
     In [182]: number.isnumeric()
     Out[182]: True
+
     In [183]: number.isalnum()
     Out[183]: True
+
     In [184]: number.isalpha()
     Out[184]: False
+
     In [185]: fancy = "Th!$ $tr!ng h@$ $ymb0l$"
+
     In [186]: fancy.isalnum()
     Out[186]: False
 
@@ -139,6 +182,10 @@ Testing
 String Literals
 -----------------
 
+Sometimes when you are creating a string, you want to put an non-normal character in there --one that isn't strictly a letter or symbol, such as newlines, etc.
+
+To do that, python support a set of "escape" sequences -- when a character follows a backslash, it gets interpreted as having a particular meaning.
+
 Common Escape Sequences::
 
     \\  Backslash (\)
@@ -149,8 +196,10 @@ Common Escape Sequences::
     \t  ASCII Horizontal Tab (TAB)
     \ooo  Character with octal value ooo
     \xhh  Character with hex value hh
+    \uxxxx Charactor with Unicode code point value xxxx
+    \N{char-name} Charactor with Unicdoe name char_name
 
-for example -- for tab-separated values:
+For example -- for tab-separated values:
 
 .. code-block:: ipython
 
@@ -160,11 +209,14 @@ for example -- for tab-separated values:
     these   are separated   by  tabs
 
 https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
+
 https://docs.python.org/3/library/stdtypes.html#string-methods
 
 Raw Strings
 ------------
 
+There are times when you want a literal backslash in your string: Windows file paths, regular expressions.  Tomake this easy, Pyhton support "raw" strings -- string literals where the backslash does not have special meaning:
+
 Add an ``r`` in front of the string literal:
 
 **Escape Sequences Ignored**
@@ -174,6 +226,7 @@ Add an ``r`` in front of the string literal:
     In [408]: print("this\nthat")
     this
     that
+
     In [409]: print(r"this\nthat")
     this\nthat
 
@@ -184,10 +237,33 @@ Add an ``r`` in front of the string literal:
     In [415]: r"\"
     SyntaxError: EOL while scanning string literal
 
-putting a backslash right before the end quote confuses the interpreter!
+Putting a backslash right before the end quote confuses the interpreter!
+
+Raw strings can be very handy for things like regular expressions that need embedded backslashes.
+
+Building Long String Literals
+-----------------------------
+
+If you put two string literals next to each other in the code, Python will join them into one when compiling:
 
-This can be very handy for things like regular expressions that need embedded backslashes.
+.. code-block:: ipython
 
+    In [6]: "this" "that"
+    Out[6]: 'thisthat'
+
+(note: no comma in between!)
+THis may not look useful, but when combined with the fact that Python joins together lines when inside a parentheses, it can be a nice way to make larger string literals:
+
+.. code-block:: ipython
+
+    In [7]: print("This is the first line\n"
+       ...:       "And here is another line\n"
+       ...:       "If I don't put in a newline"
+       ...:       "I can get an very long line in, without making the"
+       ...:       "line of code too long.")
+    This is the first line
+    And here is another line
+    If I don't put in a newlineI can get an very long line in, without making the line of code too long.
 
 Ordinal values
 --------------
@@ -196,27 +272,33 @@ Characters in strings are stored as numeric values:
 
 * "ASCII" values: 1-127
 
-* Unicode values -- 1 - 1,114,111 (!!!)
+* Unicode "code points" -- 1 - 1,114,111 (!!!)
 
-Unicode supports a LOT of characters --every character in every language known to man -- and then some :-)
+Unicode supports a LOT of characters -- every character in every language known to man -- and then some :-). The Unicode code poitns for the characters in the ASCII character set are the same as ASCII -- so handy for us English speakers.
 
-To get the value:
+To get the value, use ``ord()``:
 
 .. code-block:: ipython
 
     In [109]: for i in 'Chris':
        .....:     print(ord(i), end=' ')
     67 104 114 105 115
+
+To get the character from the code point, use ``chr()``:
+
+.. code-block:: ipython
+
     In [110]: for i in (67,104,114,105,115):
        .....:     print(chr(i), end='')
     Chris
 
-For the English language, stick with ASCII, otherwise use full Unicode: it's easy with Python3 -- more on that in a later lesson.
-
+For the English language, stick with ASCII, otherwise use, full Unicode: it's easy with Python3
 
-Building Strings from data
+Building Strings from Data
 --------------------------
 
+We often have some data in Python variables -- maybe strings, maybe numbers -- and we often want to combine that data with text to make a custom message of some sort.
+
 You could, but please don't(!), do this:
 
 .. code-block:: python
@@ -225,11 +307,12 @@ You could, but please don't(!), do this:
 
 (I know -- we did that in the grid_printing exercise)
 
-Do this instead:
+Why not? It's slow and not very flexible.  Python provides a few ways to "format" text, so you can do this instead:
 
-.. code-block:: python
+.. code-block:: ipython
 
-    'Hello {}!'.format(name)
+    In [11]: 'Hello {}!'.format(name)
+    Out[11]: 'Hello Chris!'
 
 It's much faster and safer, and easier to modify as code gets complicated.
 
@@ -249,12 +332,12 @@ This is very similar to C-style string formatting (`sprintf`).
 
 It's still around, and handy --- but ...
 
-The "new" ``format()`` method is more powerful and flexible, so we'll focus on that in this class.
+The "new" ``format()`` method is more powerful and flexible, so we'll focus on that in this class.  And there is now the newer "f-strings" (see below) which provide a lot of that "quick and dirty" convenience, while using the same formatting codes as ``.format()``
 
 String Formatting
 -----------------
 
-The string ``format()`` method:
+The string ``.format()`` method:
 
 .. code-block:: ipython
 
@@ -292,7 +375,7 @@ The counts must agree:
     IndexError: tuple index out of range
 
 
-Named placeholders:
+Named Placeholders:
 -------------------
 
 .. code-block:: ipython
@@ -320,7 +403,7 @@ The format operator works with string variables, too:
     In [82]: s.format(a, b, a/b)
     Out[82]: '12 / 3 = 4.000000'
 
-So you can dynamically build a format string, and then use it in multiple places in the code.
+So you can save a format string, or even built it up dynamically, and then use it in multiple places in the code.
 
 
 Complex Formatting
@@ -390,13 +473,13 @@ For this most simple example::
 
   f"some text: {expression}"
 
-`expression` is any valid python expression(remember that an expression is a combination of values and operators and names that produces a value).
+`expression` is any valid python expression (remember that an expression is a combination of values and operators and names that produces a value).
 
 The expression is evaluated, and then, if it is not a string, it is converted to one, so it's really::
 
   f"some text: {str(expression)}"
 
-Let's see how that works in practice:
+Let's see how this works in practice:
 
 .. code-block:: ipython
 
@@ -439,14 +522,14 @@ And it has to be an expression, not a statement -- so you can't put a for loop o
 
 You can see how this can be a very powerful and quick way to get things done.
 
-f-string use
+F-string Use
 ------------
 
-F-strings are a very new Python feature. They will cause a syntax error in any Python version older than 3.6 -- and 3.6 was first released on December 23, 2016 -- less than a year from this writing.
+F-strings are a fairly new Python feature. They will cause a syntax error in any Python version older than 3.6 -- 3.6 was first released on December 23, 2016 -- only a couple years from this writing.
 
 So there is not much out there in the wild, and I have yet to see it in production code.
 
-They are not currently used in any of the examples in this course.
+They are not currently used in many of the examples in this course.
 
 Nevertheless, they are a nifty feature that could be very useful, so feel free to use them where it makes you code cleaner and clearer.
 
diff --git a/source/modules/Unicode.rst b/source/modules/Unicode.rst
index e2c0f49f..9f2ca514 100644
--- a/source/modules/Unicode.rst
+++ b/source/modules/Unicode.rst
@@ -36,11 +36,11 @@ What the heck is Unicode anyway?
 
   * MacRoman, Windows 1252, etc...
 
-  * There is now "latin-1", but still a lot of old files around.
+  * There is now "latin-1", a 1-byte encoding suitable for European languages -- but still a lot of old files around that use the old ones.
 
 * Non-Western European languages required totally incompatible 1-byte encodings
 
-* No way to mix languages with different alphabets.
+* This means there was no way to mix languages with different alphabets in the same document (web page, etc.)
 
 
 Enter Unicode
@@ -52,14 +52,15 @@ The Unicode idea is pretty simple:
 But how do you express that in bytes?
   * Early days: we can fit all the code points in a two byte integer (65536 characters)
 
-  * Turns out that didn't work -- now need 32 bit integer to hold all of unicode "raw" (UTC-4)
+  * Turns out that didn't work -- 65536 is not enough for all languages. So we now need 32 bit integer to hold all of Unicode "raw" (UTC-4).
+  * But it's a waste of space to use 4 full bytes for each character, when so many don't require that much space.
 
 Enter "encodings":
   * An encoding is a way to map specific bytes to a code point.
 
   * Each code point can be represented by one or more bytes.
 
-  * Each encoding is different -- if you don't know the encoding, you don't know how to interpret the bytes! (though maybe you can guess...)
+  * Each encoding is different -- if you don't know the encoding, you don't know how to interpret the bytes! (though maybe you can guess)
 
 
 Unicode
@@ -79,7 +80,7 @@ http://www.joelonsoftware.com/articles/Unicode.html
 
 * Python provides some abstractions to make it easier to deal with bytes
 
-**Unicode is a biggie**
+**Unicode is a Biggie**
 
 Actually, dealing with numbers rather than bytes is big
 
@@ -92,12 +93,12 @@ Mechanics
 What are strings?
 -----------------
 
-Py2 strings were simply sequences of bytes.  When text was one per charater that worked fine.
+Py2 strings were simply sequences of bytes.  When text was one per character that worked fine.
 
-Py3 strings (or Unicode strings in py2) are sequences of platonic characters.
+Py3 strings (or Unicode strings in py2) are sequences of "platonic characters".
 
 It's almost one code point per character -- there are complications
-with combined characters: accents, etc -- but we can mostly ignore those.
+with combined characters: accents, etc -- but we can mostly ignore those -- you will get far thiking of a code point as a character.
 
 Platonic characters cannot be written to disk or network!
 
@@ -153,12 +154,12 @@ If you need to deal with the actual bytes for some reason, you may need to conve
 
 And can get even more confusing with py2 strings being *both* text and bytes!
 
-This is actually one of teh biggest differences between Python 2 and Python 3. As an ordinary user (particulary one that used English...), you may not notice -- text is text, and things generally "just work", but under the hood it is very different, and folks writting libraries for things like internet protocols struggle with the differences.
+This is actually one of the biggest differences between Python 2 and Python 3. As an ordinary user (particularly one that used English...), you may not notice -- text is text, and things generally "just work", but under the hood it is very different, and folks writing libraries for things like Internet protocols struggle with the differences.
 
-Using unicode in Py2
+Using Unicode in Py2
 ---------------------
 
-IF you do need to write Python2 code, you really should use Unicode.
+If you do need to write Python2 code, you really should use Unicode.
 
 Here are the basics:
 
@@ -215,23 +216,24 @@ Encoding and Decoding
 Unicode Literals
 ------------------
 
-1) Use unicode in your source files:
+1) Use Unicode in your source files:
 
 .. code-block:: python
 
     # -*- coding: utf-8 -*-
 
-2) Escape the unicode characters:
+(This is only required on Py2 -- the UTF-8 encoding is default for Python 3)
+
+2) Escape the Unicode characters:
 
 .. code-block:: python
 
   print u"The integral sign: \u222B"
   print u"The integral sign: \N{integral}"
 
-Lots of tables of code points online:
+Lots of tables of code points are available online:
 
-One example:
-  http://inamidst.com/stuff/unidata/
+One example:  http://inamidst.com/stuff/unidata/
 
 :download:`hello_unicode.py  <../examples/unicode/hello_unicode.py>`.
 
@@ -258,10 +260,10 @@ Python has a default encoding (usually ascii)
 
 The default encoding will get used in unexpected places!
 
-Using Unicode everywhere
+Using Unicode Everywhere
 -------------------------
 
-Python 2.6 and above have a nice feature to make it easier to use unicode everywhere
+Python 2.6 and above have a nice feature to make it easier to use Unicode everywhere
 
 .. code-block:: python
 
@@ -280,9 +282,9 @@ After running that line, the ``u''`` is assumed
     In [5]: type(s)
     Out[5]: unicode
 
-NOTE: You can still get py2 strings from other sources!
+NOTE: You can still get py2 strings from other sources! So you still need to think about ``str`` vs ``unicdode``
 
-This is a really good idea if you want to write code compatible with Python2 and 3
+This is a really good idea if you want to write code compatible with Python2 and 3.
 
 Encodings
 ----------
@@ -310,11 +312,11 @@ Probably the one you'll use most -- most common in Internet protocols (xml, JSON
 
 Nice properties:
 
-* ASCII compatible: First 127 characters are the same
+* ASCII compatible: First 127 characters are the same as ASCII
 
 * Any ascii string is a utf-8 string
 
-* Compact for mostly-english text.
+* Compact for mostly-English text.
 
 Gotchas:
 
@@ -327,7 +329,7 @@ UTF-16
 
 Kind of like UTF-8, except it uses at least 16bits (2 bytes) for each character: NOT ASCII compatible.
 
-But is still needs more than two bytes for some code points, so you still can't process it as one per character.
+But is still needs more than two bytes for some code points, so you still can't process it as two bytes per character.
 
 In C/C++ held in a "wide char" or "wide string".
 
@@ -341,7 +343,7 @@ There is a lot of criticism on the net about UTF-16 -- it's kind of the worst of
 * You can't assume every character is the same number of bytes
 * It takes up more memory than UTF-8
 
-`UTF Considered Harmful `_
+`UTF-16 Considered Harmful `_
 
 But to be fair:
 
@@ -393,6 +395,8 @@ use io.open:
 
 (https://docs.python.org/2/library/io.html#module-interface)
 
+.. note: This is all for Python 2 -- the built in ``open`` in Py3 does utf-8 by default.
+
 Encodings Built-in to Python:
   http://docs.python.org/2/library/codecs.html#standard-encodings
 
@@ -436,14 +440,14 @@ Exception messages:
 Unicode in Python 3
 ----------------------
 
-The "string" object is unicode.
+The "string" object **is** Unicode (always).
 
 Py3 has two distinct concepts:
 
-* "text" -- uses the str object (which is always unicode!)
+* "text" -- uses the str object (which is always Unicode!)
 * "binary data" -- uses bytes or bytearray
 
-Everything that's about text is unicode.
+Everything that's about text is Unicode.
 
 Everything that requires binary data uses bytes.
 
@@ -451,6 +455,34 @@ It's all much cleaner.
 
 (by the way, the recent implementations are very efficient...)
 
+So you can pretty much ignore encodings and all that for most basic text processing.
+If you do find yourself needing to deal with binary data, you ay need to encode/decode stuff yourself.  IN which case, Python provides an ``.encode()`` method on strings that encode the string to a bytes object with the encoding you select:
+
+.. code-block:: ipython
+
+    In [3]: this_in_utf16 = "this".encode('utf-16')
+
+    In [4]: this_in_utf16
+    Out[4]: b'\xff\xfet\x00h\x00i\x00s\x00'
+
+And bytes objects have a ``.decode`` method that decodes the bytes and makes a string object:
+
+    In [5]: this_in_utf16.decode('utf-16')
+    Out[5]: 'this'
+
+It's all quite simple an robust.
+
+.. note::
+  During the long and painful transition from Python2 to Python3, the Unicode-always string type was a major source of complaints.  There are many rants and `well thought out posts `_ about it still available on the internet. It was enough to think that Python had made a huge mistake.
+
+  But there are a couple key points to remember:
+
+  * The primary people struggling were those that wrote (or wored with) libraries that had to deal with protocols that used both binary and text data in the same data stream.
+
+  * As of Python 3.4 or so, the python string object had grown the features it needed to support even those ugly binary+text use cases.
+
+  For a typical user, the Python3 text model is MUCH easier to deal with and less error prone.
+
 
 Exercises
 =========
From 62896ee875dde86e4863cf134feec0a9ec852c12 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sat, 27 Apr 2019 13:53:35 -0700
Subject: [PATCH 73/87] updated the Naming Things page.
---
 source/modules/NamingThings.rst               | 43 +++++++++++++++++++
 source/solutions/Lesson02/fizz_buzz.py        | 20 ++++++++-
 .../solutions/Lesson02/fizz_buzz_one_liner.py |  6 +--
 3 files changed, 64 insertions(+), 5 deletions(-)
diff --git a/source/modules/NamingThings.rst b/source/modules/NamingThings.rst
index 362ada94..e0cc5a8b 100644
--- a/source/modules/NamingThings.rst
+++ b/source/modules/NamingThings.rst
@@ -91,6 +91,49 @@ And then singular for a single item in that collection:
     line = line.replace(",", " ")
     ....
 
+What about Hungarian Notation?
+------------------------------
+
+`Hungarian Notation `_
+is a naming system where the data type is part of the name:
+
+.. code-block:: python
+
+  strFirstName = "Chris"
+
+  listDonations = [400.0, 125.0, 1000.0]
+
+  int_num_days = 30
+
+This method is not recommended nor widely used in the Python community.
+
+One reason is Python's dynamic typing -- it really isn't important what type a value is, but rather, what it means.
+And you may end up refactoring the code to use a different type, and then do you want to have to rename everything?
+Or worse, the type in the name no longer matches the actual type in the code -- and that's really bad.  I have seen code like this:
+
+.. code-block:: python
+
+  strNumber = input("How many would you like?")
+  strNumber = int(strNumber)
+
+  for i in range(strNumber):
+      ...
+
+So you have a name used for a string, then it gets converted to an integer, and the data type no longer matches the name.  Wouldn't you be better off if that had never been named with the type in the first place?
+
+While widely used in some circles, it is generally considered bad style in the Python community -- so:
+
+ **Do not use Hungarian Notation**
+
+More About Naming Things
+------------------------
+
 Here's a nice talk about naming:
 
 `Jack Diederich: Name things Once `_
+
+One note about that talk -- Jack is mostly encouraging people to not use names that are too long and unnecessarily specific.
+However, with beginners, it's often tempting to use names that are too *short* and *non-specific*, like "x" and "item" -- so you need to strike a balance.
+
+
+
diff --git a/source/solutions/Lesson02/fizz_buzz.py b/source/solutions/Lesson02/fizz_buzz.py
index 6204fc8c..45b9fd73 100755
--- a/source/solutions/Lesson02/fizz_buzz.py
+++ b/source/solutions/Lesson02/fizz_buzz.py
@@ -18,15 +18,31 @@ def fizzbuzz1(n):
             print(i)
 
 
+def fizzbuzz1b(n):
+    """
+    Save one computation -- if it's a multiple of 3 and 5, it's a
+    multiple of 15
+    """
+    for i in range(1, n + 1):
+        if i % 15 == 0:
+            print("FizzBuzz")
+        elif i % 3 == 0:
+            print("Fizz")
+        elif i % 5 == 0:
+            print("Buzz")
+        else:
+            print(i)
+
+
 def fizzbuzz2(n):
     """
     Why evaluate i%3 and i%5 twice?
     """
     for i in range(1, n + 1):
         msg = ''
-        if i % 3 == 0:
+        if not i % 3:
             msg += "Fizz"
-        if i % 5 == 0:
+        if not i % 5:
             msg += "Buzz"
         if msg:
             print(msg)
diff --git a/source/solutions/Lesson02/fizz_buzz_one_liner.py b/source/solutions/Lesson02/fizz_buzz_one_liner.py
index f77dde9f..494c5ecd 100644
--- a/source/solutions/Lesson02/fizz_buzz_one_liner.py
+++ b/source/solutions/Lesson02/fizz_buzz_one_liner.py
@@ -1,15 +1,15 @@
 #!/usr/bin/env python3
 
 # This One Liner solution to the Fizz Buzz problem
-# was found by a student on the internet
+# was found by a student on the Internet
 
 for i in range(1,101): print([i,'Fizz','Buzz','FizzBuzz'][(i%3==0)+2*(i%5==0)])
 
 # this is a good example of why the most compact code is not always the
 # best -- readability counts!
-# And this is pretty impenatrable.
+# And this is pretty impenetrable.
 # but it's also pretty nifty logic, so below,
-# It's unpacked to make it easeir to understand.
+# It's unpacked to make it easier to understand.
 
 # first, add some white space to make it pep8 compatible, and more readable.
 
From 9b76e0e9e6bc8742918a5f4215015f3b756ef623 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sun, 5 May 2019 11:44:55 -0700
Subject: [PATCH 74/87] a bit of clarification
---
 source/exercises/dict_lab.rst | 9 ++++++++-
 source/exercises/file_lab.rst | 4 +++-
 2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/source/exercises/dict_lab.rst b/source/exercises/dict_lab.rst
index e048e032..6e1d100a 100644
--- a/source/exercises/dict_lab.rst
+++ b/source/exercises/dict_lab.rst
@@ -76,11 +76,18 @@ Dictionaries 2
 * Using the dictionary from item 1: Make a dictionary using the same keys but
   with the number of 't's in each value as the value (consider upper and lower case?).
 
+  The result should look something like::
+
+      {"name": 0
+       "city": 2
+       "cake": 2
+      }
+
 Sets
 ----
 
 * Create sets s2, s3 and s4 that contain numbers from zero through twenty,
-  divisible by 2, 3 and 4.
+  divisible by 2, 3 and 4 (figure out a way to compute those -- don't just type them in).
 
 * Display the sets.
 
diff --git a/source/exercises/file_lab.rst b/source/exercises/file_lab.rst
index a7b26bb3..a05fcdca 100644
--- a/source/exercises/file_lab.rst
+++ b/source/exercises/file_lab.rst
@@ -29,7 +29,9 @@ Paths and File Processing
     writing). Note that for binary files, you can't use ``readline()`` --
     lines don't have any meaning for binary files.
 
-  - Test it with both text and binary files (maybe jpeg or something of your choosing).
+  - Test it with both text and binary files (maybe a jpeg or something of your choosing).
+
+  - This should only be a few lines of code :-)
 
 
 File reading and parsing
From 2f5ed8edc582fe2d5432464c8ff335cbef2f66c7 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sun, 5 May 2019 11:46:23 -0700
Subject: [PATCH 75/87] typo
---
 source/exercises/dict_lab.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/source/exercises/dict_lab.rst b/source/exercises/dict_lab.rst
index 6e1d100a..9c863c54 100644
--- a/source/exercises/dict_lab.rst
+++ b/source/exercises/dict_lab.rst
@@ -102,5 +102,5 @@ Sets 2
 
 * Create a frozenset with the letters in 'marathon'.
 
-* display the union and intersection of the two sets.
+* Display the union and intersection of the two sets.
 
From 178bbe711588f37d9e6e21672f86991fb7907064 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sun, 5 May 2019 12:06:25 -0700
Subject: [PATCH 76/87] a bit of clarification and simiplification
---
 source/examples/file_exercise/students.txt |  4 ++--
 source/exercises/file_lab.rst              | 16 +++++++++-------
 2 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/source/examples/file_exercise/students.txt b/source/examples/file_exercise/students.txt
index dc8508d5..a85853a8 100644
--- a/source/examples/file_exercise/students.txt
+++ b/source/examples/file_exercise/students.txt
@@ -23,8 +23,8 @@ Mitchell, Joni: php, mysql, python
 Ramone, John: Johnny, rex, db
 King, Carol: r
 Waters, Muddy: perl, python
-Star, Richard: Ringo, Tom, shell, python, vb
-Smith, Patricia: Patti, Gene, python
+Star, Richard: Ringo, shell, python, vb
+Smith, Patricia: Patti, python
 Morrison, Jim: fortran, perl, sql, python
 Marley, Robert: Bob, c, c++, lisp
 Simon, Paul: bash, python, sql
diff --git a/source/exercises/file_lab.rst b/source/exercises/file_lab.rst
index a05fcdca..9aa4dfd9 100644
--- a/source/exercises/file_lab.rst
+++ b/source/exercises/file_lab.rst
@@ -16,13 +16,10 @@ Paths and File Processing
 =========================
 
 * Write a program which prints the full path for all files in the current
-  directory, one per line
+  directory, one per line. Use either the ``os`` module or ``pathlib``.
 
 * Write a program which copies a file from a source, to a destination
-  (without using shutil, or the OS copy command).
-
-  - Advanced: make it work for any size file: i.e. don't read the entire
-    contents of the file into memory at once.
+  (without using shutil, or the OS copy command (you are essentially writing a simple version of the OS copy command)).
 
   - This should work for any kind of file, so you need to open
     the files in binary mode: ``open(filename, 'rb')`` (or ``'wb'`` for
@@ -31,6 +28,9 @@ Paths and File Processing
 
   - Test it with both text and binary files (maybe a jpeg or something of your choosing).
 
+  - Advanced: make it work for any size file: i.e. don't read the entire
+    contents of the file into memory at once.
+
   - This should only be a few lines of code :-)
 
 
@@ -43,9 +43,9 @@ Download this text file:
 
 In it, you will find a list of names and what programming languages they have used in the past. This may be similar to a list generated at the beginning of this class.
 
-Write a little script that reads that file, and generates a list of all the languages that have been used.
+Write a little script that reads that file and generates a list of all the languages that have been used.
 
-What might be the best data structure to use to keep track of bunch of values without duplication?
+What might be the best data structure to use to keep track of bunch of values (the languages) without duplication?
 
 The file format:
 ----------------
@@ -62,5 +62,7 @@ So a colon after the name, then the nickname, and then one or more languages.
 
 However, like real data files, the file is NOT well-formed. Only some lines have nicknames, and other small differences, so you will need to write some code to make sure you get it all correct.
 
+How can you tell the difference between a nickname and a language?
+
 Extra challenge: keep track of how many students specified each language.
 
From d088f58b1771d07728beea4e7a535d43dc984235 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sun, 5 May 2019 12:30:00 -0700
Subject: [PATCH 77/87] updating mailroom solution
---
 source/solutions/Lesson04/mailroom2.py        |  59 +++--
 .../mailroom/mailroom_basic/mailroom.py       | 159 -------------
 .../mailroom/mailroom_basic/mailroom2.py      | 220 ------------------
 3 files changed, 40 insertions(+), 398 deletions(-)
 delete mode 100755 source/solutions/mailroom/mailroom_basic/mailroom.py
 delete mode 100755 source/solutions/mailroom/mailroom_basic/mailroom2.py
diff --git a/source/solutions/Lesson04/mailroom2.py b/source/solutions/Lesson04/mailroom2.py
index c4ca1790..a586655e 100755
--- a/source/solutions/Lesson04/mailroom2.py
+++ b/source/solutions/Lesson04/mailroom2.py
@@ -2,8 +2,11 @@
 """
 mailroom assignment
 
-This version uses a dict for the main db, and exception handling to
-check input
+This version uses a dict for the main db, and a dict to"switch" on the user's
+input choices.
+
+it also write the thank you letters to files.
+
 """
 
 import sys
@@ -20,6 +23,7 @@
 # This makes it easier to have a 'normalized' key.
 #  you could get a bit fancier by having each "record" be a dict, with
 #   "name" and "donations" as keys.
+
 def get_donor_db():
     return {'william gates iii': ("William Gates III", [653772.32, 12.17]),
             'jeff bezos': ("Jeff Bezos", [877.33]),
@@ -65,20 +69,7 @@ def add_donor(name):
     return donor
 
 
-def main_menu_selection():
-    """
-    Print out the main application menu and then read the user input.
-    """
-    action = input(dedent('''
-      Choose an action:
-
-      1 - Send a Thank You
-      2 - Create a Report
-      3 - Send letters to everyone
-      4 - Quit
 
-      > '''))
-    return action.strip()
 
 
 def gen_letter(donor):
@@ -195,7 +186,7 @@ def save_letters_to_disk():
         letter = gen_letter(donor)
         # I don't like spaces in filenames...
         filename = donor[0].replace(" ", "_") + ".txt"
-        print("writting letter to:", donor[0])
+        print("writing letter to:", donor[0])
         open(filename, 'w').write(letter)
 
 
@@ -213,18 +204,48 @@ def quit():
     sys.exit(0)
 
 
-if __name__ == "__main__":
+def main_menu():
+    """
+    Run the main menu for mailroom
+    """
+    prompt = dedent('''
+                    Choose an action:
 
-    donor_db = get_donor_db()
+                    1 - Send a Thank You
+                    2 - Create a Report
+                    3 - Send letters to everyone
+                    4 - Quit
+
+                    > ''')
 
     selection_dict = {"1": send_thank_you,
                       "2": print_donor_report,
                       "3": save_letters_to_disk,
                       "4": quit}
 
+    run_menu(prompt, selection_dict)
+
+
+def run_menu(prompt, selection_dict):
+    """
+    run an interactive menu
+
+    :param prompt: What you want to ask the user
+
+    :param selection_dict: Dict of possible user impots mapped to
+                           the actions to take.
+    """
     while True:
-        selection = main_menu_selection()
+
+        selection = input(prompt).strip()
         try:
+            # This calls teh function in the selection_dict
             selection_dict[selection]()
         except KeyError:
             print("error: menu selection is invalid!")
+
+if __name__ == "__main__":
+
+    donor_db = get_donor_db()
+
+    main_menu()
diff --git a/source/solutions/mailroom/mailroom_basic/mailroom.py b/source/solutions/mailroom/mailroom_basic/mailroom.py
deleted file mode 100755
index 9fd134a0..00000000
--- a/source/solutions/mailroom/mailroom_basic/mailroom.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/env python3
-
-"""
-Mailroom Exercise -- as of Session 3 -- no dictionaries or Exceptions
-"""
-
-from textwrap import dedent  # nifty utility!
-import math
-
-# In memory representation of the donor database
-# using a tuple for each donor
-# -- kind of like a record in a database table
-# the donations are in a list -- so you can add to them
-# Note the mutable inside an immutable
-
-donor_db = [("William Gates, III", [653772.32, 12.17]),
-            ("Jeff Bezos", [877.33]),
-            ("Paul Allen", [663.23, 43.87, 1.32]),
-            ("Mark Zuckerberg", [1663.23, 4300.87, 10432.0]),
-            ]
-
-#loop through the donor list and print the 0th element of the list
-def print_donors():
-    print("Donor list:\n")
-    for donor in donor_db:
-        print(donor[0])
-
-
-def find_donor(name):
-    """
-    find a donor in the donor db
-
-    :param: the name of the donor
-
-    :returns: The donor data structure -- None if not in the donor_db
-    """
-    for donor in donor_db:
-        # do a case-insenstive compare
-        if name.strip().lower() == donor[0].lower():
-            return donor
-    return None
-
-
-def main_menu_selection():
-    """
-    Print out the main application menu and then read the user input.
-    """
-    action = input(dedent('''
-      Choose an action:
-
-      't' - Send a Thank You
-      'r' - Create a Report
-      'q' - Quit
-
-      > '''))
-    return action.strip()
-
-
-def gen_letter(donor):
-    """
-    Generate a thank you letter for the donor
-
-    :param: donor tuple
-
-    :returns: string with letter
-    """
-    return dedent('''
-          Dear {}
-
-          Thank you for your very kind donation of ${:.2f}.
-          It will be put to very good use.
-
-                         Sincerely,
-                            -The Team
-          '''.format(donor[0], donor[1][-1]))
-
-
-def send_thank_you():
-    """
-    Execute the logic to record a donation and generate a thank you message.
-    """
-    # Read a valid donor to send a thank you from, handling special commands to
-    # let the user navigate as defined.
-    while True:
-        name = input("Enter a donor's name "
-                     "(or 'list' to see all donors or 'menu' to exit)> ").strip()
-        if name == "list":
-            print_donors()
-        elif name == "menu":
-            return
-        else:
-            break
-
-    # Now prompt the user for a donation amount to apply. Since this is
-    # also an exit point to the main menu, we want to make sure this is
-    # done before mutating the donors list object.
-    while True:
-        amount_str = input("Enter a donation amount (or 'menu' to exit) > ").strip()
-        if amount_str == "menu":
-            return
-        # Make sure amount is a valid amount before leaving the input loop
-        amount = float(amount_str)
-        # NOTE: this is getting a bit carried away...
-        if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00:
-            print("error: donation amount is invalid\n")
-            continue  # not really needed, but makes it more clear
-        else:
-            break
-
-    # If this is a new user, ensure that the database has the necessary data structure.
-    donor = find_donor(name)
-    if donor is None:
-        donor = (name, [])
-        donor_db.append(donor)
-
-    # Record the donation
-    # Note how the donor object can be manipulated while it is in the donors list.
-    donor[1].append(amount)
-
-    print(gen_letter(donor))
-
-
-def sort_key(item):
-    return item[1]
-
-
-def print_donor_report():
-    """
-    Generate the report of the donors and amounts donated.
-    """
-    # First, reduce the raw data into a summary list view
-    report_rows = []
-    for (name, gifts) in donor_db:
-        total_gifts = sum(gifts)
-        num_gifts = len(gifts)
-        avg_gift = total_gifts / num_gifts
-        report_rows.append((name, total_gifts, num_gifts, avg_gift))
-
-    # sort the report data
-    report_rows.sort(key=sort_key)
-    # print it out in with a nice format.
-    print("{:25s} | {:11s} | {:9s} | {:12s}".format(
-          "Donor Name", "Total Given", "Num Gifts", "Average Gift"))
-    print("-" * 66)
-    for row in report_rows:
-        print("{:25s}   {:11.2f}   {:9d}   {:12.2f}".format(*row))
-
-if __name__ == "__main__":
-    running = True
-    while running:
-        selection = main_menu_selection()
-        if selection == "t":
-            send_thank_you()
-        elif selection == "r":
-            print_donor_report()
-        elif selection == "q":
-            running = False
-        else:
-            print("error: menu selection is invalid!")
diff --git a/source/solutions/mailroom/mailroom_basic/mailroom2.py b/source/solutions/mailroom/mailroom_basic/mailroom2.py
deleted file mode 100755
index 81d3a97e..00000000
--- a/source/solutions/mailroom/mailroom_basic/mailroom2.py
+++ /dev/null
@@ -1,220 +0,0 @@
-#!/usr/bin/env python
-"""
-mailroom assignment
-
-This version uses a dict for the main db, and exception handling to
-check input
-"""
-
-import sys
-import math
-
-# handy utility to make pretty printing easier
-from textwrap import dedent
-
-
-# In memory representation of the donor database
-# using a tuple for each donor
-# -- kind of like a record in a database table
-# using a dict with a lower case version of the donor's name as the key
-# This makes it easier to have a 'normalized' key.
-#  you could get a bit fancier by having each "record" be a dict, with
-#   "name" and "donations" as keys.
-def get_donor_db():
-    return {'william gates iii': ("William Gates III", [653772.32, 12.17]),
-            'jeff bezos': ("Jeff Bezos", [877.33]),
-            'paul allen': ("Paul Allen", [663.23, 43.87, 1.32]),
-            'mark zuckerberg': ("Mark Zuckerberg", [1663.23, 4300.87, 10432.0]),
-            }
-
-
-def list_donors():
-    """
-    creates a list of the donors as a string, so they can be printed
-
-    Not calling print from here makes it more flexible and easier to
-    test
-    """
-    listing = ["Donor list:"]
-    for donor in donor_db.values():
-        listing.append(donor[0])
-    return "\n".join(listing)
-
-
-def find_donor(name):
-    """
-    find a donor in the donor db
-
-    :param: the name of the donor
-
-    :returns: The donor data structure -- None if not in the donor_db
-    """
-    key = name.strip().lower()
-    return donor_db.get(key)
-
-
-def add_donor(name):
-    """
-    Add a new donor to the donor db
-
-    :param: the name of the donor
-
-    :returns: the new Donor data structure
-    """
-    name = name.strip()
-    donor = (name, [])
-    donor_db[name.lower()] = donor
-    return donor
-
-
-def main_menu_selection():
-    """
-    Print out the main application menu and then read the user input.
-    """
-    action = input(dedent('''
-      Choose an action:
-
-      1 - Send a Thank You
-      2 - Create a Report
-      3 - Send letters to everyone
-      4 - Quit
-
-      > '''))
-    return action.strip()
-
-
-def gen_letter(donor):
-    """
-    Generate a thank you letter for the donor
-
-    :param: donor tuple
-
-    :returns: string with letter
-
-    note: This doesn't actually write to a file -- that's a separate
-          function. This makes it more flexible and easier to test.
-    """
-    return dedent('''Dear {0:s},
-
-          Thank you for your very kind donation of ${1:.2f}.
-          It will be put to very good use.
-
-                         Sincerely,
-                            -The Team
-          '''.format(donor[0], donor[1][-1]))
-
-
-def send_thank_you():
-    """
-    Execute the logic to record a donation and generate a thank you message.
-    """
-    # Read a valid donor to send a thank you from, handling special commands to
-    # let the user navigate as defined.
-    while True:
-        name = input("Enter a donor's name (or list to see all donors or 'menu' to exit)> ").strip()
-        if name == "list":
-            print(list_donors())
-        elif name == "menu":
-            return
-        else:
-            break
-
-    # Now prompt the user for a donation amount to apply. Since this is
-    # also an exit point to the main menu, we want to make sure this is
-    # done before mutating the db.
-    while True:
-        amount_str = input("Enter a donation amount (or 'menu' to exit)> ").strip()
-        if amount_str == "menu":
-            return
-        # Make sure amount is a valid amount before leaving the input loop
-        try:
-            amount = float(amount_str)
-            # extra check here -- unlikely that someone will type "NaN", but
-            # it IS possible, and it is a valid floating point number:
-            # http://en.wikipedia.org/wiki/NaN
-            if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00:
-                raise ValueError
-        # in this case, the ValueError could be raised by the float() call, or by the NaN-check
-        except ValueError:
-            print("error: donation amount is invalid\n")
-        else:
-            break
-
-    # If this is a new user, ensure that the database has the necessary
-    # data structure.
-    donor = find_donor(name)
-    if donor is None:
-        donor = add_donor(name)
-
-    # Record the donation
-    donor[1].append(amount)
-    print(gen_letter(donor))
-
-
-def sort_key(item):
-    # used to sort on name in donor_db
-    return item[1]
-
-
-def generate_donor_report():
-    """
-    Generate the report of the donors and amounts donated.
-
-    :returns: the donor report as a string.
-    """
-    # First, reduce the raw data into a summary list view
-    report_rows = []
-    for (name, gifts) in donor_db.values():
-        total_gifts = sum(gifts)
-        num_gifts = len(gifts)
-        avg_gift = total_gifts / num_gifts
-        report_rows.append((name, total_gifts, num_gifts, avg_gift))
-
-    # sort the report data
-    report_rows.sort(key=sort_key)
-    report = []
-    report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name",
-                                                            "Total Given",
-                                                            "Num Gifts",
-                                                            "Average Gift"))
-    report.append("-" * 66)
-    for row in report_rows:
-        report.append("{:25s}   ${:10.2f}   {:9d}   ${:11.2f}".format(*row))
-    return "\n".join(report)
-
-
-def save_letters_to_disk():
-    """
-    make a letter for each donor, and save it to disk.
-    """
-    for donor in donor_db.values():
-        letter = gen_letter(donor)
-        # I don't like spaces in filenames...
-        filename = donor[0].replace(" ", "_") + ".txt"
-        open(filename, 'w').write(letter)
-
-
-def print_donor_report():
-    print(generate_donor_report())
-
-
-def quit():
-    sys.exit(0)
-
-if __name__ == "__main__":
-
-    donor_db = get_donor_db()
-
-    running = True
-
-    selection_dict = {"1": send_thank_you,
-                      "2": print_donor_report,
-                      "3": save_letters_to_disk,
-                      "4": quit}
-
-    while True:
-        selection = main_menu_selection()
-        try:
-            selection_dict[selection]()
-        except KeyError:
-            print("error: menu selection is invalid!")
From c68903262340f5e3b1425738000cedf7bdf0b908 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sun, 5 May 2019 13:30:26 -0700
Subject: [PATCH 78/87] updated mailroom2 solution
---
 source/solutions/Lesson04/mailroom2.py | 98 ++++++++++++++------------
 1 file changed, 53 insertions(+), 45 deletions(-)
diff --git a/source/solutions/Lesson04/mailroom2.py b/source/solutions/Lesson04/mailroom2.py
index a586655e..1a118435 100755
--- a/source/solutions/Lesson04/mailroom2.py
+++ b/source/solutions/Lesson04/mailroom2.py
@@ -45,6 +45,14 @@ def list_donors():
     return "\n".join(listing)
 
 
+def print_donor_list():
+    """
+    Doesn't do much, but keeps the printing separate
+    """
+    print(list_donors())
+    print()
+
+
 def find_donor(name):
     """
     Find a donor in the donor db
@@ -68,10 +76,6 @@ def add_donor(name):
     donor_db[name.lower()] = donor
     return donor
 
-
-
-
-
 def gen_letter(donor):
     """
     Generate a thank you letter for the donor
@@ -92,35 +96,35 @@ def gen_letter(donor):
           '''.format(donor[0], donor[1][-1]))
 
 
-def take_donation(name):
+def take_donation():
     """
     Ask user for donation amount, and then add it  to the DB
     """
     # Now prompt the user for a donation amount to apply. Since this is
     # also an exit point to the main menu, we want to make sure this is
     # done before mutating the db.
+    print("in take_donation")
+    name = input("Enter a donor name (new or existing): \n >")
     while True:
-        amount_str = input("Enter a donation amount (or 'menu' to exit)> ").strip()
-        if amount_str == "menu":
+        amount_str = input("Enter a donation amount (or  to exit)> ").strip()
+        if not amount_str:
+            # if they provide no input, go back to previous menu
             return
         # Make sure amount is a valid amount before leaving the input loop
-        try:
-            amount = float(amount_str)
-            # extra check here -- unlikely that someone will type "NaN", but
-            # it IS possible, and it is a valid floating point number:
-            # http://en.wikipedia.org/wiki/NaN
-            if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00:
-                raise ValueError
-        # in this case, the ValueError could be raised by the float() call, or by the NaN-check
-        except ValueError:
+        amount = float(amount_str)
+        # extra check here -- unlikely that someone will type "NaN", but
+        # it IS possible, and it is a valid floating point number:
+        # http://en.wikipedia.org/wiki/NaN
+        if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00:
             print("error: donation amount is invalid\n")
+            continue
         else:
             break
 
-    # If this is a new user, ensure that the database has the necessary
-    # data structure.
     donor = find_donor(name)
+    # If the donor is not found, it's a new donor
     if donor is None:
+        # add the new donor to the database
         donor = add_donor(name)
 
     # Record the donation
@@ -129,23 +133,6 @@ def take_donation(name):
     print(gen_letter(donor))
 
 
-def send_thank_you():
-    """
-    Execute the logic to record a donation and generate a thank you message.
-    """
-    # Read a valid donor to send a thank you from, handling special commands to
-    # let the user navigate as defined.
-    while True:
-        name = input("Enter a donor's name or 'list' to see all donors or "
-                     "'menu' to exit to main menu > ").strip()
-        if name == "list":
-            print(list_donors())
-        elif name == "menu":
-            return
-        else:
-            take_donation(name)
-
-
 def sort_key(item):
     # used to sort on name in donor_db
     return item[1]
@@ -194,6 +181,10 @@ def print_donor_report():
     print(generate_donor_report())
 
 
+def return_to_menu():
+    ''' Return True to trigger exit out of sub-loop'''
+    return True
+
 def quit():
     """
     quit the program
@@ -203,6 +194,21 @@ def quit():
     """
     sys.exit(0)
 
+def send_thank_you():
+    """
+    Execute the logic to record a donation and generate a thank you message.
+    """
+    # Read a valid donor to send a thank you from, handling special commands to
+    # let the user navigate as defined.
+    prompt = ("To send a thank you, select one:\n\n"
+              "(1) Update donor and send thank-you\n"
+              "(2) List all existing DONORS\n"
+              "(3) Return to main menu\n > ")
+    selection_dict = {"1": take_donation,
+                      "2": print_donor_list,
+                      "3": return_to_menu,
+                      }
+    run_menu(prompt, selection_dict)
 
 def main_menu():
     """
@@ -211,10 +217,10 @@ def main_menu():
     prompt = dedent('''
                     Choose an action:
 
-                    1 - Send a Thank You
-                    2 - Create a Report
-                    3 - Send letters to everyone
-                    4 - Quit
+                    (1) - Send a Thank You
+                    (2) - Create a Report
+                    (3) - Send letters to everyone
+                    (4) - Quit
 
                     > ''')
 
@@ -236,13 +242,15 @@ def run_menu(prompt, selection_dict):
                            the actions to take.
     """
     while True:
-
-        selection = input(prompt).strip()
-        try:
-            # This calls teh function in the selection_dict
-            selection_dict[selection]()
-        except KeyError:
+        selection = input(prompt).strip().lower()
+        action = selection_dict.get(selection, None)
+        if action is None:
             print("error: menu selection is invalid!")
+        else:
+            if action():
+                # break out of the loop if action returns True
+                break
+
 
 if __name__ == "__main__":
 
From 05cb383caaa16f053ae344433dd23773353818e4 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sun, 5 May 2019 21:46:25 -0700
Subject: [PATCH 79/87] a bit of copy editing.
---
 source/modules/Comprehensions.rst | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/source/modules/Comprehensions.rst b/source/modules/Comprehensions.rst
index a813b5ab..397933f4 100644
--- a/source/modules/Comprehensions.rst
+++ b/source/modules/Comprehensions.rst
@@ -88,7 +88,7 @@ Comprehensions and map()
 
 Comprehensions are another way of expressing the "map" pattern from functional programming.
 
-Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. And there is nothing that can be expressed with ``map()`` that cannot be done with a comprehension. IF youare not familiar with ``map()``, you can saftly skip this, but if you are:
+Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. And there is nothing that can be expressed with ``map()`` that cannot be done with a comprehension. If you are not familiar with ``map()``, you can safely skip this, but if you are:
 
 .. code-block:: python
 
@@ -100,7 +100,7 @@ is the same as:
 
     [a_function(item), for item in an_iterable]
 
-In this case, the comprehension is a tad wordier than ``map()``.  BUt comprehensions really shine when you do'nt already have a handy function to pass to map:
+In this case, the comprehension is a tad wordier than ``map()``.  But comprehensions really shine when you don't already have a handy function to pass to map:
 
 .. code-block:: python
 
@@ -144,7 +144,7 @@ This kind of "filtering" loop can be achieved by adding a conditional to the com
 
     new_list = [expr for var in a_list if something_is_true]
 
-This is expressing the "filter" pattern and the "map" pattern at the same time -- one reason I like the comprehension sytax so much.
+This is expressing the "filter" pattern and the "map" pattern at the same time -- one reason I like the comprehension syntax so much.
 
 
 .. rubric:: Examples:
@@ -477,4 +477,4 @@ Once you've got the hang of it, you may want to read this so you don't overdo it
 
 https://treyhunner.com/2019/03/abusing-and-overusing-list-comprehensions-in-python/
 
-Trey writes a lot of good stuff -- I recommned browsing his site.
+Trey writes a lot of good stuff -- I recommend browsing his site.
From 0a6ed5ec3850adfdbec7a81a08df89c007497a44 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sun, 12 May 2019 10:35:27 -0700
Subject: [PATCH 80/87] added mailroom 3 solution
---
 source/solutions/Lesson05/mailroom3.py | 253 +++++++++++++++++++++++++
 1 file changed, 253 insertions(+)
 create mode 100755 source/solutions/Lesson05/mailroom3.py
diff --git a/source/solutions/Lesson05/mailroom3.py b/source/solutions/Lesson05/mailroom3.py
new file mode 100755
index 00000000..958cee05
--- /dev/null
+++ b/source/solutions/Lesson05/mailroom3.py
@@ -0,0 +1,253 @@
+#!/usr/bin/env python
+"""
+mailroom assignment
+
+This version uses a dict for the main db, and a dict to"switch" on the user's
+input choices.
+
+it also write the thank you letters to files.
+
+"""
+
+import sys
+import math
+from operator import itemgetter
+
+# handy utility to make pretty printing easier
+from textwrap import dedent
+
+
+# In memory representation of the donor database
+# using a tuple for each donor
+# -- kind of like a record in a database table
+# using a dict with a lower case version of the donor's name as the key
+# This makes it easier to have a 'normalized' key.
+#  you could get a bit fancier by having each "record" be a dict, with
+#   "name" and "donations" as keys.
+
+def get_donor_db():
+    return {'william gates iii': ("William Gates III", [653772.32, 12.17]),
+            'jeff bezos': ("Jeff Bezos", [877.33]),
+            'paul allen': ("Paul Allen", [663.23, 43.87, 1.32]),
+            'mark zuckerberg': ("Mark Zuckerberg", [1663.23, 4300.87, 10432.0]),
+            }
+
+
+def list_donors():
+    """
+    Create a list of the donors as a string, so they can be printed
+
+    Not calling print from here makes it more flexible and easier to
+    test
+    """
+    listing = ["Donor list:"]
+    for donor in donor_db.values():
+        listing.append(donor[0])
+    return "\n".join(listing)
+
+
+def print_donor_list():
+    """
+    Doesn't do much, but keeps the printing separate
+    """
+    print(list_donors())
+    print()
+
+
+def find_donor(name):
+    """
+    Find a donor in the donor db
+
+    :param: the name of the donor
+    :returns: The donor data structure -- None if not in the donor_db
+    """
+    key = name.strip().lower()
+    return donor_db.get(key)
+
+
+def add_donor(name):
+    """
+    Add a new donor to the donor db
+
+    :param: the name of the donor
+    :returns: the new Donor data structure
+    """
+    name = name.strip()
+    donor = (name, [])
+    donor_db[name.lower()] = donor
+    return donor
+
+
+def gen_letter(donor):
+    """
+    Generate a thank you letter for the donor
+
+    :param: donor tuple
+    :returns: string with letter
+
+    note: This doesn't actually write to a file -- that's a separate
+          function. This makes it more flexible and easier to test.
+    """
+    return dedent('''Dear {0:s},
+
+          Thank you for your very kind donation of ${1:.2f}.
+          It will be put to very good use.
+
+                         Sincerely,
+                            -The Team
+          '''.format(donor[0], donor[1][-1]))
+
+
+def take_donation():
+    """
+    Ask user for donation amount, and then add it  to the DB
+    """
+    # Now prompt the user for a donation amount to apply. Since this is
+    # also an exit point to the main menu, we want to make sure this is
+    # done before mutating the db.
+    print("in take_donation")
+    name = input("Enter a donor name (new or existing): \n >")
+    while True:
+        amount_str = input("Enter a donation amount (or  to exit)> ").strip()
+        if not amount_str:
+            # if they provide no input, go back to previous menu
+            return
+        # Make sure amount is a valid amount before leaving the input loop
+        try:
+            amount = float(amount_str)
+            # extra check here -- unlikely that someone will type "NaN", but
+            # it IS possible, and it is a valid floating point number:
+            # http://en.wikipedia.org/wiki/NaN
+            if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00:
+                raise ValueError
+        except ValueError:
+            print("error: donation amount is invalid\n")
+            continue
+        else:
+            break
+
+    donor = find_donor(name)
+    # If the donor is not found, it's a new donor
+    if donor is None:
+        # add the new donor to the database
+        donor = add_donor(name)
+
+    # Record the donation
+    donor[1].append(amount)
+    # print the thank you letter
+    print(gen_letter(donor))
+
+
+def sort_key(item):
+    # used to sort on name in donor_db
+    return item[1]
+
+
+def generate_donor_report():
+    """
+    Generate the report of the donors and amounts donated.
+
+    :returns: the donor report as a string.
+    """
+    # First, reduce the raw data into a summary list view
+    report_rows = []
+    for (name, gifts) in donor_db.values():
+        total_gifts = sum(gifts)
+        num_gifts = len(gifts)
+        avg_gift = total_gifts / num_gifts
+        report_rows.append((name, total_gifts, num_gifts, avg_gift))
+
+    # sort the report data
+    report_rows.sort(key=itemgetter(1), reverse=True)
+    report = []
+    report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name",
+                                                            "Total Given",
+                                                            "Num Gifts",
+                                                            "Average Gift"))
+    report.append("-" * 66)
+    for row in report_rows:
+        report.append("{:25s}   ${:10.2f}   {:9d}   ${:11.2f}".format(*row))
+    return "\n".join(report)
+
+
+def save_letters_to_disk():
+    """
+    make a letter for each donor, and save it to disk.
+    """
+    for donor in donor_db.values():
+        letter = gen_letter(donor)
+        # I don't like spaces in filenames...
+        filename = donor[0].replace(" ", "_") + ".txt"
+        print("writing letter to:", donor[0])
+        open(filename, 'w').write(letter)
+
+
+def print_donor_report():
+    print(generate_donor_report())
+
+
+def return_to_menu():
+    ''' Return True to trigger exit out of sub-loop'''
+    return True
+
+
+def send_thank_you():
+    """
+    Execute the logic to record a donation and generate a thank you message.
+    """
+    # Read a valid donor to send a thank you from, handling special commands to
+    # let the user navigate as defined.
+    prompt = ("To send a thank you, select one:\n\n"
+              "(1) Update donor and send thank-you\n"
+              "(2) List all existing DONORS\n"
+              "(3) Return to main menu\n > ")
+    selection_dict = {"1": take_donation,
+                      "2": print_donor_list,
+                      "3": return_to_menu,
+                      }
+    run_menu(prompt, selection_dict)
+
+def main_menu():
+    """
+    Run the main menu for mailroom
+    """
+    prompt = dedent('''
+                    Choose an action:
+
+                    (1) - Send a Thank You
+                    (2) - Create a Report
+                    (3) - Send letters to everyone
+                    (4) - Quit
+
+                    > ''')
+
+    selection_dict = {"1": send_thank_you,
+                      "2": print_donor_report,
+                      "3": save_letters_to_disk,
+                      "4": quit}
+
+    run_menu(prompt, selection_dict)
+
+
+def run_menu(prompt, selection_dict):
+    """
+    run an interactive menu
+
+    :param prompt: What you want to ask the user
+
+    :param selection_dict: Dict of possible user impots mapped to
+                           the actions to take.
+    """
+    while True:
+        selection = input(prompt).strip().lower()
+        try:
+            if selection_dict[selection]():
+                # break out of the loop if action returns True
+                break
+        except KeyError:
+            print("error: menu selection is invalid!")
+
+
+if __name__ == "__main__":
+    donor_db = get_donor_db()
+    main_menu()
From 0d724986f85c9b25854e57298c26b11395c35809 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sun, 12 May 2019 10:41:32 -0700
Subject: [PATCH 81/87] clean up the solutions a bit
---
 .../Lesson05/dict_set_with_comps_solution.py          |  2 +-
 source/solutions/Lesson05/fizz_buzz_comprehension.py  | 11 ++++++++++-
 2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/source/solutions/Lesson05/dict_set_with_comps_solution.py b/source/solutions/Lesson05/dict_set_with_comps_solution.py
index a60375eb..6bb42b2e 100644
--- a/source/solutions/Lesson05/dict_set_with_comps_solution.py
+++ b/source/solutions/Lesson05/dict_set_with_comps_solution.py
@@ -22,7 +22,7 @@
 # 2. Using a list comprehension, build a dictionary of numbers from zero
 # to fifteen and the hexadecimal equivalent (string is fine).
 
-print(dict([(i, hex(i)) for i in range(16)]))
+print(dict(((i, hex(i)) for i in range(16))))
 
 # 3. Do the previous entirely with a dict comprehension -- should be a one-liner
 
diff --git a/source/solutions/Lesson05/fizz_buzz_comprehension.py b/source/solutions/Lesson05/fizz_buzz_comprehension.py
index 44588fde..a29fbf4d 100755
--- a/source/solutions/Lesson05/fizz_buzz_comprehension.py
+++ b/source/solutions/Lesson05/fizz_buzz_comprehension.py
@@ -1,6 +1,15 @@
 #!/usr/bin/env python3
 
+"""
+doing all of fizzbuzz in a comprehension
 
-fb = [[str(i), 'Fizz', 'Buzz', 'FizzBuzz'][(i % 3 == 0) + 2 * (i % 5 == 0)] for i in range(1, 101)]
+Is this a good idea?
+ -- probably not, but it's kind of cool
+
+ Note that it uses a generator comprehension, so it won't actually get
+ computed until the join() call
+"""
+
+fb = ([str(i), 'Fizz', 'Buzz', 'FizzBuzz'][(i % 3 == 0) + 2 * (i % 5 == 0)] for i in range(1, 101))
 
 print('\n'.join(fb))
From cae42970e2697d137c383bf55b1b951216771683 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Tue, 14 May 2019 07:42:29 -0700
Subject: [PATCH 82/87] added ignore for files generated by mailroom3
---
 source/solutions/Lesson05/.gitignore | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100644 source/solutions/Lesson05/.gitignore
diff --git a/source/solutions/Lesson05/.gitignore b/source/solutions/Lesson05/.gitignore
new file mode 100644
index 00000000..88917bc7
--- /dev/null
+++ b/source/solutions/Lesson05/.gitignore
@@ -0,0 +1,4 @@
+Jeff_Bezos.txt
+Mark_Zuckerberg.txt
+Paul_Allen.txt
+William_Gates_III.txt
From 0e62a10b5dcfb72133ddcfaade30f5a7e6abe364 Mon Sep 17 00:00:00 2001
From: Chris Barker 
Date: Sat, 18 May 2019 17:03:05 -0400
Subject: [PATCH 83/87] updated to the latest mailroom3 used in previous lesson
 solutions
---
 .../Lesson06/{mailroom2.py => mailroom3.py}   | 131 +++++++++++-------
 .../{test_mailroom2.py => test_mailroom3.py}  |   7 +-
 2 files changed, 88 insertions(+), 50 deletions(-)
 rename source/solutions/Lesson06/{mailroom2.py => mailroom3.py} (68%)
 rename source/solutions/Lesson06/{test_mailroom2.py => test_mailroom3.py} (94%)
diff --git a/source/solutions/Lesson06/mailroom2.py b/source/solutions/Lesson06/mailroom3.py
similarity index 68%
rename from source/solutions/Lesson06/mailroom2.py
rename to source/solutions/Lesson06/mailroom3.py
index 49dfd81d..958cee05 100755
--- a/source/solutions/Lesson06/mailroom2.py
+++ b/source/solutions/Lesson06/mailroom3.py
@@ -2,12 +2,16 @@
 """
 mailroom assignment
 
-This version uses a dict for the main db, and exception handling to
-check input, and has been factored to be amenable to testing.
+This version uses a dict for the main db, and a dict to"switch" on the user's
+input choices.
+
+it also write the thank you letters to files.
+
 """
 
 import sys
 import math
+from operator import itemgetter
 
 # handy utility to make pretty printing easier
 from textwrap import dedent
@@ -20,6 +24,7 @@
 # This makes it easier to have a 'normalized' key.
 #  you could get a bit fancier by having each "record" be a dict, with
 #   "name" and "donations" as keys.
+
 def get_donor_db():
     return {'william gates iii': ("William Gates III", [653772.32, 12.17]),
             'jeff bezos': ("Jeff Bezos", [877.33]),
@@ -30,7 +35,7 @@ def get_donor_db():
 
 def list_donors():
     """
-    creates a list of the donors as a string, so they can be printed
+    Create a list of the donors as a string, so they can be printed
 
     Not calling print from here makes it more flexible and easier to
     test
@@ -41,12 +46,19 @@ def list_donors():
     return "\n".join(listing)
 
 
+def print_donor_list():
+    """
+    Doesn't do much, but keeps the printing separate
+    """
+    print(list_donors())
+    print()
+
+
 def find_donor(name):
     """
-    find a donor in the donor db
+    Find a donor in the donor db
 
     :param: the name of the donor
-
     :returns: The donor data structure -- None if not in the donor_db
     """
     key = name.strip().lower()
@@ -58,7 +70,6 @@ def add_donor(name):
     Add a new donor to the donor db
 
     :param: the name of the donor
-
     :returns: the new Donor data structure
     """
     name = name.strip()
@@ -67,28 +78,11 @@ def add_donor(name):
     return donor
 
 
-def main_menu_selection():
-    """
-    Print out the main application menu and then read the user input.
-    """
-    action = input(dedent('''
-      Choose an action:
-
-      1 - Send a Thank You
-      2 - Create a Report
-      3 - Send letters to everyone
-      4 - Quit
-
-      > '''))
-    return action.strip()
-
-
 def gen_letter(donor):
     """
     Generate a thank you letter for the donor
 
     :param: donor tuple
-
     :returns: string with letter
 
     note: This doesn't actually write to a file -- that's a separate
@@ -104,27 +98,19 @@ def gen_letter(donor):
           '''.format(donor[0], donor[1][-1]))
 
 
-def send_thank_you():
+def take_donation():
     """
-    Execute the logic to record a donation and generate a thank you message.
+    Ask user for donation amount, and then add it  to the DB
     """
-    # Read a valid donor to send a thank you from, handling special commands to
-    # let the user navigate as defined.
-    while True:
-        name = input("Enter a donor's name (or list to see all donors or 'menu' to exit)> ").strip()
-        if name == "list":
-            print(list_donors())
-        elif name == "menu":
-            return
-        else:
-            break
-
     # Now prompt the user for a donation amount to apply. Since this is
     # also an exit point to the main menu, we want to make sure this is
     # done before mutating the db.
+    print("in take_donation")
+    name = input("Enter a donor name (new or existing): \n >")
     while True:
-        amount_str = input("Enter a donation amount (or 'menu' to exit)> ").strip()
-        if amount_str == "menu":
+        amount_str = input("Enter a donation amount (or  to exit)> ").strip()
+        if not amount_str:
+            # if they provide no input, go back to previous menu
             return
         # Make sure amount is a valid amount before leaving the input loop
         try:
@@ -134,20 +120,21 @@ def send_thank_you():
             # http://en.wikipedia.org/wiki/NaN
             if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00:
                 raise ValueError
-        # in this case, the ValueError could be raised by the float() call, or by the NaN-check
         except ValueError:
             print("error: donation amount is invalid\n")
+            continue
         else:
             break
 
-    # If this is a new user, ensure that the database has the necessary
-    # data structure.
     donor = find_donor(name)
+    # If the donor is not found, it's a new donor
     if donor is None:
+        # add the new donor to the database
         donor = add_donor(name)
 
     # Record the donation
     donor[1].append(amount)
+    # print the thank you letter
     print(gen_letter(donor))
 
 
@@ -171,7 +158,7 @@ def generate_donor_report():
         report_rows.append((name, total_gifts, num_gifts, avg_gift))
 
     # sort the report data
-    report_rows.sort(key=sort_key)
+    report_rows.sort(key=itemgetter(1), reverse=True)
     report = []
     report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name",
                                                             "Total Given",
@@ -191,6 +178,7 @@ def save_letters_to_disk():
         letter = gen_letter(donor)
         # I don't like spaces in filenames...
         filename = donor[0].replace(" ", "_") + ".txt"
+        print("writing letter to:", donor[0])
         open(filename, 'w').write(letter)
 
 
@@ -198,23 +186,68 @@ def print_donor_report():
     print(generate_donor_report())
 
 
-def quit():
-    sys.exit(0)
+def return_to_menu():
+    ''' Return True to trigger exit out of sub-loop'''
+    return True
 
-if __name__ == "__main__":
 
-    donor_db = get_donor_db()
+def send_thank_you():
+    """
+    Execute the logic to record a donation and generate a thank you message.
+    """
+    # Read a valid donor to send a thank you from, handling special commands to
+    # let the user navigate as defined.
+    prompt = ("To send a thank you, select one:\n\n"
+              "(1) Update donor and send thank-you\n"
+              "(2) List all existing DONORS\n"
+              "(3) Return to main menu\n > ")
+    selection_dict = {"1": take_donation,
+                      "2": print_donor_list,
+                      "3": return_to_menu,
+                      }
+    run_menu(prompt, selection_dict)
+
+def main_menu():
+    """
+    Run the main menu for mailroom
+    """
+    prompt = dedent('''
+                    Choose an action:
 
-    running = True
+                    (1) - Send a Thank You
+                    (2) - Create a Report
+                    (3) - Send letters to everyone
+                    (4) - Quit
+
+                    > ''')
 
     selection_dict = {"1": send_thank_you,
                       "2": print_donor_report,
                       "3": save_letters_to_disk,
                       "4": quit}
 
+    run_menu(prompt, selection_dict)
+
+
+def run_menu(prompt, selection_dict):
+    """
+    run an interactive menu
+
+    :param prompt: What you want to ask the user
+
+    :param selection_dict: Dict of possible user impots mapped to
+                           the actions to take.
+    """
     while True:
-        selection = main_menu_selection()
+        selection = input(prompt).strip().lower()
         try:
-            selection_dict[selection]()
+            if selection_dict[selection]():
+                # break out of the loop if action returns True
+                break
         except KeyError:
             print("error: menu selection is invalid!")
+
+
+if __name__ == "__main__":
+    donor_db = get_donor_db()
+    main_menu()
diff --git a/source/solutions/Lesson06/test_mailroom2.py b/source/solutions/Lesson06/test_mailroom3.py
similarity index 94%
rename from source/solutions/Lesson06/test_mailroom2.py
rename to source/solutions/Lesson06/test_mailroom3.py
index e633e575..49cfad52 100644
--- a/source/solutions/Lesson06/test_mailroom2.py
+++ b/source/solutions/Lesson06/test_mailroom3.py
@@ -5,7 +5,7 @@
 """
 import os
 
-import mailroom2 as mailroom
+import mailroom3 as mailroom
 
 # so that it's there for the tests
 mailroom.donor_db = mailroom.get_donor_db()
@@ -50,6 +50,11 @@ def test_gen_letter():
 
 
 def test_add_donor():
+    """
+    adds a new donor
+
+    then tests that the donor is added, and that a donation is properly recorded.
+    """
     name = "Fred Flintstone  "
 
     donor = mailroom.add_donor(name)
From 9155414b6c46661d30f818693d46fd97277597d3 Mon Sep 17 00:00:00 2001
From: smckellips 
Date: Tue, 28 May 2019 17:08:41 -0700
Subject: [PATCH 84/87] Misc typos (#185)
* Text update
* Text Update
---
 source/exercises/html_renderer.rst          | 4 ++--
 source/exercises/html_renderer_tutorial.rst | 2 +-
 source/modules/Strings.rst                  | 6 +++---
 source/scripts/dict_as_switch.rst           | 2 +-
 4 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/source/exercises/html_renderer.rst b/source/exercises/html_renderer.rst
index 8175b30b..8761e90d 100644
--- a/source/exercises/html_renderer.rst
+++ b/source/exercises/html_renderer.rst
@@ -30,7 +30,7 @@ If you don't know html -- just look at the example and copy that. And you can re
 
 The exercise is broken down into a number of steps -- each requiring a few more OO concepts in Python.
 
-The goal of the code is render html. The goal of the *exercise* is to build up a simple object hierarchy with:
+The goal of the code is to render html. The goal of the *exercise* is to build up a simple object hierarchy with:
 
 * classes
 * class attributes
@@ -219,7 +219,7 @@ Part B:
 
 Now it gets fun!
 
-Now that you have multipel types of elements, it's worth looking a bit at how html works. A given element can hold text, but it can *also* hold other elements. So we need to update our ``Element`` classes to support that.
+Now that you have multiple types of elements, it's worth looking a bit at how html works. A given element can hold text, but it can *also* hold other elements. So we need to update our ``Element`` classes to support that.
 
 Extend the ``Element.render()`` method so that it can render other elements inside the tag in addition to strings. A recursion-like approach should do it. i.e. it can call the ``render()`` method of the elements it contains.
 
diff --git a/source/exercises/html_renderer_tutorial.rst b/source/exercises/html_renderer_tutorial.rst
index e86de07a..762688c0 100644
--- a/source/exercises/html_renderer_tutorial.rst
+++ b/source/exercises/html_renderer_tutorial.rst
@@ -831,7 +831,7 @@ Now we are getting a little more interesting.
 
 This is easy; you know how to do that, yes?
 
-But the training wheels are off -- you are going to need to write your own tests now.  So before you create the ``Head`` element class, write a test for it. You should be able to copy and paste one of the previous tests, and just change the name of the class and the tag value. Remember to give yo test a new name, or it will simply replace the previous test.
+But the training wheels are off -- you are going to need to write your own tests now.  So before you create the ``Head`` element class, write a test for it. You should be able to copy and paste one of the previous tests, and just change the name of the class and the tag value. Remember to give your test a new name, or it will simply replace the previous test.
 
 I like to run the tests as soon as I make a new one. If nothing else, I can make sure I have one more test.
 
diff --git a/source/modules/Strings.rst b/source/modules/Strings.rst
index ac712fea..cafbfcc7 100644
--- a/source/modules/Strings.rst
+++ b/source/modules/Strings.rst
@@ -196,8 +196,8 @@ Common Escape Sequences::
     \t  ASCII Horizontal Tab (TAB)
     \ooo  Character with octal value ooo
     \xhh  Character with hex value hh
-    \uxxxx Charactor with Unicode code point value xxxx
-    \N{char-name} Charactor with Unicdoe name char_name
+    \uxxxx Character with Unicode code point value xxxx
+    \N{char-name} Character with Unicdoe name char_name
 
 For example -- for tab-separated values:
 
@@ -215,7 +215,7 @@ https://docs.python.org/3/library/stdtypes.html#string-methods
 Raw Strings
 ------------
 
-There are times when you want a literal backslash in your string: Windows file paths, regular expressions.  Tomake this easy, Pyhton support "raw" strings -- string literals where the backslash does not have special meaning:
+There are times when you want a literal backslash in your string: Windows file paths, regular expressions.  To make this easy, Python support "raw" strings -- string literals where the backslash does not have special meaning:
 
 Add an ``r`` in front of the string literal:
 
diff --git a/source/scripts/dict_as_switch.rst b/source/scripts/dict_as_switch.rst
index 872bbf87..171f8d0d 100644
--- a/source/scripts/dict_as_switch.rst
+++ b/source/scripts/dict_as_switch.rst
@@ -105,7 +105,7 @@ Perhaps a little silly for only two options,
 but I hope you get the idea.
 
 And if you establish a protocol for what those functions return,
-you can use the return value -- perhaps as simple as a True or FAlse to indicate success.
+you can use the return value -- perhaps as simple as a True or False to indicate success.
 
 Or a sentinel value to indicate it's time to break out of a loop.
 
From bce41c45765708d82ec9c1f4fce1f5a2f20bc05e Mon Sep 17 00:00:00 2001
From: "Christopher H.Barker, PhD" 
Date: Wed, 5 Jun 2019 23:03:33 -0300
Subject: [PATCH 85/87] removed "wait for classmethod"
---
 source/exercises/circle_class.rst | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/source/exercises/circle_class.rst b/source/exercises/circle_class.rst
index 26377e95..b2c47af9 100644
--- a/source/exercises/circle_class.rst
+++ b/source/exercises/circle_class.rst
@@ -32,8 +32,7 @@ You will use:
 
   - properties.
   - a bunch of "magic methods".
-  - a classmethod (after you've learned about them...).
-
+  - a classmethod.
 
 General Instructions:
 ---------------------
@@ -121,8 +120,6 @@ The user should not be able to set the area:
 Step 5:
 -------
 
-**NOTE:** wait on this one 'till we learn about class methods..
-
 Add an "alternate constructor" that lets the user create a Circle directly
 with the diameter:
 
@@ -134,6 +131,8 @@ with the diameter:
     >> print(c.radius)
     4
 
+Hint: This is a good use case for a ``classmethod``
+
 Step 6:
 -------
 
From 8e369daa2cbf93895b173870dc147ce7179b6727 Mon Sep 17 00:00:00 2001
From: "Christopher H.Barker, PhD" 
Date: Mon, 5 Aug 2019 09:50:05 -0700
Subject: [PATCH 86/87] fixed some typos
---
 source/modules/Tutorial.rst | 56 +++++++++++++++++++++----------------
 1 file changed, 32 insertions(+), 24 deletions(-)
diff --git a/source/modules/Tutorial.rst b/source/modules/Tutorial.rst
index 1f9a33e3..e5eb5740 100644
--- a/source/modules/Tutorial.rst
+++ b/source/modules/Tutorial.rst
@@ -39,7 +39,7 @@ There are a number of ways to run python code:
 - At the interpreter, often referred to as a REPL (Read, Evaluate, Print Loop)
 - At an enhanced interpreter such as iPython
 - In a browser-based interactive system such as the Jupyter Notebook
-- From and IDE, such as IDLE or PyCharm
+- From an IDE, such as IDLE or PyCharm
 - Calling python from the command line to run a file.
 
 While working with an interactive interpreter can be an excellent way to explore Python (and I highly recommend it), For this tutorial, to get you used to "real" production development, you will write, edit, and save your code in a programmer's text editor, and run it from the command line.
@@ -47,7 +47,8 @@ While working with an interactive interpreter can be an excellent way to explore
 A Programmer's Text Editor
 --------------------------
 
-A bit here about an editor, and recommendations on selecting one, with pointers to documentation about editor configuration.
+See These notes for getting set up with an editor and Python itself: :ref:`setting_up_dev_environment`
+
 
 The Python Interpreter
 ----------------------
@@ -66,19 +67,19 @@ These each have their own special uses for interaction the the Java VM or CLR, o
 
 [link to setting up your environment here]
 
-For this tutorial, you will need cPython version 3.6, installed and running so that when you type "python" at your command line, it starts up:
+For this tutorial, you will need cPython version 3.7, installed and running so that when you type "python" at your command line, it starts up:
 
 .. code-block:: bash
 
   MacBook-Pro:~ Chris$ python
-  Python 3.6.2 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06)
+  Python 3.7.4 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06)
   [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
   Type "help", "copyright", "credits" or "license" for more information.
   >>>
 
-Your result may be slightly different, but it should say Python 3.6. *something* there at the top, and give you the command prompt (``>>>``) at the end.
+Your result may be slightly different, but it should say Python 3.7. *something* there at the top, and give you the command prompt (``>>>``) at the end.
 
-You can get out of it by typing ctrl+D (on OS_X and Linux) or ctrl+Z (On Windows), or typing ``exit()`` and hitting .
+You can get out of it by typing ctrl+D (on OS-X and Linux) or ctrl+Z (On Windows), or typing ``exit()`` and then .
 
 Your first "program"
 --------------------
@@ -120,7 +121,7 @@ In this case, python ran the one line of code you put in that file, which told i
 The print function
 ------------------
 
-you can display jsut about anything in Python with the ``print()`` function. Simply type::
+you can display just about anything in Python with the ``print()`` function. Simply type::
 
   print(what you want to print)
 
@@ -129,20 +130,20 @@ examples:
   print(45)
   print("this is a bit of text")
 
-you can print more than one thing by separating them with parentheses::
+you can print more than one thing by separating them with commas, inside the parenthesis::
 
-  print("the value of pi is:", 3.1459, "to two decimal places")
+  print("the value of pi is:", 3.1459, "to four decimal places")
 
 Text in Python
 --------------
 
-Text in python is supported by the "str" datatype, which is short for "string". The text datatype is often referred to called "strings" in computer science because it is strings of characters.
+Text in python is supported by the "str" datatype, which is short for "string". The text datatype is often referred to as "strings" in computer science because it is a series, or string of characters.
 
-In Python3, strings can be any length, and contain any character (even in virtually any language). This is because they support "Unicode" which is a system for representing all the characters of virtually all the languages used on earth.
+In Python3, strings can be any length, and contain any character (in virtually any language). This is because they support "Unicode" which is a system for representing all the characters of virtually all the languages used on earth.
 
 There are many complications to full support of Unicode, but for the most part, in Python it "just works". Any text you can put in your text editor should work fine.
 
-To create a str, you simply type what you want surrounded by either double or, single quotes (the apostrophe).
+To create a str, you simply type what you want surrounded by either double or single quotes (the apostrophe).
 
 Type this in a new file, called ``strings.py``:
 
@@ -161,13 +162,13 @@ run the file, and you should get something like this::
   MacBook-Pro:tutorial Chris$ python strings.py
   This is a basic string
   This is exactly the same string
-  you want to use double quotes if there's an apostrophe, like this: ' in the string
-  and you can use single quotes if you want to "quote" a word
+  You want to use double quotes if there's an apostrophe, like this: ' in the string
+  You can use single quotes if you want to "quote" a word
 
 Numbers in Python
 -----------------
 
-Python support two types of numbers in Python: integers (int) -- or whole numbers:
+Python supports two types of numbers: integers (int) -- or "whole numbers", with no fractional part:
 
 .. code-block:: python
 
@@ -180,18 +181,18 @@ integers can be negative or positive and as large as you want:
 >>> print(12345678987654321234567890987654321234567898765)
 12345678987654321234567890987654321234567898765
 
-"real numbers" are called "floating point" (float) numbers. They are internally stored as binary, but you wirte them as regular decimal numbers:
+"real numbers" are called "floating point" (float) numbers. They are internally stored as binary, but you write them as regular decimal (base 10) numbers:
 
 .. code-block:: python
 
     2.3, 3.0, 3.2459, -23.21
 
-For the most part, Python will convert from integer to floating point numbers for you.
+Note tht while the integer`3` and the float `3.0` have the same value, they are different types of numbers. But for the most part, Python will convert from integer to floating point numbers for you, so this distiction is rarely important.
 
 Math
 ----
 
-Being a computer language, python, of course supports the regular math functions. type the following into a file named math.py and run it:
+Being a computer language, Python, of course, supports the regular math functions. Type the following into a file named math.py and run it:
 
 .. code-block:: python
 
@@ -204,12 +205,12 @@ Being a computer language, python, of course supports the regular math functions
   print("twelve divided by 5 is:")
   print(12 // 5)
 
-What is the difference between ``12 / 5`` and ``12 // 5`` ?
+What is the difference between ``12 / 5`` and ``12 // 5`` ? Run your this code and find out.
 
-Order of operations
+Order of Operations
 -------------------
 
-Python follows the standard rules of operator precedence -- which operations are performed first when there are a bunch in a row:
+Python follows the standard rules of "operator precedence" from algebra -- which operations are performed first when there are a bunch in a row:
 
 https://en.wikipedia.org/wiki/Order_of_operations
 
@@ -237,7 +238,7 @@ Variables
 
 Directly printing things is not all that useful -- though Python does make a good calculator!
 
-Do do anything more complicated, you need to store values to be used later. We do this by "assigning" them to a variable. SAve the follwing in a variables.py file:
+To do anything more complicated, you need to store values to be used later. We do this by "assigning" them to a "variable", essentially givng them a name. Save the follwing in a ``variables.py`` file:
 
 .. code-block:: python
 
@@ -249,7 +250,7 @@ Do do anything more complicated, you need to store values to be used later. We d
 
 The equals sign: ``=`` is the "assignment operator". It assigns a value to a name, and then when you use the name in the future, Python will replace it with the value it is assigned to when it is used.
 
-names can (and generally should) be long and descriptive, and can contain letters, numbers (but not at the beginning) and some symbols, like the underscore character:
+Names can (and generally should) be long and descriptive, and can contain letters, numbers (but not at the beginning) and only a few symbols, like the underscore character:
 
 .. code-block:: python
 
@@ -289,7 +290,14 @@ And this?
     print ("that")  # I think we need this line too
     print("the other")
 
-comments can come after running code on a line.
+comments can come after running code on a line as well. USing the hash to "comment out" parts of code is used in two ways:
+
+1) To add a little extra description to some code, to explain what it doing.
+
+2) To temporarily disable some code
+
+
+
 
 
 
From 3b2c6223f4e73770d603738475d372499f3ed00b Mon Sep 17 00:00:00 2001
From: Natasha 
Date: Sun, 31 May 2020 18:55:14 -0700
Subject: [PATCH 87/87] OO intro assignment (#189)
* oo intro
* oo intro
---
 source/exercises/oo_intro.py  | 103 ++++++++++++++++++++++++++++++++++
 source/exercises/oo_intro.rst |  30 ++++++++++
 2 files changed, 133 insertions(+)
 create mode 100644 source/exercises/oo_intro.py
 create mode 100644 source/exercises/oo_intro.rst
diff --git a/source/exercises/oo_intro.py b/source/exercises/oo_intro.py
new file mode 100644
index 00000000..522e62a0
--- /dev/null
+++ b/source/exercises/oo_intro.py
@@ -0,0 +1,103 @@
+import math
+import operator
+from uuid import uuid4
+
+
+class Row:
+    """This class represents a single row with ID, first name, last name and state attributes"""
+    def __init__(self, fname: str, lname: str, state: str):
+        self.row_id = str(uuid4())  # randomly generated unique ID
+        self.fname = fname
+        self.lname = lname
+        self.state = state
+
+    def __str__(self):
+        return f"| {self.row_id} | {self.fname + ' ' + self.lname:<15} | {self.state} |"
+
+
+class Report:
+    def __init__(self, limit: int):
+        self.limit = limit
+        self.rows = []
+
+    def add_row(self, row: Row) -> None:
+        """Add a row object to the list"""
+        pass
+
+    def remove_row(self, row_id: str) -> None:
+        """Remove a row object by the row ID"""
+        pass
+
+    def size(self) -> int:
+        """Return how many total rows the report has"""
+        pass
+
+    def get_number_of_pages(self) -> int:
+        """Get how many pages the report has; this will be based on limit variable.
+        If your limit=4 and rows list has 6 records then there are two pages: page1 has 4 records, page2 has 2 records
+        hint: you'll want to round up
+        """
+        pass
+
+    def get_paged_rows(self, sort_field: str, page: int) -> list:
+        """Return a list of rows for a specific page number
+        :param sort_field:  field to sort on, "name" or "-name" (descending)
+        :param page:        specific page for returning data
+        :return:            list of row objects for specific page
+
+        Hints:
+        1. you'll want to determine if sort is reversed or not (remember that sorted() takes in param for that)
+            this is based on if the fields start with a minus sign for DESCENDING sort
+        2. when sorting on passed in field you can use handy `operator` library with `attrgetter` method (look up official docs)
+        3. to actually determine what rows belong on the specific page you'll be using list slicing (remember lesson 03?)
+            here is an illustration to help with the code logic:
+                our list has 6 rows => [, , , , , ]
+                for page=2 we expect to get => [, ]
+                with slicing you'll want to offset your list by 4 in this case
+                (extra hint: we can define offset as `offset = (page - 1) * self.limit`)
+
+        """
+        pass
+
+
+if __name__ == "__main__":
+
+    report = Report(4)
+
+    report.add_row(Row("natasha", "smith", "WA"))
+    report.add_row(Row("devin", "lei", "WA"))
+    report.add_row(Row("bob", "li", "CA"))
+    report.add_row(Row("tracy", "jones", "OR"))
+    report.add_row(Row("johny", "jakes", "WA"))
+    report.add_row(Row("derek", "wright", "WA"))
+
+
+    def run_report(sort_field):
+        print(f"... PAGED REPORT SORTED BY: '{sort_field}'...")
+        page = 1
+        while True:
+            rows = report.get_paged_rows(sort_field, page=page)
+
+            if not rows:
+                break
+
+            input(f"Press ENTER to see page {page}")
+
+            print(f"PAGE: {page} of {report.get_number_of_pages()}")
+            print("---------------------------------------------------------------")
+
+            for row in rows:
+                print(row)
+
+            print("---------------------------------------------------------------")
+
+            page += 1
+
+
+    run_report("fname")
+
+    print(f"\n\nRemoving student: {report.rows[1].fname} [{report.rows[1].row_id}]... \n\n")
+
+    report.remove_row(report.rows[1].row_id)
+
+    run_report("-fname")
diff --git a/source/exercises/oo_intro.rst b/source/exercises/oo_intro.rst
new file mode 100644
index 00000000..703b3df3
--- /dev/null
+++ b/source/exercises/oo_intro.rst
@@ -0,0 +1,30 @@
+.. _oo_intro:
+
+########################
+OO Intro - Report Class
+########################
+
+This assignment uses Object Oriented Programming to design a class that can be used to manage data reporting.
+
+We have done some reporting in our mailroom program, but that report was pretty simple and used simple functions to accomplish the work.
+
+We will explore here, how one can utilize OO to improve and enhance reporting capabilities.
+
+
+Procedure
+=========
+
+You will use :download:`oo_intro.py` file as a starting point for your code.
+
+You will notice that ``Report`` class will have attributes and methods defined for you, including input parameters (and their types) as well as expected output. You will need to fill out the code for each defined method and docstrings contain additional information on what is expected.
+
+The ``Report`` class uses another class that is fully defined for you, the ``Row`` class. This class represents a single row in your report, and the report class will hold a list of the row instances.
+There are big advantages to using a class like ``Row`` in contrast to a simple dictionary, this design creates a clear contract on what's expected to be as part of a row, where with a dictionary it is easy to misspell or miss a key.
+A class like ``Row`` is often called a dataclass (since it only holds "data" and doesn't actually have any logic to it).
+
+This is such a popular pattern that python introduced dataclasses to make this even simpler and you can read more about them here:
+https://realpython.com/python-data-classes/. You do not use them in this assignment but you should know that they exist and why.
+
+
+
+And at last, you will of course want to include unit tests covering all of your class methods.
\ No newline at end of file