diff --git a/index.rst b/index.rst
index 1ebd96aa078d751fc328e07fbff72fbcddfee42f..60b5a1d73a89b06270eb2efed6a2d9aa04cc5977 100644
--- a/index.rst
+++ b/index.rst
@@ -27,7 +27,7 @@ Lessons Overview
    lessons/07-reading-tabular.rst
    lessons/08-data-frames.rst
    lessons/09-plotting.rst
-   lessons/14-looping-data-sets.rst
+   lessons/14-custom-lesson.rst
    lessons/15-coffee.rst
    lessons/16-writing-functions.rst
    lessons/17-scope.rst
diff --git a/lessons/01-run-quit.rst b/lessons/01-run-quit.rst
index d1bca096628e93a9305e13188f36654155eaeaa7..ae9f026a2ce1211f03cfcdf786b503aa9e84d562 100644
--- a/lessons/01-run-quit.rst
+++ b/lessons/01-run-quit.rst
@@ -468,6 +468,7 @@ Or use [named links][data_carpentry].
   For example, what happens when this cell is executed?
 
   .. code-block:: python
+    :linenos:
 
     7 * 3
     2 + 1
@@ -494,6 +495,7 @@ Or use [named links][data_carpentry].
   For example, put the following in a code cell:
 
   .. code-block:: python
+    :linenos:
 
     x = 6 * 7 + 12
     print(x)
diff --git a/lessons/02-variables.rst b/lessons/02-variables.rst
index 3bc95da10164ac39f3d1c575fe113315cfe36f65..0a9255664221b53ba1894386a6114387d091a912 100644
--- a/lessons/02-variables.rst
+++ b/lessons/02-variables.rst
@@ -34,6 +34,7 @@ Use variables to store values.
 - Here, Python assigns an age to a variable `age` and a name in quotes to a variable `first_name`.
 
   .. code-block:: python
+    :linenos:
 
     age = 42
     first_name = 'Ahmed'
@@ -49,6 +50,7 @@ Use variables to store values.
 - The values passed to the function are called **arguments**
 
 .. code-block:: python
+  :linenos:
 
   print(first_name, 'is', age, 'years old')
 
@@ -69,6 +71,7 @@ Variable lifetime
   Python reports an error. (Unlike some languages, which "guess" a default value.)
 
 .. code-block:: python
+  :linenos:
 
   print(last_name)
 
@@ -94,11 +97,13 @@ Variable lifetime
   following content, in this order:
 
   .. code-block:: python
+    :linenos:
 
     print(myval)
 
 
   .. code-block:: python
+    :linenos:
 
     myval = 1
 
@@ -118,6 +123,7 @@ Using variables
   - Remember, we assigned the value `42` to `age` a few lines ago.
 
 .. code-block:: python
+  :linenos:
 
   age = age + 3
   print('Age in three years:', age)
@@ -145,6 +151,7 @@ Indexing
   :alt: A line of Python code, print(atom\_name[0]), demonstrates that using the zero index will output just the initial letter, in this case 'h' for helium.
 
 .. code-block:: python
+  :linenos:
 
   atom_name = 'helium'
   print(atom_name[0])
@@ -171,6 +178,7 @@ Slicing
   taking a slice returns a copy of part of the original string.
 
 .. code-block:: python
+  :linenos:
 
   atom_name = 'sodium'
   print(atom_name[0:3])
@@ -187,6 +195,7 @@ Slicing
 - Use the built-in function `len` to find the length of a string.
 
 .. code-block:: python
+  :linenos:
 
   print(len('helium'))
 
@@ -225,6 +234,7 @@ Naming Convention
 - Python doesn't care what you call variables as long as they obey the rules (alphanumeric characters and the underscore).
 
 .. code-block:: python
+  :linenos:
 
   flabadab = 42
   ewr_422_yY = 'Ahmed'
@@ -266,6 +276,7 @@ Naming Convention
   *after* each statement is executed.
 
   .. code-block:: python
+    :linenos:
 
     # Command  # Value of x   # Value of y   # Value of swap #
     x = 1.0    #              #              #               #
@@ -306,6 +317,7 @@ Naming Convention
   then check your prediction.)
 
   .. code-block:: python
+    :linenos:
 
     initial = 'left'
     position = initial
@@ -318,6 +330,7 @@ Naming Convention
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     print(position)
 
@@ -355,6 +368,7 @@ Naming Convention
   can convert it into a string using the `str` built-in function and then perform an index operation on that string.
 
   .. code-block:: python
+    :linenos:
 
     a = 123
     print(a[1])
@@ -366,6 +380,7 @@ Naming Convention
 
 
   .. code-block:: python
+    :linenos:
 
     a = str(123)
     print(a[1])
@@ -385,6 +400,7 @@ Naming Convention
   What does the following program print?
 
   .. code-block:: python
+    :linenos:
 
     atom_name = 'carbon'
     print('atom_name[1:3] is:', atom_name[1:3])
@@ -409,6 +425,7 @@ Naming Convention
   Given the following string:
 
   .. code-block:: python
+    :linenos:
 
     species_name = "Acacia buxifolia"
 
diff --git a/lessons/03-types-conversion.rst b/lessons/03-types-conversion.rst
index c2b6114f7222dcd0b9036372743ec5fef82beaa5..fc395fa66cf66fd7cdd2a117759478d01f550927 100644
--- a/lessons/03-types-conversion.rst
+++ b/lessons/03-types-conversion.rst
@@ -22,7 +22,7 @@ Exercises: 10 min
 -------------------
 
 Types
------
+=====
 
 - Every value in a program has a specific type.
 - Integer (`int`): represents positive or negative whole numbers like 3 or -512.
@@ -39,6 +39,7 @@ Types
   - But remember: the *value* has the type --- the *variable* is just a label.
 
 .. code-block:: python
+  :linenos:
 
   print(type(52))
 
@@ -47,6 +48,7 @@ Types
   <class 'int'>
 
 .. code-block:: python
+  :linenos:
 
   fitness = 'average'
   print(type(fitness))
@@ -70,6 +72,7 @@ Types
   It is possible to find out by using the built-in function `type()`.
 
   .. code-block:: python
+    :linenos:
 
     print(type(3.4))
 
@@ -89,6 +92,7 @@ Types and Operations
 - Types control what operations (or methods) can be performed on a given value.
 
 .. code-block:: python
+  :linenos:
 
   print(5 - 3)
 
@@ -97,6 +101,7 @@ Types and Operations
   2
 
 .. code-block:: python
+  :linenos:
 
   print('hello' - 'h')
 
@@ -116,6 +121,7 @@ String Operations
 - "Adding" character strings concatenates them.
 
 .. code-block:: python
+  :linenos:
 
   full_name = 'Ahmed' + ' ' + 'Walsh'
   print(full_name)
@@ -128,6 +134,7 @@ String Operations
   - Since multiplication is repeated addition.
 
 .. code-block:: python
+  :linenos:
 
   separator = '=' * 10
   print(separator)
@@ -143,6 +150,7 @@ String Operations
 - The built-in function `len` counts the number of characters in a string.
 
 .. code-block:: python
+  :linenos:
 
   print(len(full_name))
 
@@ -153,6 +161,7 @@ String Operations
 - But numbers don't have a length (not even zero).
 
 .. code-block:: python
+  :linenos:
 
   print(len(52))
 
@@ -172,6 +181,7 @@ Strings and Numbers
 - Cannot add numbers and strings.
 
 .. code-block:: python
+  :linenos:
 
   print(1 + '2')
 
@@ -188,6 +198,7 @@ Strings and Numbers
 - Some types can be converted to other types by using the type name as a function.
 
 .. code-block:: python
+  :linenos:
 
   print(1 + int('2'))
   print(str(1) + '2')
@@ -204,6 +215,7 @@ Integer and Float
   - Python 3 automatically converts integers to floats as needed.
 
 .. code-block:: python
+  :linenos:
 
   print('half is', 1 / 2.0)
   print('three squared is', 3.0 ** 2)
@@ -227,6 +239,7 @@ Integer and Float
   integers are automatically converted to floats as necessary.
 
   .. code-block:: python
+    :linenos:
 
     result = 3.25 + 4
     print(result, 'is', type(result))
@@ -251,6 +264,7 @@ Variable Values
 - This does **not** happen in programming languages.
 
 .. code-block:: python
+  :linenos:
 
   variable_one = 1
   variable_two = 5 * variable_one
@@ -299,12 +313,146 @@ Variable Values
 
 -------------------
 
+Built-in Data Types
+===================
+
+Many data types are supported by a basic Python installation. They can hold text, single values or many different values. Here are some typically used data types. More can be found in the `Python Documentation <https://docs.python.org/3/library/stdtypes.html#>`__.
+
+Non-Iterables
+-------------
+
+**Boolean Types**
+
+.. code-block:: python
+
+  x = True  # bool -> For yes/no type data
+
+**Numeric Types**
+
+.. code-block:: python
+
+  x = 20  # int -> For Integer numbers
+  x = 20.5  # float -> For floating point numbers
+  x = 4 + 1j  # complex -> For complex numbers
+
+Iterables
+---------
+
+**Text Sequence Types**
+
+.. code-block:: python
+
+  x = "Hello World"  # str -> For text and characters
+
+**Sequence Types**
+
+.. code-block:: python
+
+  x = ["apple", 2, "cherry"]  # list -> For a list of values that can be changed
+  x = ("apple", "banana", 3)  # tuple -> For a set of values that can not be changed
+  x = range(6)  # range -> A `generator` that dynamically generates values each time it is iterated
+
+**Mapping Type**
+
+.. code-block:: python
+
+  x = {"name" : "John", "age" : 36}  # dict -> For a named set of keyword-value pairs
+
+
+**Binary Sequence Types**
+
+.. code-block:: python
+
+  x = b"Hello"  # bytes -> Binary string (hexadecimal string '48656c6c6f')
+  x = bytearray([5, 3, 7])  # bytearray -> Binary list (hexadecimal integers '050307')
+
+Operations
+==========
+
+Depending on the type of variable, they can be used to perform calculations or do other operations. Here are some commonly used operations for the data types.
+
+Boolean Operations
+------------------
+
++-----------+--------------------------------------------+
+| Operation | Result                                     |
++===========+============================================+
+| `x or y`  | if `x` is true, then `x`, else `y`         |
++-----------+--------------------------------------------+
+| `x and y` | if `x` is false, then `x`, else `y`        |
++-----------+--------------------------------------------+
+| `not x`   | if `x` is false, then `True`, else `False` |
++-----------+--------------------------------------------+
+
+Comparisons
+-----------
+
++-----------+-------------------------+
+| Operation | Result                  |
++===========+=========================+
+| `<`       | strictly less than      |
++-----------+-------------------------+
+| `<=`      | less than or equal      |
++-----------+-------------------------+
+| `>`       | strictly greater than   |
++-----------+-------------------------+
+| `>=`      | greater than or equal   |
++-----------+-------------------------+
+| `==`      | equal                   |
++-----------+-------------------------+
+| `!=`      | not equal               |
++-----------+-------------------------+
+| `is`      | object identity         |
++-----------+-------------------------+
+| `is not`  | negated object identity |
++-----------+-------------------------+
+
+Numeric Operations
+------------------
+
++----------------+----------------------------------------------------------------------------+
+| Operation      | Result                                                                     |
++================+============================================================================+
+| x + y          | sum of x and y                                                             |
++----------------+----------------------------------------------------------------------------+
+| x - y          | difference of x and y                                                      |
++----------------+----------------------------------------------------------------------------+
+| x * y          | product of x and y                                                         |
++----------------+----------------------------------------------------------------------------+
+| x / y          | quotient of x and y                                                        |
++----------------+----------------------------------------------------------------------------+
+| x // y         | floored quotient of x and y                                                |
++----------------+----------------------------------------------------------------------------+
+| x % y          | remainder of x / y                                                         |
++----------------+----------------------------------------------------------------------------+
+| -x             | x negated                                                                  |
++----------------+----------------------------------------------------------------------------+
+| +x             | x unchanged                                                                |
++----------------+----------------------------------------------------------------------------+
+| x ** y         | x to the power y                                                           |
++----------------+----------------------------------------------------------------------------+
+| abs(x)         | absolute value or magnitude of x                                           |
++----------------+----------------------------------------------------------------------------+
+| int(x)         | x converted to integer                                                     |
++----------------+----------------------------------------------------------------------------+
+| float(x)       | x converted to floating point                                              |
++----------------+----------------------------------------------------------------------------+
+| complex(re, im)| a complex number with real part re, imaginary part im. im defaults to zero.|
++----------------+----------------------------------------------------------------------------+
+| divmod(x, y)   | the pair (x // y, x % y)                                                   |
++----------------+----------------------------------------------------------------------------+
+| pow(x, y)      | x to the power y                                                           |
++----------------+----------------------------------------------------------------------------+
+
+-------------------
+
+
 .. attention:: **Division Types**
 
-  In Python 3, the `//` operator performs integer (whole-number) floor division, the `/` operator performs floating-point
-  division, and the `%` (or *modulo*) operator calculates and returns the remainder from integer division:
+  In Python 3, the `//` operator performs integer (whole-number) floor division, the `/` operator performs floating-point division, and the `%` (or *modulo*) operator calculates and returns the remainder from integer division:
 
   .. code-block:: python
+    :linenos:
 
     print('5 // 3:', 5 // 3)
     print('5 / 3:', 5 / 3)
@@ -316,23 +464,17 @@ Variable Values
     5 / 3: 1.6666666666666667
     5 % 3: 2
 
-  If `num_subjects` is the number of subjects taking part in a study,
-  and `num_per_survey` is the number that can take part in a single survey,
-  write an expression that calculates the number of surveys needed
-  to reach everyone once.
+  If `num_subjects` is the number of subjects taking part in a study, and `num_per_survey` is the number that can take part in a single survey, write an expression that calculates the number of surveys needed to reach everyone once.
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-  We want the minimum number of surveys that reaches everyone once, which is
-  the rounded up value of `num_subjects/ num_per_survey`. This is
-  equivalent to performing a floor division with `//` and adding 1. Before
-  the division we need to subtract 1 from the number of subjects to deal with
-  the case where `num_subjects` is evenly divisible by `num_per_survey`.
+  We want the minimum number of surveys that reaches everyone once, which is the rounded up value of `num_subjects/ num_per_survey`. This is equivalent to performing a floor division with `//` and adding 1. Before the division we need to subtract 1 from the number of subjects to deal with the case where `num_subjects` is evenly divisible by `num_per_survey`.
 
   .. code-block:: python
+    :linenos:
 
     num_subjects = 600
     num_per_survey = 42
@@ -356,6 +498,7 @@ Variable Values
   and `int()` will convert a floating point number to an integer:
 
   .. code-block:: python
+    :linenos:
 
     print("string to float:", float("3.4"))
     print("float to int:", int(3.4))
@@ -368,6 +511,7 @@ Variable Values
   If the conversion doesn't make sense, however, an error message will occur.
 
   .. code-block:: python
+    :linenos:
 
     print("string to float:", float("Hello world!"))
 
@@ -385,6 +529,7 @@ Variable Values
   - Why do you think it does that?
 
   .. code-block:: python
+    :linenos:
 
     print("fractional string to int:", int("3.4"))
 
@@ -398,6 +543,7 @@ Variable Values
   magic - isn't that part of its charm?
 
   .. code-block:: python
+    :linenos:
 
     int("3.4")
 
@@ -414,6 +560,7 @@ Variable Values
   typecasts, you must convert it explicitly in code.
 
   .. code-block:: python
+    :linenos:
 
     int(float("3.4"))
 
@@ -433,6 +580,7 @@ Variable Values
   Note: there may be more than one right answer.
 
   .. code-block:: python
+    :linenos:
 
     first = 1.0
     second = "1"
@@ -467,6 +615,7 @@ Variable Values
   as `val.real` and `val.imag`.
 
   .. code-block:: python
+    :linenos:
 
     a_complex_number = 6 + 2j
     print(a_complex_number.real)
diff --git a/lessons/04-built-in.rst b/lessons/04-built-in.rst
index db2de75edae72d47bf9e69758fce3f0301d04fd6..d0947160432a2ef3236786bbad6c621391b444f7 100644
--- a/lessons/04-built-in.rst
+++ b/lessons/04-built-in.rst
@@ -28,6 +28,7 @@ Comments
 - Use comments to add documentation to programs.
 
 .. code-block:: python
+  :linenos:
 
   # This sentence isn't executed by Python.
   adjustment = 0.5   # Neither is this - anything after '#' is ignored.
@@ -45,6 +46,7 @@ Function Arguments
   - Must always use parentheses, even if they're empty, so that Python knows a function is being called.
 
 .. code-block:: python
+  :linenos:
 
   print('before')
   print()
@@ -63,6 +65,7 @@ Function Returns
 - If the function doesn't have a useful result to return, it usually returns the special value `None`. `None` is a Python object that stands in anytime there is no value.
 
 .. code-block:: python
+  :linenos:
 
   result = print('example')
   print('result of print is', result)
@@ -82,6 +85,7 @@ Common Functions
   - "Larger" and "smaller" use (0-9, A-Z, a-z) to compare letters.
 
 .. code-block:: python
+  :linenos:
 
   print(max(1, 2, 3))
   print(min('a', 'A', '0'))
@@ -97,6 +101,7 @@ Common Functions
   2. What is the final value of `radiance`?
 
   .. code-block:: python
+    :linenos:
 
     radiance = 1.0
     radiance = max(2.1, 2.0 + min(radiance, 1.1 * radiance - 0.5))
@@ -107,11 +112,13 @@ Common Functions
       <summary markdown="span"><b>Solution</b></summary>Solution
 
   1. Order of operations:
+
     1. `1.1 * radiance = 1.1`
     2. `1.1 - 0.5 = 0.6`
     3. `min(radiance, 0.6) = 0.6`
     4. `2.0 + 0.6 = 2.6`
     5. `max(2.1, 2.6) = 2.6`
+
   2. At the end, `radiance = 2.6`
 
   .. raw:: html
@@ -129,6 +136,7 @@ More Function Arguments
 - And they must be given things that can meaningfully be compared.
 
 .. code-block:: python
+  :linenos:
 
   print(max(1, 'a'))
 
@@ -148,6 +156,7 @@ Default Arguments
 - By default, rounds to zero decimal places.
 
 .. code-block:: python
+  :linenos:
 
   round(3.712)
 
@@ -158,6 +167,7 @@ Default Arguments
 - We can specify the number of decimal places we want.
 
 .. code-block:: python
+  :linenos:
 
   round(3.712, 1)
 
@@ -174,6 +184,7 @@ Methods
 - Some methods are used for internal Python operations, and are marked with double underlines.
 
 .. code-block:: python
+  :linenos:
 
   my_string = 'Hello world!'  # creation of a string object
 
@@ -193,6 +204,7 @@ Methods
 - You might even see them chained together.  They operate left to right.
 
 .. code-block:: python
+  :linenos:
 
   print(my_string.isupper())          # Not all the letters are uppercase
   print(my_string.upper())            # This capitalizes all the letters
@@ -212,6 +224,7 @@ Methods
 - Every built-in function has online documentation.
 
 .. code-block:: python
+  :linenos:
 
   help(round)
 
@@ -244,6 +257,7 @@ Syntax Errors
 - Won't even try to run the program if it can't be parsed.
 
 .. code-block:: python
+  :linenos:
 
   # Forgot to close the quote marks around the string.
   name = 'Feng
@@ -256,6 +270,7 @@ Syntax Errors
   SyntaxError: EOL while scanning string literal
 
 .. code-block:: python
+  :linenos:
 
   # An extra '=' in the assignment.
   age = = 52
@@ -270,6 +285,7 @@ Syntax Errors
 - Look more closely at the error message:
 
 .. code-block:: python
+  :linenos:
 
   print("hello world"
 
@@ -291,6 +307,7 @@ Runtime Errors
 - Python reports a runtime error when something goes wrong while a program is executing.
 
 .. code-block:: python
+  :linenos:
 
   age = 53
   remaining = 100 - aege # mis-spelled 'age'
@@ -312,6 +329,7 @@ Runtime Errors
   2. Does `max(len(rich), poor)` run or produce an error message? If it runs, does its result make any sense?
 
   .. code-block:: python
+    :linenos:
 
     easy_string = "abc"
     print(max(easy_string))
@@ -327,6 +345,7 @@ Runtime Errors
 
 
   .. code-block:: python
+    :linenos:
 
     print(max(easy_string))
 
@@ -335,6 +354,7 @@ Runtime Errors
     c
 
   .. code-block:: python
+    :linenos:
 
     print(max(rich, poor))
 
@@ -343,6 +363,7 @@ Runtime Errors
     tin
 
   .. code-block:: python
+    :linenos:
 
     print(max(len(rich), len(poor)))
 
diff --git a/lessons/05-coffee.rst b/lessons/05-coffee.rst
index 502198918b9cf4c79cb8993730048a0a0186086d..9dd6841946a70fa535b56116f5648193981a0231 100644
--- a/lessons/05-coffee.rst
+++ b/lessons/05-coffee.rst
@@ -5,7 +5,7 @@ Coffee Break
 Break: 15 min
 
 Reflection exercise
--------------
+-------------------
 
 Over coffee, reflect on and discuss the following:
 
diff --git a/lessons/06-libraries.rst b/lessons/06-libraries.rst
index e4bd1cf74c5b01b7563a3eac7ab44eb5059a67b2..c628c13e69b9ac284e6562a8644aedcfad4488d6 100644
--- a/lessons/06-libraries.rst
+++ b/lessons/06-libraries.rst
@@ -51,6 +51,7 @@ Libraries Basics
 - Using `math`, one of the modules in the standard library:
 
 .. code-block:: python
+  :linenos:
 
   import math
 
@@ -74,6 +75,7 @@ Libraries Basics
 - Works just like help for a function.
 
 .. code-block:: python
+  :linenos:
 
   help(math)
 
@@ -110,6 +112,7 @@ Libraries Basics
 - Then refer to them directly without library name as prefix.
 
 .. code-block:: python
+  :linenos:
 
   from math import cos, pi
 
@@ -127,6 +130,7 @@ Import Aliases
 - Then refer to items in the library using that shortened name.
 
 .. code-block:: python
+  :linenos:
 
   import math as m
 
@@ -169,6 +173,7 @@ Import Aliases
   You want to select a random character from a string:
 
   .. code-block:: python
+    :linenos:
 
     bases = 'ACTTGCTTGAC'
 
@@ -189,6 +194,7 @@ Import Aliases
   to get a random integer between 0 and 10, and then select the `bases` character at that index:
 
   .. code-block:: python
+    :linenos:
 
     from random import randrange
 
@@ -198,6 +204,7 @@ Import Aliases
   or more compactly:
 
   .. code-block:: python
+    :linenos:
 
     from random import randrange
 
@@ -206,6 +213,7 @@ Import Aliases
   Perhaps you found the `random.sample <https://docs.python.org/3/library/random.html#random.sample>`__ function? It allows for slightly less typing but might be a bit harder to understand just by reading:
 
   .. code-block:: python
+    :linenos:
 
     from random import sample
 
@@ -216,6 +224,7 @@ Import Aliases
   The simplest and shortest solution is the `random.choice <https://docs.python.org/3/library/random.html#random.choice>`__ function that does exactly what we want:
 
   .. code-block:: python
+    :linenos:
 
     from random import choice
 
@@ -232,6 +241,7 @@ Import Aliases
   Rearrange the following statements so that a random DNA base is printed and  its index in the string. Not all statements may be needed.  Feel free to use/add intermediate variables.
 
   .. code-block:: python
+    :linenos:
 
     bases="ACTTGCTTGAC"
     import math
@@ -246,6 +256,7 @@ Import Aliases
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     import math
     import random
@@ -288,6 +299,7 @@ Import Aliases
   3. Which form do you find easier to read?
 
   .. code-block:: python
+    :linenos:
 
     import math as m
     angle = ____.degrees(____.pi / 2)
@@ -299,6 +311,7 @@ Import Aliases
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     import math as m
     angle = m.degrees(m.pi / 2)
@@ -307,6 +320,7 @@ Import Aliases
   can be written as
 
   .. code-block:: python
+    :linenos:
 
     import math
     angle = math.degrees(math.pi / 2)
@@ -348,7 +362,7 @@ Import Aliases
 
   .. note::
 
-  Although library call 4 works, importing all names from a module using a wildcard import is `not recommended <https://pep8.org/#imports>`__ as it makes it unclear which names from the module are used in the code. In general it is best to make your imports as specific as possible and to only import what your code uses. In library call 1, the `import` statement explicitly tells us that the `sin` function is imported from the `math` module, but library call 4 does not convey this information.
+    Although library call 4 works, importing all names from a module using a wildcard import is `not recommended <https://pep8.org/#imports>`__ as it makes it unclear which names from the module are used in the code. In general it is best to make your imports as specific as possible and to only import what your code uses. In library call 1, the `import` statement explicitly tells us that the `sin` function is imported from the `math` module, but library call 4 does not convey this information.
 
   .. raw:: html
 
@@ -363,6 +377,7 @@ Import Aliases
   3. Why *wouldn't* programmers always use this form of `import`?
 
   .. code-block:: python
+    :linenos:
 
     ____ math import ____, ____
     angle = degrees(pi / 2)
@@ -374,6 +389,7 @@ Import Aliases
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     from math import degrees, pi
     angle = degrees(pi / 2)
@@ -393,6 +409,7 @@ Import Aliases
   2. Run the code, and read the error message. What type of error is it?
 
   .. code-block:: python
+    :linenos:
 
     from math import log
     log(0)
diff --git a/lessons/07-reading-tabular.rst b/lessons/07-reading-tabular.rst
index ea2d3d463e4c1d4c8801b9224b381727ab9edc43..d2f1511c012d95d88049360f37baf357112c2407 100644
--- a/lessons/07-reading-tabular.rst
+++ b/lessons/07-reading-tabular.rst
@@ -35,6 +35,7 @@ The `pandas` Library
   - Returns a dataframe that you can assign to a variable
 
 .. code-block:: python
+  :linenos:
 
   import pandas as pd
 
@@ -83,6 +84,7 @@ Indexing dataframes
 - Naming the dataframe `data_oceania_country` tells us which region the data includes (`oceania`) and how it is indexed (`country`).
 
 .. code-block:: python
+  :linenos:
 
   data_oceania_country = pd.read_csv('data/gapminder_gdp_oceania.csv', index_col='country')
   print(data_oceania_country)
@@ -110,6 +112,7 @@ Metadata of a dataframe
 - Use the `DataFrame.info()` method to find out more about a dataframe.
 
 .. code-block:: python
+  :linenos:
 
   data_oceania_country.info()
 
@@ -153,6 +156,7 @@ Accessing Columns
 - Called a *member variable*, or just *member*.
 
 .. code-block:: python
+  :linenos:
 
   print(data_oceania_country.columns)
 
@@ -173,6 +177,7 @@ Transposing Dataframes
 - Like `columns`, it is a member variable.
 
 .. code-block:: python
+  :linenos:
 
   print(data_oceania_country.T)
 
@@ -201,6 +206,7 @@ Basic Statistics
 `DataFrame.describe()` gets the summary statistics of only the columns that have numerical data. All other columns are ignored, unless you use the argument `include='all'`.
 
 .. code-block:: python
+  :linenos:
 
   print(data_oceania_country.describe())
 
@@ -250,6 +256,7 @@ Basic Statistics
   To read in a CSV, we use `pd.read_csv` and pass the filename `'data/gapminder_gdp_americas.csv'` to it. We also once again pass the column name `'country'` to the parameter `index_col` in order to index by country. The summary statistics can be displayed with the `DataFrame.describe()` method.
 
   .. code-block:: python
+    :linenos:
 
     data_americas = pd.read_csv('data/gapminder_gdp_americas.csv', index_col='country')
     data_americas.describe()
@@ -277,6 +284,7 @@ Basic Statistics
   1. We can check out the first five rows of `data_americas` by executing `data_americas.head()` which lets us view the beginning of the DataFrame. We can specify the number of rows we wish to see by specifying the parameter `n` in our call to `data_americas.head()`. To view the first three rows, execute:
 
   .. code-block:: python
+    :linenos:
 
     data_americas.head(n=3)
 
@@ -309,12 +317,14 @@ Basic Statistics
   2. To check out the last three rows of `data_americas`, we would use the command, `americas.tail(n=3)`, analogous to `head()` used above. However, here we want to look at the last three columns so we need to change our view and then use `tail()`. To do so, we create a new DataFrame in which rows and columns are switched:
 
   .. code-block:: python
+    :linenos:
 
     americas_flipped = data_americas.T
 
   We can then view the last three columns of `americas` by viewing the last three rows of `americas_flipped`:
 
   .. code-block:: python
+    :linenos:
 
     americas_flipped.tail(n=3)
 
@@ -344,12 +354,14 @@ Basic Statistics
     so we can flip it back:
 
   .. code-block:: python
+    :linenos:
 
     americas_flipped.tail(n=3).T
 
   .. note:: We could have done the above in a single line of code by 'chaining' the commands:
 
     .. code-block:: python
+      :linenos:
 
       data_americas.T.tail(n=3).T
 
@@ -381,6 +393,7 @@ Basic Statistics
   We need to specify the path to the file of interest in the call to `pd.read_csv`. We first need to 'jump' out of the folder `thesis` using '../' and then into the folder `field_data` using 'field\_data/'. Then we can specify the filename \`microbes.csv. The result is as follows:
 
   .. code-block:: python
+    :linenos:
 
     data_microbes = pd.read_csv('../field_data/microbes.csv')
 
@@ -402,12 +415,14 @@ Basic Statistics
   In order to write the DataFrame `data_americas` to a file called `processed.csv`, execute the following command:
 
   .. code-block:: python
+    :linenos:
 
     data_americas.to_csv('processed.csv')
 
   For help on `read_csv` or `to_csv`, you could execute, for example:
 
   .. code-block:: python
+    :linenos:
 
     help(data_americas.to_csv)
     help(pd.read_csv)
diff --git a/lessons/08-data-frames.rst b/lessons/08-data-frames.rst
index 80d14970565229b228c410b6befa28aa9502b92a..25100ddf012edc3af07cd7d8e2069f557b97bde9 100644
--- a/lessons/08-data-frames.rst
+++ b/lessons/08-data-frames.rst
@@ -43,6 +43,7 @@ Selecting by position
 - Can specify location by numerical index analogously to 2D version of character selection in strings.
 
 .. code-block:: python
+  :linenos:
 
   import pandas as pd
   data = pd.read_csv('data/gapminder_gdp_europe.csv', index_col='country')
@@ -59,6 +60,7 @@ Selecting by label
 - Can specify location by row and/or column name.
 
 .. code-block:: python
+  :linenos:
 
   print(data.loc["Albania", "gdpPercap_1952"])
 
@@ -73,6 +75,7 @@ Slicing Notation
 - Just like Python's usual slicing notation.
 
 .. code-block:: python
+  :linenos:
 
   print(data.loc["Albania", :])
 
@@ -95,6 +98,7 @@ Slicing Notation
 - Would get the same result printing `data.loc["Albania"]` (without a second index).
 
 .. code-block:: python
+  :linenos:
 
   print(data.loc[:, "gdpPercap_1952"])
 
@@ -119,6 +123,7 @@ Combining Slicing and Selecting
 - Select multiple columns or rows using `DataFrame.loc` and a named slice.
 
 .. code-block:: python
+  :linenos:
 
   print(data.loc['Italy':'Poland', 'gdpPercap_1962':'gdpPercap_1972'])
 
@@ -143,6 +148,7 @@ Working with the Results
 - E.g., calculate max of a slice.
 
 .. code-block:: python
+  :linenos:
 
   print(data.loc['Italy':'Poland', 'gdpPercap_1962':'gdpPercap_1972'].max())
 
@@ -154,6 +160,7 @@ Working with the Results
   dtype: float64
 
 .. code-block:: python
+  :linenos:
 
   print(data.loc['Italy':'Poland', 'gdpPercap_1962':'gdpPercap_1972'].min())
 
@@ -175,6 +182,7 @@ Conditionals in Selecting
 - Returns a similarly-shaped dataframe of `True` and `False`.
 
 .. code-block:: python
+  :linenos:
 
   # Use a subset of data to keep output readable.
   subset = data.loc['Italy':'Poland', 'gdpPercap_1962':'gdpPercap_1972']
@@ -210,6 +218,7 @@ Masking Data
 - A frame full of Booleans is sometimes called a *mask* because of how it can be used.
 
 .. code-block:: python
+  :linenos:
 
   mask = subset > 10000
   print(subset[mask])
@@ -228,6 +237,7 @@ Masking Data
 - Useful because NaNs are ignored by operations like max, min, average, etc.
 
 .. code-block:: python
+  :linenos:
 
   print(subset[subset > 10000].describe())
 
@@ -254,6 +264,7 @@ For instance, let's say we want to have a clearer view on how the European count
 2. We then estimate a *wealthy score* based on the historical (from 1962 to 2007) values, where we account how many times a country has participated in the groups of *lower* or *higher* GDP
 
 .. code-block:: python
+  :linenos:
 
   mask_higher = data > data.mean()
   wealth_score = mask_higher.aggregate('sum', axis=1) / len(data.columns)
@@ -297,6 +308,7 @@ For instance, let's say we want to have a clearer view on how the European count
 Finally, for each group in the `wealth_score` table, we sum their (financial) contribution across the years surveyed using chained methods:
 
 .. code-block:: python
+  :linenos:
 
   print(data.groupby(wealth_score).sum())
 
@@ -325,6 +337,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   Assume Pandas has been imported into your notebook and the Gapminder GDP data for Europe has been loaded:
 
   .. code-block:: python
+    :linenos:
 
     import pandas as pd
 
@@ -340,6 +353,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   The selection can be done by using the labels for both the row (`'Serbia'`) and the column (`'gdpPercap_2007'`):
 
   .. code-block:: python
+    :linenos:
 
     print(data_europe.loc['Serbia', 'gdpPercap_2007'])
 
@@ -361,6 +375,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   2. Based on this, what rule governs what is included (or not) in numerical slices and named slices in Pandas?
 
   .. code-block:: python
+    :linenos:
 
     print(data_europe.iloc[0:2, 0:2])
     print(data_europe.loc['Albania':'Belgium', 'gdpPercap_1952':'gdpPercap_1962'])
@@ -404,6 +419,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   Explain what each line in the following short program does: What is in `first`, `second`, etc.?
 
   .. code-block:: python
+    :linenos:
 
     first = pd.read_csv('data/gapminder_all.csv', index_col='country')
     second = first[first['continent'] == 'Americas']
@@ -419,12 +435,14 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   Let's go through this piece of code line by line.
 
   .. code-block:: python
+    :linenos:
 
     first = pd.read_csv('data/gapminder_all.csv', index_col='country')
 
   This line loads the dataset containing the GDP data from all countries into a dataframe called `first`. The `index_col='country'` parameter selects which column to use as the row labels in the dataframe.
 
   .. code-block:: python
+    :linenos:
 
     second = first[first['continent'] == 'Americas']
 
@@ -433,18 +451,21 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   .. hint:: First assign the expression to a variable
 
   .. code-block:: python
+    :linenos:
 
     third = second.drop('Puerto Rico')
 
   As the syntax suggests, this line drops the row from `second` where the label is 'Puerto Rico'. The resulting dataframe `third` has one row less than the original dataframe `second`.
 
   .. code-block:: python
+    :linenos:
 
     fourth = third.drop('continent', axis = 1)
 
   Again we apply the drop function, but in this case we are dropping not a row but a whole column. To accomplish this, we need to specify also the `axis` parameter (we want to drop the second column which has index 1).
 
   .. code-block:: python
+    :linenos:
 
     fourth.to_csv('result.csv')
 
@@ -461,6 +482,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   Explain in simple terms what `idxmin` and `idxmax` do in the short program below. When would you use these methods?
 
   .. code-block:: python
+    :linenos:
 
     data = pd.read_csv('data/gapminder_gdp_europe.csv', index_col='country')
     print(data.idxmin())
@@ -498,18 +520,21 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   1:
 
   .. code-block:: python
+    :linenos:
 
     data['gdpPercap_1982']
 
   2:
 
   .. code-block:: python
+    :linenos:
 
     data.loc['Denmark',:]
 
   3:
 
   .. code-block:: python
+    :linenos:
 
     data.loc[:,'gdpPercap_1985':]
 
@@ -518,6 +543,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   4:
 
   .. code-block:: python
+    :linenos:
 
     data['gdpPercap_2007']/data['gdpPercap_1952']
 
@@ -551,6 +577,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   1. Access a single column:
 
   .. code-block:: python
+    :linenos:
 
     # by name
     data["col_name"]   # as a Series
@@ -573,6 +600,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   2. Access a single row:
 
   .. code-block:: python
+    :linenos:
 
     # by name using .loc
     data.loc["row_name"] # as a Series
@@ -592,6 +620,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   3. Access an individual DataFrame element:
 
   .. code-block:: python
+    :linenos:
 
     # by column/row names
     data["column_name"]["row_name"]         # as a Series
@@ -632,6 +661,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   4. Access several columns:
 
   .. code-block:: python
+    :linenos:
 
     # by name
     data[["col1", "col2", "col3"]]
@@ -643,6 +673,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   5. Access several rows
 
   .. code-block:: python
+    :linenos:
 
     # by name
     data.loc[["row1", "row2", "row3"]]
@@ -653,6 +684,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   6. Access a subset of specific rows and columns
 
   .. code-block:: python
+    :linenos:
 
     # by names
     data.loc[["row1", "row2", "row3"], ["col1", "col2", "col3"]]
@@ -669,6 +701,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   7. Access a subset of row and column ranges
 
   .. code-block:: python
+    :linenos:
 
     # by name
     data.loc["row1":"row2", "col1":"col2"]
@@ -693,6 +726,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   Python includes a `dir()` function that can be used to display all of the available methods (functions) that are built into a data object.  In Episode 4, we used some methods with a string. But we can see many more are available by using `dir()`:
 
   .. code-block:: python
+    :linenos:
 
     my_string = 'Hello world!'   # creation of a string object
     dir(my_string)
@@ -700,6 +734,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   This command returns:
 
   .. code-block:: python
+    :linenos:
 
     ['__add__',
     ...
@@ -723,6 +758,7 @@ Finally, for each group in the `wealth_score` table, we sum their (financial) co
   Among many choices, `dir()` lists the `median()` function as a possibility.  Thus,
 
   .. code-block:: python
+    :linenos:
 
     data.median()
 
diff --git a/lessons/09-plotting.rst b/lessons/09-plotting.rst
index e5ac80899a4425efcdde9e406c504a3650b329ee..79e7a200f727a200decec1e0800904a2e6021fa5 100644
--- a/lessons/09-plotting.rst
+++ b/lessons/09-plotting.rst
@@ -28,6 +28,7 @@ The `matplotlib` library
 - The Jupyter Notebook will render plots inline by default.
 
 .. code-block:: python
+  :linenos:
 
   import matplotlib.pyplot as plt
 
@@ -40,6 +41,7 @@ The `matplotlib` library
 - When creating multiple plots the object oriented method is more transparent because it is clear where things are plot.
 
 .. code-block:: python
+  :linenos:
 
   # Create some data
   time = [0, 1, 2, 3]
@@ -66,6 +68,7 @@ The `matplotlib` library
   Instruct `matplotlib` to show a figure:
 
   .. code-block:: python
+    :linenos:
 
     plt.show()
 
@@ -80,6 +83,7 @@ Plotting with `pandas`
 - Before plotting, we convert the column headings from a `string` to `integer` data type, since they represent numerical values, using `str.replace() <https://pandas.pydata.org/docs/reference/api/pandas.Series.str.replace.html>`__ to remove the `gpdPercap_` prefix and then `astype(int) <https://pandas.pydata.org/docs/reference/api/pandas.Series.astype.html>`__ to convert the series of string values (`['1952', '1957', ..., '2007']`) to a series of integers: `[1925, 1957, ..., 2007]`.
 
 .. code-block:: python
+  :linenos:
 
   import pandas as pd
 
@@ -109,6 +113,7 @@ Select and Transform Data
 - We can transpose the data in order to plot multiple series.
 
 .. code-block:: python
+  :linenos:
 
   data.T.plot()
   plt.ylabel('GDP per capita')
@@ -123,6 +128,7 @@ Plot Styles
 - For example, do a bar plot using a fancier style.
 
 .. code-block:: python
+  :linenos:
 
   plt.style.use('ggplot')
   data.T.plot(kind='bar')
@@ -142,6 +148,7 @@ Using the `plot` Function
 - The color and format of markers can also be specified as an additional optional argument e.g., `b-` is a blue line, `g--` is a green dashed line.
 
 .. code-block:: python
+  :linenos:
 
   years = data.columns
   gdp_australia = data.loc['Australia']
@@ -160,6 +167,7 @@ Using the `plot` Function
   **Figure Options**
 
   .. code-block:: python
+    :linenos:
 
     fig, axes = plot.subplots(
       dpi=150,  # Resolution of the Figure (150=publication, 300=high quality)
@@ -171,6 +179,7 @@ Using the `plot` Function
   **Plot Options**
 
   .. code-block:: python
+    :linenos:
 
     plot(
       x, y,  # The data
@@ -192,6 +201,7 @@ Using the `plot` Function
   *also for `y`axis*
 
   .. code-block:: python
+    :linenos:
 
     ax.set_xlabel('Test X-Axis')  # Sets Label of axis
     ax.set_xlim(0, 300)  # Sets axis limits
@@ -203,6 +213,7 @@ Using the `plot` Function
 - You can plot many sets of data together.
 
 .. code-block:: python
+  :linenos:
 
   # Select two countries' worth of data.
   gdp_australia = data.loc['Australia']
@@ -230,6 +241,7 @@ Using the `plot` Function
   - Provide a label for each dataset in the figure:
 
   .. code-block:: python
+    :linenos:
 
     ax.plot(years, gdp_australia, label='Australia')
     ax.plot(years, gdp_nz, label='New Zealand')
@@ -237,6 +249,7 @@ Using the `plot` Function
   - Instruct `matplotlib` to create the legend.
 
   .. code-block:: python
+    :linenos:
 
     fig.legend()  # This adds the legend to the figure object (can be outside the axis)
     ax.legend()  # This adds the legend into the axis
@@ -250,6 +263,7 @@ Saving your plot to a file
 If you are satisfied with the plot you see you may want to save it to a file, perhaps to include it in a publication. There is a function in the matplotlib.pyplot module that accomplishes this: `savefig <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html>`__. Calling this function, e.g. with
 
 .. code-block:: python
+  :linenos:
 
   plt.savefig('my_figure.png')
 
@@ -260,6 +274,7 @@ Note that functions in `plt` refer to a global figure variable and after a figur
 When using dataframes, data is often generated and plotted to screen in one line. In addition to using `plt.savefig`, we can save a reference to the current figure in a local variable (with `plt.gcf`) and call the `savefig` class method from that variable to save the figure to file.
 
 .. code-block:: python
+  :linenos:
 
   data.plot(kind='bar')
   fig = plt.gcf() # get current figure
@@ -301,6 +316,7 @@ You can find a list of all plot types in the `Matplotlib Documentation <https://
   Fill in the blanks below to plot the minimum GDP per capita over time for all the countries in Europe. Modify it again to plot the maximum GDP per capita over time for Europe.
 
   .. code-block:: python
+    :linenos:
 
     data_europe = pd.read_csv('data/gapminder_gdp_europe.csv', index_col='country')
     data_europe.____.plot(label='min')
@@ -314,6 +330,7 @@ You can find a list of all plot types in the `Matplotlib Documentation <https://
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     data_europe = pd.read_csv('data/gapminder_gdp_europe.csv', index_col='country')
     data_europe.min().plot(label='min')
@@ -340,6 +357,7 @@ You can find a list of all plot types in the `Matplotlib Documentation <https://
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     data_asia = pd.read_csv('data/gapminder_gdp_asia.csv', index_col='country')
     data_asia.describe().T.plot(kind='scatter', x='min', y='max')
@@ -356,6 +374,7 @@ You can find a list of all plot types in the `Matplotlib Documentation <https://
   You might note that the variability in the maximum is much higher than that of the minimum.  Take a look at the maximum and the max indexes:
 
   .. code-block:: python
+    :linenos:
 
     data_asia = pd.read_csv('data/gapminder_gdp_asia.csv', index_col='country')
     data_asia.max().plot()
@@ -385,6 +404,7 @@ You can find a list of all plot types in the `Matplotlib Documentation <https://
   This short program creates a plot showing the correlation between GDP and life expectancy for 2007, normalizing marker size by population:
 
   .. code-block:: python
+    :linenos:
 
     data_all = pd.read_csv('data/gapminder_all.csv', index_col='country')
     data_all.plot(
diff --git a/lessons/11-lists.rst b/lessons/11-lists.rst
index 69678711ff05ca6d535e369c84c7f91d3ae056f2..843eec849121fa7eaf2c7c334301261f4f67e5d1 100644
--- a/lessons/11-lists.rst
+++ b/lessons/11-lists.rst
@@ -31,6 +31,7 @@ List Basics
 - Use `len` to find out how many values are in a list.
 
 .. code-block:: python
+  :linenos:
 
   pressures = [0.273, 0.275, 0.277, 0.275, 0.276]
   print('pressures:', pressures)
@@ -48,6 +49,7 @@ Indexing Lists
 - Just like strings.
 
 .. code-block:: python
+  :linenos:
 
   print('zeroth item of pressures:', pressures[0])
   print('fourth item of pressures:', pressures[4])
@@ -84,6 +86,7 @@ Changing Items
 - Use an index expression on the left of assignment to replace a value.
 
 .. code-block:: python
+  :linenos:
 
   pressures[0] = 0.265
   print('pressures is now:', pressures)
@@ -99,6 +102,7 @@ Adding Items
 - Use `list_name.append` to add items to the end of a list.
 
 .. code-block:: python
+  :linenos:
 
   primes = [2, 3, 5]
   print('primes is initially:', primes)
@@ -125,6 +129,7 @@ Adding Items
 - `extend` is similar to `append`, but it allows you to combine two lists.  For example:
 
 .. code-block:: python
+  :linenos:
 
   teen_primes = [11, 13, 17, 19]
   middle_aged_primes = [37, 41, 43, 47]
@@ -150,6 +155,7 @@ Deleting Items
 - `del` is not a function or a method, but a statement in the language.
 
 .. code-block:: python
+  :linenos:
 
   primes = [2, 3, 5, 7, 9]
   print('primes before removing last item:', primes)
@@ -166,6 +172,7 @@ Deleting Items
   Fill in the blanks so that the program below produces the output shown.
 
   .. code-block:: python
+    :linenos:
 
     values = ____
     values.____(1)
@@ -186,6 +193,7 @@ Deleting Items
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     values = []
     values.append(1)
@@ -206,6 +214,7 @@ Deleting Items
   What does the following program print?
 
   .. code-block:: python
+    :linenos:
 
     element = 'helium'
     print(element[-1])
@@ -255,6 +264,7 @@ Data Types in Lists
 - A single list may contain numbers, strings, and anything else.
 
 .. code-block:: python
+  :linenos:
 
   goals = [1, 'Create lists.', 2, 'Extract items from lists.', 3, 'Modify lists.']
 
@@ -265,6 +275,7 @@ Strings and Lists
 - Get single characters from a character string using indexes in square brackets.
 
 .. code-block:: python
+  :linenos:
 
   element = 'carbon'
   print('zeroth character:', element[0])
@@ -287,6 +298,7 @@ Character Strings
 - Python considers the string to be a single value with parts, not a collection of values.
 
 .. code-block:: python
+  :linenos:
 
   element[0] = 'C'
 
@@ -301,6 +313,7 @@ Character Strings
   What does the following program print?
 
   .. code-block:: python
+    :linenos:
 
     element = 'fluorine'
     print(element[::2])
@@ -317,6 +330,7 @@ Character Strings
   The program prints
 
   .. code-block:: python
+    :linenos:
 
     furn
     eniroulf
@@ -335,6 +349,7 @@ Character Strings
   What does the following program print?
 
   .. code-block:: python
+    :linenos:
 
     element = 'lithium'
     print(element[0:20])
@@ -373,6 +388,7 @@ Index Errors
   - Cannot be detected as the code is parsed because the index might be calculated based on data.
 
 .. code-block:: python
+  :linenos:
 
   print('99th element of element is:', element[99])
 
@@ -387,6 +403,7 @@ Index Errors
   Given this:
 
   .. code-block:: python
+    :linenos:
 
     print('string to list:', list('tin'))
     print('list to string:', ''.join(['g', 'o', 'l', 'd']))
@@ -405,11 +422,7 @@ Index Errors
       <summary markdown="span"><b>Solution</b></summary>
 
   1. [`list('some string')`](https://docs.python.org/3/library/stdtypes.html#list) converts a string into a list containing all of its characters.
-  2. [`join`](https://docs.python.org/3/library/stdtypes.html#str.join) returns a string that is the *concatenation*
-    of each string element in the list and adds the separator between each element in the list. This results in
-    `x-y-z`. The separator between the elements is the string that provides this method.
-
-
+  2. [`join`](https://docs.python.org/3/library/stdtypes.html#str.join) returns a string that is the *concatenation* of each string element in the list and adds the separator between each element in the list. This results in `x-y-z`. The separator between the elements is the string that provides this method.
 
   .. raw:: html
 
@@ -422,6 +435,7 @@ Index Errors
   What do these two programs print? In simple terms, explain the difference between `sorted(letters)` and `letters.sort()`.
 
   .. code-block:: python
+    :linenos:
 
     # Program A
     letters = list('gold')
@@ -429,6 +443,7 @@ Index Errors
     print('letters is', letters, 'and result is', result)
 
   .. code-block:: python
+    :linenos:
 
     # Program B
     letters = list('gold')
@@ -465,6 +480,7 @@ Index Errors
   What do these two programs print? In simple terms, explain the difference between `new = old` and `new = old[:]`.
 
   .. code-block:: python
+    :linenos:
 
     # Program A
     old = list('gold')
@@ -473,6 +489,7 @@ Index Errors
     print('new is', new, 'and old is', old)
 
   .. code-block:: python
+    :linenos:
 
     # Program B
     old = list('gold')
diff --git a/lessons/12-for-loops.rst b/lessons/12-for-loops.rst
index 1ec9b9babb8ed2cddd0b423c68f8698135216646..943ec0a58837e829344f048f06815b375d1d827b 100644
--- a/lessons/12-for-loops.rst
+++ b/lessons/12-for-loops.rst
@@ -26,6 +26,7 @@ Exercises: 15 min
 - "for each thing in this group, do these operations"
 
 .. code-block:: python
+  :linenos:
 
   for number in [2, 3, 5]:
       print(number)
@@ -33,6 +34,7 @@ Exercises: 15 min
 - This `for` loop is equivalent to:
 
 .. code-block:: python
+  :linenos:
 
   print(2)
   print(3)
@@ -52,6 +54,7 @@ Syntax of a `for` Loop
 - A `for` loop is made up of a collection, a loop variable, and a body.
 
 .. code-block:: python
+  :linenos:
 
   for number in [2, 3, 5]:
       print(number)
@@ -69,6 +72,7 @@ Syntax of a `for` Loop
   - Any consistent indentation is legal, but almost everyone uses four spaces.
 
 .. code-block:: python
+  :linenos:
 
   for number in [2, 3, 5]:
   print(number)
@@ -80,6 +84,7 @@ Syntax of a `for` Loop
 - Indentation is always meaningful in Python.
 
 .. code-block:: python
+  :linenos:
 
   firstName = "Jon"
     lastName = "Smith"
@@ -121,6 +126,7 @@ Loop Variables
   - Meaningless: their names can be anything at all.
 
 .. code-block:: python
+  :linenos:
 
   for kitten in [2, 3, 5]:
       print(kitten)
@@ -133,6 +139,7 @@ Loop Body
 - Hard for human beings to keep larger chunks of code in mind.
 
 .. code-block:: python
+  :linenos:
 
   primes = [2, 3, 5]
   for p in primes:
@@ -159,6 +166,7 @@ Loop Body
   - Exactly the legal indices of a list or character string of length N
 
 .. code-block:: python
+  :linenos:
 
   print('a range is not a list: range(0, 3)')
   for number in range(0, 3):
@@ -177,6 +185,7 @@ Loop Body
   Create a table showing the numbers of the lines that are executed when this program runs, and the values of the variables after each line is executed.
 
   .. code-block:: python
+    :linenos:
 
     total = 0
     for char in "tin":
@@ -221,6 +230,7 @@ Accumulator Patterns
   2. Update the variable with values from a collection.
 
 .. code-block:: python
+  :linenos:
 
   # Sum the first 10 integers.
   total = 0
@@ -248,6 +258,7 @@ Accumulator Patterns
   1. **Total length of the strings in the list:**: `["red", "green", "blue"] => 12`
 
     .. code-block:: python
+      :linenos:
 
       total = 0
       for word in ["red", "green", "blue"]:
@@ -260,6 +271,7 @@ Accumulator Patterns
         <summary markdown="span"><b>Solution</b></summary>
 
     .. code-block:: python
+      :linenos:
 
       total = 0
       for word in ["red", "green", "blue"]:
@@ -273,6 +285,7 @@ Accumulator Patterns
   2. **List of word lengths:**: `["red", "green", "blue"] => [3, 5, 4]`
 
     .. code-block:: python
+      :linenos:
 
       lengths = ____
       for word in ["red", "green", "blue"]:
@@ -285,6 +298,7 @@ Accumulator Patterns
         <summary markdown="span"><b>Solution</b></summary>
 
     .. code-block:: python
+      :linenos:
 
       lengths = []
       for word in ["red", "green", "blue"]:
@@ -298,6 +312,7 @@ Accumulator Patterns
   3. **Concatenate all words:**: `["red", "green", "blue"] => "redgreenblue"`
 
     .. code-block:: python
+      :linenos:
 
       words = ["red", "green", "blue"]
       result = ____
@@ -311,6 +326,7 @@ Accumulator Patterns
         <summary markdown="span"><b>Solution</b></summary>
 
     .. code-block:: python
+      :linenos:
 
       words = ["red", "green", "blue"]
       result = ""
@@ -334,6 +350,7 @@ Accumulator Patterns
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     acronym = ""
     for word in ["red", "green", "blue"]:
@@ -351,6 +368,7 @@ Accumulator Patterns
   Reorder and properly indent the lines of code below so that they print a list with the cumulative sum of data. The result should be `[1, 3, 5, 10]`.
 
   .. code-block:: python
+    :linenos:
 
     cumulative.append(total)
     for number in data:
@@ -366,6 +384,7 @@ Accumulator Patterns
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     total = 0
     data = [1,2,2,5]
@@ -388,6 +407,7 @@ Accumulator Patterns
   Fill in the blanks in the program below so that it prints "nit" (the reverse of the original character string "tin").
 
   .. code-block:: python
+    :linenos:
 
     original = "tin"
     result = ____
@@ -401,6 +421,7 @@ Accumulator Patterns
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     original = "tin"
     result = ""
@@ -425,6 +446,7 @@ List Comprehensions
   - This is prevented in many cases by list comprehensions
 
 .. code-block:: python
+  :linenos:
 
   # Generate a list of values between 3 and 6
   numbers = [num for num in range(3, 7)]
@@ -451,6 +473,7 @@ List Comprehensions
   4. Repeat steps 2 and 3, until you have fixed all the errors.
 
   .. code-block:: python
+    :linenos:
 
     for number in range(10):
         # use a if the number is a multiple of 3, otherwise use b
@@ -470,6 +493,7 @@ List Comprehensions
   - We want to add the string `"a"` to `message`, not the undefined variable `a`.
 
   .. code-block:: python
+    :linenos:
 
     message = ""
     for number in range(10):
@@ -493,6 +517,7 @@ List Comprehensions
   3. Fix the error.
 
   .. code-block:: python
+    :linenos:
 
     seasons = ['Spring', 'Summer', 'Fall', 'Winter']
     print('My favorite season is ', seasons[4])
@@ -505,6 +530,7 @@ List Comprehensions
   This list has 4 elements and the index to access the last element in the list is `3`.
 
   .. code-block:: python
+    :linenos:
 
     seasons = ['Spring', 'Summer', 'Fall', 'Winter']
     print('My favorite season is ', seasons[3])
diff --git a/lessons/13-conditionals.rst b/lessons/13-conditionals.rst
index 83330cf992b6f1f952cb4a7f4d548ce0cb777cc8..16afc8d8043afbaab4a563fd8dc6d3ea506c72a4 100644
--- a/lessons/13-conditionals.rst
+++ b/lessons/13-conditionals.rst
@@ -29,6 +29,7 @@ The `if` statement
   - Body containing one or more statements is indented (usually by 4 spaces)
 
 .. code-block:: python
+  :linenos:
 
   mass = 3.54
   if mass > 3.0:
@@ -50,6 +51,7 @@ Conditionals in Loops
 - But useful when we have a collection to process.
 
 .. code-block:: python
+  :linenos:
 
   masses = [3.54, 2.07, 9.22, 1.86, 1.71]
   for m in masses:
@@ -68,6 +70,7 @@ The `else` statement
 - Allows us to specify an alternative to execute when the `if` *branch* isn't taken.
 
 .. code-block:: python
+  :linenos:
 
   masses = [3.54, 2.07, 9.22, 1.86, 1.71]
   for m in masses:
@@ -94,6 +97,7 @@ The `elif` statement
 - Must come before the `else` (which is the "catch all").
 
 .. code-block:: python
+  :linenos:
 
   masses = [3.54, 2.07, 9.22, 1.86, 1.71]
   for m in masses:
@@ -120,6 +124,7 @@ Execution Order
 - So ordering matters.
 
 .. code-block:: python
+  :linenos:
 
   grade = 85
   if grade >= 70:
@@ -136,6 +141,7 @@ Execution Order
 - Does *not* automatically go back and re-evaluate if values change.
 
 .. code-block:: python
+  :linenos:
 
   velocity = 10.0
   if velocity > 20.0:
@@ -151,6 +157,7 @@ Execution Order
 - Often use conditionals in a loop to "evolve" the values of variables.
 
 .. code-block:: python
+  :linenos:
 
   velocity = 10.0
   for i in range(5): # execute the loop 5 times
@@ -186,6 +193,7 @@ Execution Order
   relations within a conditional using `and` and `or`.  Continuing the example above, suppose you have
 
   .. code-block:: python
+    :linenos:
 
     mass   = [ 3.54,  2.07,  9.22,  1.86,  1.71]
     velocity = [10.00, 20.00, 30.00, 25.00, 20.00]
@@ -204,12 +212,14 @@ Execution Order
   Just like with arithmetic, you can and should use parentheses whenever there is possible ambiguity.  A good general rule is to *always* use parentheses when mixing `and` and `or` in the same condition.  That is, instead of:
 
   .. code-block:: python
+    :linenos:
 
     if mass[i] <= 2 or mass[i] >= 5 and velocity[i] > 20:
 
   write one of these:
 
   .. code-block:: python
+    :linenos:
 
     if (mass[i] <= 2 or mass[i] >= 5) and velocity[i] > 20:
     if mass[i] <= 2 or (mass[i] >= 5 and velocity[i] > 20):
@@ -223,6 +233,7 @@ Execution Order
   What does this program print?
 
   .. code-block:: python
+    :linenos:
 
     pressure = 71.9
     if pressure > 50.0:
@@ -253,6 +264,7 @@ Execution Order
   and ones where the original list's values were positive.
 
   .. code-block:: python
+    :linenos:
 
     original = [-1.5, 0.2, 0.4, 0.0, -1.3, 0.4]
     result = ____
@@ -273,6 +285,7 @@ Execution Order
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     original = [-1.5, 0.2, 0.4, 0.0, -1.3, 0.4]
     result = []
@@ -295,6 +308,7 @@ Execution Order
   Modify this program so that it finds the largest and smallest values in the list no matter what the range of values originally is.
 
   .. code-block:: python
+    :linenos:
 
     values = [...some test data...]
     smallest, largest = None, None
@@ -315,6 +329,7 @@ Execution Order
       <summary markdown="span"><b>Solution</b></summary>
 
   .. code-block:: python
+    :linenos:
 
     values = [-2,1,65,78,-54,-24,100]
     smallest, largest = None, None
@@ -331,6 +346,7 @@ Execution Order
   It can be argued that an advantage of using this method would be to make the code more readable. However, a disadvantage is that this code is not efficient because within each iteration of the `for` loop statement, there are two more loops that run over two numbers each (the `min` and `max` functions). It would be more efficient to iterate over each number just once:
 
   .. code-block:: python
+    :linenos:
 
     values = [-2,1,65,78,-54,-24,100]
     smallest, largest = None, None
@@ -346,6 +362,7 @@ Execution Order
   comparison test. The simplest solution is often the best:
 
   .. code-block:: python
+    :linenos:
 
     values = [-2,1,65,78,-54,-24,100]
     smallest = min(values)
diff --git a/lessons/14-custom-lesson.rst b/lessons/14-custom-lesson.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d114bc09ddadde3e4c28f39b18d8895d23f4566e
--- /dev/null
+++ b/lessons/14-custom-lesson.rst
@@ -0,0 +1,26 @@
+===============================
+Lesson 12 - Custom Lesson (WIP)
+===============================
+
+Teaching: 5 min
+Exercises: 10 min
+-------------------
+
+.. highlights:: **Highlights**
+
+  - Be able to read and write globbing expressions that match sets of files.
+  - Use glob to create lists of files.
+  - Write for loops to perform operations on files given their names in a list.
+
+.. hint::
+
+  - How can I process many data sets with a single command?
+
+-------------------
+
+
+.. admonition:: **Summary**
+
+  - Use a `for` loop to process files given a list of their names.
+  - Use `glob.glob` to find sets of files whose names match a pattern.
+  - Use `glob` and `for` to process batches of files.
diff --git a/lessons/14-looping-data-sets.rst b/lessons/14-looping-data-sets.rst
deleted file mode 100644
index c750a627551f0e4c1f2982841e018d4d7c0660a5..0000000000000000000000000000000000000000
--- a/lessons/14-looping-data-sets.rst
+++ /dev/null
@@ -1,252 +0,0 @@
-==================================
-Lesson 12 - Looping Over Data Sets
-==================================
-
--------------------
-Teaching: 5 min
-Exercises: 10 min
--------------------
-
-.. highlights:: **Highlights**
-
-- Be able to read and write globbing expressions that match sets of files.
-- Use glob to create lists of files.
-- Write for loops to perform operations on files given their names in a list.
-
-.. hint::
-
-- How can I process many data sets with a single command?
-
--------------------
-
-## Use a `for` loop to process files given a list of their names.
-
-- A filename is a character string.
-- And lists can contain character strings.
-
-.. code-block:: python
-
-import pandas as pd
-for filename in ['data/gapminder_gdp_africa.csv', 'data/gapminder_gdp_asia.csv']:
-    data = pd.read_csv(filename, index_col='country')
-    print(filename, data.min())
-
-.. code-block::
-
-data/gapminder_gdp_africa.csv gdpPercap_1952    298.846212
-gdpPercap_1957    335.997115
-gdpPercap_1962    355.203227
-gdpPercap_1967    412.977514
-⋮ ⋮ ⋮
-gdpPercap_1997    312.188423
-gdpPercap_2002    241.165877
-gdpPercap_2007    277.551859
-dtype: float64
-data/gapminder_gdp_asia.csv gdpPercap_1952    331
-gdpPercap_1957    350
-gdpPercap_1962    388
-gdpPercap_1967    349
-⋮ ⋮ ⋮
-gdpPercap_1997    415
-gdpPercap_2002    611
-gdpPercap_2007    944
-dtype: float64
-
-## Use [`glob.glob`](https://docs.python.org/3/library/glob.html#glob.glob) to find sets of files whose names match a pattern.
-
-- In Unix, the term "globbing" means "matching a set of files with a pattern".
-- The most common patterns are:
-  - `*` meaning "match zero or more characters"
-  - `?` meaning "match exactly one character"
-- Python's standard library contains the [`glob`](https://docs.python.org/3/library/glob.html) module to provide pattern matching functionality
-- The [`glob`](https://docs.python.org/3/library/glob.html) module contains a function also called `glob` to match file patterns
-- E.g., `glob.glob('*.txt')` matches all files in the current directory
-  whose names end with `.txt`.
-- Result is a (possibly empty) list of character strings.
-
-.. code-block:: python
-
-import glob
-print('all csv files in data directory:', glob.glob('data/*.csv'))
-
-.. code-block::
-
-all csv files in data directory: ['data/gapminder_all.csv', 'data/gapminder_gdp_africa.csv', \
-'data/gapminder_gdp_americas.csv', 'data/gapminder_gdp_asia.csv', 'data/gapminder_gdp_europe.csv', \
-'data/gapminder_gdp_oceania.csv']
-
-.. code-block:: python
-
-print('all PDB files:', glob.glob('*.pdb'))
-
-.. code-block::
-
-all PDB files: []
-
-## Use `glob` and `for` to process batches of files.
-
-- Helps a lot if the files are named and stored systematically and consistently
-  so that simple patterns will find the right data.
-
-.. code-block:: python
-
-for filename in glob.glob('data/gapminder_*.csv'):
-    data = pd.read_csv(filename)
-    print(filename, data['gdpPercap_1952'].min())
-
-.. code-block::
-
-data/gapminder_all.csv 298.8462121
-data/gapminder_gdp_africa.csv 298.8462121
-data/gapminder_gdp_americas.csv 1397.717137
-data/gapminder_gdp_asia.csv 331.0
-data/gapminder_gdp_europe.csv 973.5331948
-data/gapminder_gdp_oceania.csv 10039.59564
-
-- This includes all data, as well as per-region data.
-- Use a more specific pattern in the exercises to exclude the whole data set.
-- But note that the minimum of the entire data set is also the minimum of one of the data sets,
-  which is a nice check on correctness.
-
-.. attention:: ## Determining Matches
-
-Which of these files is *not* matched by the expression `glob.glob('data/*as*.csv')`?
-
-1. `data/gapminder_gdp_africa.csv`
-2. `data/gapminder_gdp_americas.csv`
-3. `data/gapminder_gdp_asia.csv`
-
-  .. raw:: html
-
-    <details>
-      <summary markdown="span"><b>Solution</b></summary>
-
-1 is not matched by the glob.
-
-
-
-  .. raw:: html
-
-    </details>
-
--------------------
-
-.. attention:: ## Minimum File Size
-
-Modify this program so that it prints the number of records in
-the file that has the fewest records.
-
-.. code-block:: python
-
-import glob
-import pandas as pd
-fewest = ____
-for filename in glob.glob('data/*.csv'):
-    dataframe = pd.____(filename)
-    fewest = min(____, dataframe.shape[0])
-print('smallest file has', fewest, 'records')
-
-Note that the [`DataFrame.shape()` method][shape-method]
-returns a tuple with the number of rows and columns of the data frame.
-
-  .. raw:: html
-
-    <details>
-      <summary markdown="span"><b>Solution</b></summary>
-
-.. code-block:: python
-
-import glob
-import pandas as pd
-fewest = float('Inf')
-for filename in glob.glob('data/*.csv'):
-    dataframe = pd.read_csv(filename)
-    fewest = min(fewest, dataframe.shape[0])
-print('smallest file has', fewest, 'records')
-
-You might have chosen to initialize the `fewest` variable with a number greater than the numbers
-you're dealing with, but that could lead to trouble if you reuse the code with bigger numbers.
-Python lets you use positive infinity, which will work no matter how big your numbers are.
-What other special strings does the [`float` function][float-function] recognize?
-
-
-
-  .. raw:: html
-
-    </details>
-
--------------------
-
-.. attention:: ## Comparing Data
-
-Write a program that reads in the regional data sets
-and plots the average GDP per capita for each region over time
-in a single chart.
-
-  .. raw:: html
-
-    <details>
-      <summary markdown="span"><b>Solution</b></summary>
-
-This solution builds a useful legend by using the [string `split` method][split-method] to
-extract the `region` from the path 'data/gapminder\_gdp\_a\_specific\_region.csv'.
-
-.. code-block:: python
-
-import glob
-import pandas as pd
-import matplotlib.pyplot as plt
-fig, ax = plt.subplots(1,1)
-for filename in glob.glob('data/gapminder_gdp*.csv'):
-    dataframe = pd.read_csv(filename)
-    # extract <region> from the filename, expected to be in the format 'data/gapminder_gdp_<region>.csv'.
-    # we will split the string using the split method and `_` as our separator,
-    # retrieve the last string in the list that split returns (`<region>.csv`),
-    # and then remove the `.csv` extension from that string.
-    region = filename.split('_')[-1][:-4]
-    dataframe.mean().plot(ax=ax, label=region)
-plt.legend()
-plt.show()
-
-  .. raw:: html
-
-    </details>
-
--------------------
-
-.. hint:: **## Dealing with File Paths
-
-The [`pathlib` module][pathlib-module] provides useful abstractions for file and path manipulation like
-returning the name of a file without the file extension. This is very useful when looping over files and
-directories. In the example below, we create a `Path` object and inspect its attributes.
-
-.. code-block:: python
-
-from pathlib import Path
-
-p = Path("data/gapminder_gdp_africa.csv")
-print(p.parent), print(p.stem), print(p.suffix)
-
-.. code-block::
-
-data
-gapminder_gdp_africa
-.csv
-
-**Hint:** It is possible to check all available attributes and methods on the `Path` object with the `dir()`
-function!
-
-
--------------------
-
-[shape-method]: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.shape.html
-[float-function]: https://docs.python.org/3/library/functions.html#float
-[split-method]: https://docs.python.org/3/library/stdtypes.html#str.split
-[pathlib-module]: https://docs.python.org/3/library/pathlib.html
-
-
-.. admonition:: **Summary**
-
-- Use a `for` loop to process files given a list of their names.
-- Use `glob.glob` to find sets of files whose names match a pattern.
-- Use `glob` and `for` to process batches of files.
diff --git a/lessons/16-writing-functions.rst b/lessons/16-writing-functions.rst
index 8aa2a3429432aa04a593cd22284f623e223d805e..eedc793123c78c32c1232a75d1691613170d1e62 100644
--- a/lessons/16-writing-functions.rst
+++ b/lessons/16-writing-functions.rst
@@ -2,175 +2,200 @@
 Lesson 13 - Writing Functions
 =============================
 
--------------------
 Teaching: 10 min
 Exercises: 15 min
+
 -------------------
 
 .. highlights:: **Highlights**
 
-- Explain and identify the difference between function definition and function call.
-- Write a function that takes a small, fixed number of arguments and produces a single result.
+  - Explain and identify the difference between function definition and function call.
+  - Write a function that takes a small, fixed number of arguments and produces a single result.
 
 .. hint::
 
-- How can I create my own functions?
+  - How can I create my own functions?
 
 -------------------
 
-## Break programs down into functions to make them easier to understand.
+What are Functions?
+===================
 
+- Break programs down into functions to make them easier to understand.
 - Human beings can only keep a few items in working memory at a time.
 - Understand larger/more complicated ideas by understanding and combining pieces.
+
   - Components in a machine.
   - Lemmas when proving theorems.
+
 - Functions serve the same purpose in programs.
+
   - *Encapsulate* complexity so that we can treat it as a single "thing".
+
 - Also enables *re-use*.
+
   - Write one time, use many times.
 
-## Define a function using `def` with a name, parameters, and a block of code.
+The `def` statement
+-------------------
 
+- Define a function using `def` with a name, parameters, and a block of code.
 - Begin the definition of a new function with `def`.
 - Followed by the name of the function.
+
   - Must obey the same rules as variable names.
+
 - Then *parameters* in parentheses.
+
   - Empty parentheses if the function doesn't take any inputs.
   - We will discuss this in detail in a moment.
+
 - Then a colon.
 - Then an indented block of code.
 
 .. code-block:: python
+  :linenos:
 
-def print_greeting():
-    print('Hello!')
-    print('The weather is nice today.')
-    print('Right?')
+  def print_greeting():
+      print('Hello!')
+      print('The weather is nice today.')
+      print('Right?')
 
-## Defining a function does not run it.
+
+Calling Functions
+-----------------
 
 - Defining a function does not run it.
+
   - Like assigning a value to a variable.
+
 - Must call the function to execute the code it contains.
 
 .. code-block:: python
+  :linenos:
 
-print_greeting()
+  print_greeting()
 
 .. code-block::
 
-Hello!
+  Hello!
 
-## Arguments in a function call are matched to its defined parameters.
+Function Arguments
+------------------
 
 - Functions are most useful when they can operate on different data.
 - Specify *parameters* when defining a function.
+
   - These become variables when the function is executed.
   - Are assigned the arguments in the call (i.e., the values passed to the function).
-  - If you don't name the arguments when using them in the call, the arguments will be matched to
-    parameters in the order the parameters are defined in the function.
+  - If you don't name the arguments when using them in the call, the arguments will be matched to parameters in the order the parameters are defined in the function.
 
 .. code-block:: python
+  :linenos:
 
-def print_date(year, month, day):
-    joined = str(year) + '/' + str(month) + '/' + str(day)
-    print(joined)
+  def print_date(year, month, day):
+      joined = str(year) + '/' + str(month) + '/' + str(day)
+      print(joined)
 
-print_date(1871, 3, 19)
+  print_date(1871, 3, 19)
 
 .. code-block::
 
-1871/3/19
+  1871/3/19
 
-Or, we can name the arguments when we call the function, which allows us to
-specify them in any order and adds clarity to the call site; otherwise as
-one is reading the code they might forget if the second argument is the month
-or the day for example.
+Or, we can name the arguments when we call the function, which allows us to specify them in any order and adds clarity to the call site; otherwise as one is reading the code they might forget if the second argument is the month or the day for example.
 
 .. code-block:: python
+  :linenos:
 
-print_date(month=3, day=19, year=1871)
+  print_date(month=3, day=19, year=1871)
 
 .. code-block::
 
-1871/3/19
+  1871/3/19
 
-- Via [Twitter](https://twitter.com/minisciencegirl/status/693486088963272705):
-  `()` contains the ingredients for the function
-  while the body contains the recipe.
+- `()` contains the ingredients for the function
+- while the body contains the recipe.
 
-## Functions may return a result to their caller using `return`.
+Function Returns
+----------------
 
+- Functions may return a result to their caller using `return`.
 - Use `return ...` to give a value back to the caller.
 - May occur anywhere in the function.
 - But functions are easier to understand if `return` occurs:
+
   - At the start to handle special cases.
   - At the very end, with a final result.
 
 .. code-block:: python
+  :linenos:
 
-def average(values):
-    if len(values) == 0:
-        return None
-    return sum(values) / len(values)
+  def average(values):
+      if len(values) == 0:
+          return None
+      return sum(values) / len(values)
 
 .. code-block:: python
+  :linenos:
 
-a = average([1, 3, 4])
-print('average of actual values:', a)
+  a = average([1, 3, 4])
+  print('average of actual values:', a)
 
 .. code-block::
 
-average of actual values: 2.6666666666666665
+  average of actual values: 2.6666666666666665
 
 .. code-block:: python
+  :linenos:
 
-print('average of empty list:', average([]))
+  print('average of empty list:', average([]))
 
 .. code-block::
 
-average of empty list: None
+  average of empty list: None
 
-- Remember: [every function returns something](04-built-in.md).
+- Remember: **every function returns something.**
 - A function that doesn't explicitly `return` a value automatically returns `None`.
 
 .. code-block:: python
+  :linenos:
 
-result = print_date(1871, 3, 19)
-print('result of call is:', result)
+  result = print_date(1871, 3, 19)
+  print('result of call is:', result)
 
 .. code-block::
 
-1871/3/19
-result of call is: None
+  1871/3/19
+  result of call is: None
 
-.. attention:: ## Identifying Syntax Errors
+.. attention:: **Identifying Syntax Errors**
 
-1. Read the code below and try to identify what the errors are
-  *without* running it.
-2. Run the code and read the error message.
-  Is it a `SyntaxError` or an `IndentationError`?
-3. Fix the error.
-4. Repeat steps 2 and 3 until you have fixed all the errors.
+  1. Read the code below and try to identify what the errors are *without* running it.
+  2. Run the code and read the error message. Is it a `SyntaxError` or an `IndentationError`?
+  3. Fix the error.
+  4. Repeat steps 2 and 3 until you have fixed all the errors.
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-def another_function
-  print("Syntax errors are annoying.")
-   print("But at least python tells us about them!")
-  print("So they are usually not too hard to fix.")
+    def another_function
+      print("Syntax errors are annoying.")
+      print("But at least python tells us about them!")
+      print("So they are usually not too hard to fix.")
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-def another_function():
-  print("Syntax errors are annoying.")
-  print("But at least Python tells us about them!")
-  print("So they are usually not too hard to fix.")
+    def another_function():
+      print("Syntax errors are annoying.")
+      print("But at least Python tells us about them!")
+      print("So they are usually not too hard to fix.")
 
   .. raw:: html
 
@@ -178,37 +203,39 @@ def another_function():
 
 -------------------
 
-.. attention:: ## Definition and Use
+.. attention:: **Definition and Use**
 
-What does the following program print?
+  What does the following program print?
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-def report(pressure):
-    print('pressure is', pressure)
+    def report(pressure):
+        print('pressure is', pressure)
 
-print('calling', report, 22.5)
+    print('calling', report, 22.5)
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-.. code-block::
+  .. code-block::
 
-calling <function report at 0x7fd128ff1bf8> 22.5
+    calling <function report at 0x7fd128ff1bf8> 22.5
 
-A function call always needs parenthesis, otherwise you get memory address of the function object. So, if we wanted to call the function named report, and give it the value 22.5 to report on, we could have our function call as follows
+  A function call always needs parenthesis, otherwise you get memory address of the function object. So, if we wanted to call the function named report, and give it the value 22.5 to report on, we could have our function call as follows
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-print("calling")
-report(22.5)
+    print("calling")
+    report(22.5)
 
-.. code-block::
+  .. code-block::
 
-calling
-pressure is 22.5
+    calling
+    pressure is 22.5
 
   .. raw:: html
 
@@ -216,48 +243,44 @@ pressure is 22.5
 
 -------------------
 
-.. attention:: ## Order of Operations
+.. attention:: **Order of Operations**
 
-1. What's wrong in this example?
+  1. What's wrong in this example?
 
   .. code-block:: python
+    :linenos:
 
-  result = print_time(11, 37, 59)
+    result = print_time(11, 37, 59)
 
-  def print_time(hour, minute, second):
-     time_string = str(hour) + ':' + str(minute) + ':' + str(second)
-     print(time_string)
+    def print_time(hour, minute, second):
+      time_string = str(hour) + ':' + str(minute) + ':' + str(second)
+      print(time_string)
 
-2. After fixing the problem above, explain why running this example code:
+  2. After fixing the problem above, explain why running this example code:
 
   .. code-block:: python
+    :linenos:
 
-  result = print_time(11, 37, 59)
-  print('result of call is:', result)
+    result = print_time(11, 37, 59)
+    print('result of call is:', result)
 
-  gives this output:
+    gives this output:
 
   .. code-block::
 
-  11:37:59
-  result of call is: None
+    11:37:59
+    result of call is: None
 
-3. Why is the result of the call `None`?
+  3. Why is the result of the call `None`?
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-1. The problem with the example is that the function `print_time()` is defined *after* the call to the function is made. Python
-  doesn't know how to resolve the name `print_time` since it hasn't been defined yet and will raise a `NameError` e.g.,
-  `NameError: name 'print_time' is not defined`
-
-2. The first line of output `11:37:59` is printed by the first line of code, `result = print_time(11, 37, 59)` that binds the value
-  returned by invoking `print_time` to the variable `result`. The second line is from the second print call to print the contents
-  of the `result` variable.
-
-3. `print_time()` does not explicitly `return` a value, so it automatically returns `None`.
+  1. The problem with the example is that the function `print_time()` is defined *after* the call to the function is made. Python doesn't know how to resolve the name `print_time` since it hasn't been defined yet and will raise a `NameError` e.g., `NameError: name 'print_time' is not defined`
+  2. The first line of output `11:37:59` is printed by the first line of code, `result = print_time(11, 37, 59)` that binds the value returned by invoking `print_time` to the variable `result`. The second line is from the second print call to print the contents of the `result` variable.
+  3. `print_time()` does not explicitly `return` a value, so it automatically returns `None`.
 
   .. raw:: html
 
@@ -265,32 +288,33 @@ pressure is 22.5
 
 -------------------
 
-.. attention:: ## Encapsulation
+.. attention:: **Encapsulation**
 
-Fill in the blanks to create a function that takes a single filename as an argument,
-loads the data in the file named by the argument,
-and returns the minimum value in that data.
+  Fill in the blanks to create a function that takes a single filename as an argument, loads the data in the file named by the argument,
+  and returns the minimum value in that data.
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-import pandas as pd
+    import pandas as pd
 
-def min_in_data(____):
-    data = ____
-    return ____
+    def min_in_data(____):
+        data = ____
+        return ____
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-import pandas as pd
+    import pandas as pd
 
-def min_in_data(filename):
-    data = pd.read_csv(filename)
-    return data.min()
+    def min_in_data(filename):
+        data = pd.read_csv(filename)
+        return data.min()
 
   .. raw:: html
 
@@ -298,41 +322,44 @@ def min_in_data(filename):
 
 -------------------
 
-.. attention:: ## Find the First
+.. attention:: **Find the First**
 
-Fill in the blanks to create a function that takes a list of numbers as an argument
-and returns the first negative value in the list.
-What does your function do if the list is empty? What if the list has no negative numbers?
+  Fill in the blanks to create a function that takes a list of numbers as an argument and returns the first negative value in the list.
 
-.. code-block:: python
+  What does your function do if the list is empty? What if the list has no negative numbers?
+
+  .. code-block:: python
+    :linenos:
 
-def first_negative(values):
-    for v in ____:
-        if ____:
-            return ____
+    def first_negative(values):
+        for v in ____:
+            if ____:
+                return ____
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-def first_negative(values):
-    for v in values:
-        if v < 0:
-            return v
+    def first_negative(values):
+        for v in values:
+            if v < 0:
+                return v
 
-If an empty list or a list with all positive values is passed to this function, it returns `None`:
+  If an empty list or a list with all positive values is passed to this function, it returns `None`:
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-my_list = []
-print(first_negative(my_list))
+    my_list = []
+    print(first_negative(my_list))
 
-.. code-block::
+  .. code-block::
 
-None
+    None
 
   .. raw:: html
 
@@ -340,209 +367,317 @@ None
 
 -------------------
 
-.. attention:: ## Calling by Name
+.. attention:: **Calling by Name**
 
-Earlier we saw this function:
+  Earlier we saw this function:
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-def print_date(year, month, day):
-    joined = str(year) + '/' + str(month) + '/' + str(day)
-    print(joined)
+    def print_date(year, month, day):
+        joined = str(year) + '/' + str(month) + '/' + str(day)
+        print(joined)
 
-We saw that we can call the function using *named arguments*, like this:
+  We saw that we can call the function using *named arguments*, like this:
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-print_date(day=1, month=2, year=2003)
+    print_date(day=1, month=2, year=2003)
 
-1. What does `print_date(day=1, month=2, year=2003)` print?
-2. When have you seen a function call like this before?
-3. When and why is it useful to call functions this way?
+  1. What does `print_date(day=1, month=2, year=2003)` print?
+  2. When have you seen a function call like this before?
+  3. When and why is it useful to call functions this way?
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-1. `2003/2/1`
-2. We saw examples of using *named arguments* when working with the pandas library. For example, when reading in a dataset
-  using `data = pd.read_csv('data/gapminder_gdp_europe.csv', index_col='country')`, the last argument `index_col` is a
-  named argument.
-3. Using named arguments can make code more readable since one can see from the function call what name the different arguments
-  have inside the function. It can also reduce the chances of passing arguments in the wrong order, since by using named arguments
-  the order doesn't matter.
-
-
+  1. `2003/2/1`
+  2. We saw examples of using *named arguments* when working with the pandas library. For example, when reading in a dataset using `data = pd.read_csv('data/gapminder_gdp_europe.csv', index_col='country')`, the last argument `index_col` is a named argument.
+  3. Using named arguments can make code more readable since one can see from the function call what name the different arguments have inside the function. It can also reduce the chances of passing arguments in the wrong order, since by using named arguments the order doesn't matter.
 
   .. raw:: html
 
     </details>
 
--------------------
+----------------------------
+
+Special Function Arguments
+==========================
 
-.. attention:: ## Encapsulation of an If/Print Block
+Keyword Arguments (defaults)
+----------------------------
 
-The code below will run on a label-printer for chicken eggs.  A digital scale will report a chicken egg mass (in grams)
-to the computer and then the computer will print a label.
+- Sometimes one would want a default value for a function argument
+
+  - For example, occasionally the color of a plot should be red instead of blue
+  - Many functions in libraries have additional options called *keyword arguments* (short `kwargs`).
+
+- Assume that we want the `print_date` function to return a base date if no other arguments are given
 
 .. code-block:: python
+  :linenos:
+
+  def print_date(year=1970, month=1, day=1):
+      joined = str(year) + '/' + str(month) + '/' + str(day)
+      print(joined)
+
+  print_date()
+  print_date(year=2001)
 
-import random
-for i in range(10):
+.. code-block::
+
+  1970/1/1
+  2001/1/1
+
+This can also be mixed, but keyword arguments should always be defined after normal arguments
+
+.. code-block:: python
+  :linenos:
 
-    # simulating the mass of a chicken egg
-    # the (random) mass will be 70 +/- 20 grams
-    mass = 70 + 20.0 * (2.0 * random.random() - 1.0)
+  def print_date(day, month, year=1970):
+      joined = str(year) + '/' + str(month) + '/' + str(day)
+      print(joined)
 
-    print(mass)
+Asterix Idioms
+--------------
 
-    # egg sizing machinery prints a label
-    if mass >= 85:
-        print("jumbo")
-    elif mass >= 70:
-        print("large")
-    elif mass < 70 and mass >= 55:
-        print("medium")
-    else:
-        print("small")
+In many documentations you will see that arguments or keyword arguments are given as `*args` or `**kwargs`. This is a placeholder for an arbitrary number of arguments that can be accepted by the function.
 
-The if-block that classifies the eggs might be useful in other situations,
-so to avoid repeating it, we could fold it into a function, `get_egg_label()`.
-Revising the program to use the function would give us this:
+Suppose we want to print all values that are given as input into a function.
 
 .. code-block:: python
+  :linenos:
 
-# revised version
-import random
-for i in range(10):
+  def print_all(*args):
+      for a in args:
+          print(a)
 
-    # simulating the mass of a chicken egg
-    # the (random) mass will be 70 +/- 20 grams
-    mass = 70 + 20.0 * (2.0 * random.random() - 1.0)
+  print_all(1)
 
-    print(mass, get_egg_label(mass))
+  print_all(3, 4, 6, 7)
 
+.. code-block::
 
-1. Create a function definition for `get_egg_label()` that will work with the revised program above.  Note that the `get_egg_label()` function's return value will be important. Sample output from the above program would be `71.23 large`.
-2. A dirty egg might have a mass of more than 90 grams, and a spoiled or broken egg will probably have a mass that's less than 50 grams.  Modify your `get_egg_label()` function to account for these error conditions. Sample output could be `25 too light, probably spoiled`.
+  1
 
-  .. raw:: html
+  3
+  4
+  6
+  7
 
-    <details>
-      <summary markdown="span"><b>Solution</b></summary>
+The same goes for keyword arguments but these are stored in a dictionary.
 
 .. code-block:: python
+  :linenos:
 
-def get_egg_label(mass):
-    # egg sizing machinery prints a label
-    egg_label = "Unlabelled"
-    if mass >= 90:
-        egg_label = "warning: egg might be dirty"
-    elif mass >= 85:
-        egg_label = "jumbo"
-    elif mass >= 70:
-        egg_label = "large"
-    elif mass < 70 and mass >= 55:
-        egg_label = "medium"
-    elif mass < 50:
-        egg_label = "too light, probably spoiled"
-    else:
-        egg_label = "small"
-    return egg_label
+  def print_quark(**kwargs):
+      for a in kwargs:
+          print(a, kwargs[a])
 
-  .. raw:: html
+  print_quark(year=1989, weekday='Monday')
 
-    </details>
+.. code-block::
 
--------------------
+  year 1989
+  weekday Monday
 
-.. attention:: ## Encapsulating Data Analysis
+Unpacking Function Inputs
+-------------------------
 
-Assume that the following code has been executed:
+Another use of `*` is to unpack iterables to give them to functions
 
 .. code-block:: python
+  :linenos:
+
+  def print_date(year, month, day):
+      joined = str(year) + '/' + str(month) + '/' + str(day)
+      print(joined)
+
+  date_tuple = (2023, 12, 31)
+  print_date(*date_tuple)
+
+  date_dict = {"year": 2021, "month": 4, "day": 1}
+  print_date(**date_dict)
+
+
+.. code-block::
+
+  2023/12/31
+  2021/4/1
 
-import pandas as pd
+-------------------
 
-data_asia = pd.read_csv('data/gapminder_gdp_asia.csv', index_col=0)
-japan = data_asia.loc['Japan']
+.. attention:: **Encapsulation of an If/Print Block**
 
-1. Complete the statements below to obtain the average GDP for Japan
-  across the years reported for the 1980s.
+  The code below will run on a label-printer for chicken eggs.  A digital scale will report a chicken egg mass (in grams) to the computer and then the computer will print a label.
 
   .. code-block:: python
+    :linenos:
+
+    import random
+    for i in range(10):
+
+        # simulating the mass of a chicken egg
+        # the (random) mass will be 70 +/- 20 grams
+        mass = 70 + 20.0 * (2.0 * random.random() - 1.0)
 
-  year = 1983
-  gdp_decade = 'gdpPercap_' + str(year // ____)
-  avg = (japan.loc[gdp_decade + ___] + japan.loc[gdp_decade + ___]) / 2
+        print(mass)
 
-2. Abstract the code above into a single function.
+        # egg sizing machinery prints a label
+        if mass >= 85:
+            print("jumbo")
+        elif mass >= 70:
+            print("large")
+        elif mass < 70 and mass >= 55:
+            print("medium")
+        else:
+            print("small")
+
+  The if-block that classifies the eggs might be useful in other situations, so to avoid repeating it, we could fold it into a function, `get_egg_label()`. Revising the program to use the function would give us this:
 
   .. code-block:: python
+    :linenos:
+
+    # revised version
+    import random
+    for i in range(10):
+
+        # simulating the mass of a chicken egg
+        # the (random) mass will be 70 +/- 20 grams
+        mass = 70 + 20.0 * (2.0 * random.random() - 1.0)
+
+        print(mass, get_egg_label(mass))
 
-  def avg_gdp_in_decade(country, continent, year):
-      data_countries = pd.read_csv('data/gapminder_gdp_'+___+'.csv',delimiter=',',index_col=0)
-      ____
-      ____
-      ____
-      return avg
 
-3. How would you generalize this function
-  if you did not know beforehand which specific years occurred as columns in the data?
-  For instance, what if we also had data from years ending in 1 and 9 for each decade?
-  (Hint: use the columns to filter out the ones that correspond to the decade,
-  instead of enumerating them in the code.)
+  1. Create a function definition for `get_egg_label()` that will work with the revised program above.  Note that the `get_egg_label()` function's return value will be important. Sample output from the above program would be `71.23 large`.
+  2. A dirty egg might have a mass of more than 90 grams, and a spoiled or broken egg will probably have a mass that's less than 50 grams.  Modify your `get_egg_label()` function to account for these error conditions. Sample output could be `25 too light, probably spoiled`.
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-1. The average GDP for Japan across the years reported for the 1980s is computed with:
-
   .. code-block:: python
+    :linenos:
+
+    def get_egg_label(mass):
+        # egg sizing machinery prints a label
+        egg_label = "Unlabelled"
+        if mass >= 90:
+            egg_label = "warning: egg might be dirty"
+        elif mass >= 85:
+            egg_label = "jumbo"
+        elif mass >= 70:
+            egg_label = "large"
+        elif mass < 70 and mass >= 55:
+            egg_label = "medium"
+        elif mass < 50:
+            egg_label = "too light, probably spoiled"
+        else:
+            egg_label = "small"
+        return egg_label
 
-  year = 1983
-  gdp_decade = 'gdpPercap_' + str(year // 10)
-  avg = (japan.loc[gdp_decade + '2'] + japan.loc[gdp_decade + '7']) / 2
+  .. raw:: html
 
-2. That code as a function is:
+    </details>
 
-  .. code-block:: python
+-------------------
 
-  def avg_gdp_in_decade(country, continent, year):
-      data_countries = pd.read_csv('data/gapminder_gdp_' + continent + '.csv', index_col=0)
-      c = data_countries.loc[country]
-      gdp_decade = 'gdpPercap_' + str(year // 10)
-      avg = (c.loc[gdp_decade + '2'] + c.loc[gdp_decade + '7'])/2
-      return avg
+.. attention:: **Encapsulating Data Analysis**
 
-3. To obtain the average for the relevant years, we need to loop over them:
+  Assume that the following code has been executed:
 
   .. code-block:: python
+    :linenos:
+
+    import pandas as pd
+
+    data_asia = pd.read_csv('data/gapminder_gdp_asia.csv', index_col=0)
+    japan = data_asia.loc['Japan']
+
+  1. Complete the statements below to obtain the average GDP for Japan across the years reported for the 1980s.
 
-  def avg_gdp_in_decade(country, continent, year):
-      data_countries = pd.read_csv('data/gapminder_gdp_' + continent + '.csv', index_col=0)
-      c = data_countries.loc[country]
+    .. code-block:: python
+      :linenos:
+
+      year = 1983
+      gdp_decade = 'gdpPercap_' + str(year // ____)
+      avg = (japan.loc[gdp_decade + ___] + japan.loc[gdp_decade + ___]) / 2
+
+  2. Abstract the code above into a single function.
+
+    .. code-block:: python
+      :linenos:
+
+      def avg_gdp_in_decade(country, continent, year):
+          data_countries = pd.read_csv('data/gapminder_gdp_'+___+'.csv',delimiter=',',index_col=0)
+          ____
+          ____
+          ____
+          return avg
+
+  3. How would you generalize this function if you did not know beforehand which specific years occurred as columns in the data? For instance, what if we also had data from years ending in 1 and 9 for each decade?
+
+  .. hint::
+
+    Use the columns to filter out the ones that correspond to the decade, instead of enumerating them in the code.
+
+  .. raw:: html
+
+    <details>
+      <summary markdown="span"><b>Solution</b></summary>
+
+  1. The average GDP for Japan across the years reported for the 1980s is computed with:
+
+    .. code-block:: python
+      :linenos:
+
+      year = 1983
       gdp_decade = 'gdpPercap_' + str(year // 10)
-      total = 0.0
-      num_years = 0
-      for yr_header in c.index: # c's index contains reported years
-          if yr_header.startswith(gdp_decade):
-              total = total + c.loc[yr_header]
-              num_years = num_years + 1
-      return total/num_years
+      avg = (japan.loc[gdp_decade + '2'] + japan.loc[gdp_decade + '7']) / 2
 
-The function can now be called by:
+  2. That code as a function is:
 
-.. code-block:: python
+    .. code-block:: python
+      :linenos:
 
-avg_gdp_in_decade('Japan','asia',1983)
+      def avg_gdp_in_decade(country, continent, year):
+          data_countries = pd.read_csv('data/gapminder_gdp_' + continent + '.csv', index_col=0)
+          c = data_countries.loc[country]
+          gdp_decade = 'gdpPercap_' + str(year // 10)
+          avg = (c.loc[gdp_decade + '2'] + c.loc[gdp_decade + '7'])/2
+          return avg
 
-.. code-block::
+  3. To obtain the average for the relevant years, we need to loop over them:
+
+    .. code-block:: python
+      :linenos:
 
-20880.023800000003
+      def avg_gdp_in_decade(country, continent, year):
+          data_countries = pd.read_csv('data/gapminder_gdp_' + continent + '.csv', index_col=0)
+          c = data_countries.loc[country]
+          gdp_decade = 'gdpPercap_' + str(year // 10)
+          total = 0.0
+          num_years = 0
+          for yr_header in c.index: # c's index contains reported years
+              if yr_header.startswith(gdp_decade):
+                  total = total + c.loc[yr_header]
+                  num_years = num_years + 1
+          return total/num_years
+
+  The function can now be called by:
+
+  .. code-block:: python
+    :linenos:
+
+    avg_gdp_in_decade('Japan','asia',1983)
+
+  .. code-block::
+
+    20880.023800000003
 
   .. raw:: html
 
@@ -550,68 +685,63 @@ avg_gdp_in_decade('Japan','asia',1983)
 
 -------------------
 
-.. attention:: ## Simulating a dynamical system
+.. attention:: **Simulating a dynamical system**
 
-In mathematics, a [dynamical system](https://en.wikipedia.org/wiki/Dynamical_system) is a system
-in which a function describes the time dependence of a point in a geometrical space. A canonical
-example of a dynamical system is the [logistic map](https://en.wikipedia.org/wiki/Logistic_map),
-a growth model that computes a new population density (between  0 and 1) based on the current
-density. In the model, time takes discrete values 0, 1, 2, ...
+  In mathematics, a `dynamical system <https://en.wikipedia.org/wiki/Dynamical_system>`__ is a system in which a function describes the time dependence of a point in a geometrical space. A canonical example of a dynamical system is the `logistic map <https://en.wikipedia.org/wiki/Logistic_map>`__, a growth model that computes a new population density (between  0 and 1) based on the current density. In the model, time takes discrete values 0, 1, 2, ...
 
-1. Define a function called `logistic_map` that takes two inputs: `x`, representing the current
-  population (at time `t`), and a parameter `r = 1`. This function should return a value
-  representing the state of the system (population) at time `t + 1`, using the mapping function:
+  1. Define a function called `logistic_map` that takes two inputs: `x`, representing the current population (at time `t`), and a parameter `r = 1`. This function should return a value representing the state of the system (population) at time `t + 1`, using the mapping function:
 
-  `f(t+1) = r * f(t) * [1 - f(t)]`
+    `f(t+1) = r * f(t) * [1 - f(t)]`
 
-2. Using a `for` or `while` loop, iterate the `logistic_map` function defined in part 1, starting
-  from an initial population of 0.5, for a period of time `t_final = 10`. Store the intermediate
-  results in a list so that after the loop terminates you have accumulated a sequence of values
-  representing the state of the logistic map at times `t = [0,1,...,t_final]` (11 values in total).
-  Print this list to see the evolution of the population.
+  2. Using a `for` or `while` loop, iterate the `logistic_map` function defined in part 1, starting from an initial population of 0.5, for a period of time `t_final = 10`. Store the intermediate results in a list so that after the loop terminates you have accumulated a sequence of values representing the state of the logistic map at times `t = [0,1,...,t_final]` (11 values in total). Print this list to see the evolution of the population.
 
-3. Encapsulate the logic of your loop into a function called `iterate` that takes the initial
-  population as its first input, the parameter `t_final` as its second input and the parameter
-  `r` as its third input. The function should return the list of values representing the state of
-  the logistic map at times `t = [0,1,...,t_final]`. Run this function for periods `t_final = 100`
-  and `1000` and print some of the values. Is the population trending toward a steady state?
+  3. Encapsulate the logic of your loop into a function called `iterate` that takes the initial population as its first input, the parameter `t_final` as its second input and the parameter `r` as its third input. The function should return the list of values representing the state of the logistic map at times `t = [0,1,...,t_final]`. Run this function for periods `t_final = 100` and `1000` and print some of the values. Is the population trending toward a steady state?
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-1. .. code-block:: python
+  1.
 
-  def logistic_map(x, r):
-      return r * x * (1 - x)
+  .. code-block:: python
+    :linenos:
 
-2. .. code-block:: python
+    def logistic_map(x, r):
+        return r * x * (1 - x)
 
-  initial_population = 0.5
-  t_final = 10
-  r = 1.0
-  population = [initial_population]
-  for t in range(t_final):
-      population.append( logistic_map(population[t], r) )
+  2.
 
-3. .. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-  def iterate(initial_population, t_final, r):
-      population = [initial_population]
-      for t in range(t_final):
-          population.append( logistic_map(population[t], r) )
-      return population
+    initial_population = 0.5
+    t_final = 10
+    r = 1.0
+    population = [initial_population]
+    for t in range(t_final):
+        population.append( logistic_map(population[t], r) )
 
-  for period in (10, 100, 1000):
-      population = iterate(0.5, period, 1)
-      print(population[-1])
+  3.
+
+  .. code-block:: python
+    :linenos:
+
+    def iterate(initial_population, t_final, r):
+        population = [initial_population]
+        for t in range(t_final):
+            population.append( logistic_map(population[t], r) )
+        return population
+
+    for period in (10, 100, 1000):
+        population = iterate(0.5, period, 1)
+        print(population[-1])
 
   .. code-block::
 
-  0.06945089389714401
-  0.009395779870614648
-  0.0009913908614406382
+    0.06945089389714401
+    0.009395779870614648
+    0.0009913908614406382
 
   The population seems to be approaching zero.
 
@@ -623,62 +753,55 @@ density. In the model, time takes discrete values 0, 1, 2, ...
 
 -------------------
 
-.. hint:: **## Using Functions With Conditionals in Pandas
+.. hint:: **Using Functions With Conditionals in Pandas**
 
-Functions will often contain conditionals.  Here is a short example that
-will indicate which quartile the argument is in based on hand-coded values
-for the quartile cut points.
+  Functions will often contain conditionals.  Here is a short example that will indicate which quartile the argument is in based on hand-coded values for the quartile cut points.
 
-.. code-block:: python
-
-def calculate_life_quartile(exp):
-    if exp < 58.41:
-        # This observation is in the first quartile
-        return 1
-    elif exp >= 58.41 and exp < 67.05:
-        # This observation is in the second quartile
-       return 2
-    elif exp >= 67.05 and exp < 71.70:
-        # This observation is in the third quartile
-       return 3
-    elif exp >= 71.70:
-        # This observation is in the fourth quartile
-       return 4
-    else:
-        # This observation has bad data
-       return None
-
-calculate_life_quartile(62.5)
+  .. code-block:: python
+    :linenos:
+
+    def calculate_life_quartile(exp):
+        if exp < 58.41:
+            # This observation is in the first quartile
+            return 1
+        elif exp >= 58.41 and exp < 67.05:
+            # This observation is in the second quartile
+          return 2
+        elif exp >= 67.05 and exp < 71.70:
+            # This observation is in the third quartile
+          return 3
+        elif exp >= 71.70:
+            # This observation is in the fourth quartile
+          return 4
+        else:
+            # This observation has bad data
+          return None
+
+    calculate_life_quartile(62.5)
 
-.. code-block::
+  .. code-block::
 
-2
+    2
 
-That function would typically be used within a `for` loop, but Pandas has
-a different, more efficient way of doing the same thing, and that is by
-*applying* a function to a dataframe or a portion of a dataframe.  Here
-is an example, using the definition above.
+  That function would typically be used within a `for` loop, but Pandas has a different, more efficient way of doing the same thing, and that is by *applying* a function to a dataframe or a portion of a dataframe.  Here is an example, using the definition above.
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-data = pd.read_csv('data/gapminder_all.csv')
-data['life_qrtl'] = data['lifeExp_1952'].apply(calculate_life_quartile)
+    data = pd.read_csv('data/gapminder_all.csv')
+    data['life_qrtl'] = data['lifeExp_1952'].apply(calculate_life_quartile)
 
-There is a lot in that second line, so let's take it piece by piece.
-On the right side of the `=` we start with `data['lifeExp']`, which is the
-column in the dataframe called `data` labeled `lifExp`.  We use the
-`apply()` to do what it says, apply the `calculate_life_quartile` to the
-value of this column for every row in the dataframe.
+  There is a lot in that second line, so let's take it piece by piece.
 
+  - On the right side of the `=` we start with `data['lifeExp']`, which is the column in the dataframe called `data` labeled `lifExp`.
+  - We use the `apply()` to do what it says, apply the `calculate_life_quartile` to the value of this column for every row in the dataframe.
 
 -------------------
 
 .. admonition:: **Summary**
 
-- Break programs down into functions to make them easier to understand.
-- Define a function using `def` with a name, parameters, and a block of code.
-- Defining a function does not run it.
-- Arguments in a function call are matched to its defined parameters.
-- Functions may return a result to their caller using `return`.
-
--------------------
+  - Break programs down into functions to make them easier to understand.
+  - Define a function using `def` with a name, parameters, and a block of code.
+  - Defining a function does not run it.
+  - Arguments in a function call are matched to its defined parameters.
+  - Functions may return a result to their caller using `return`.
diff --git a/lessons/17-scope.rst b/lessons/17-scope.rst
index 156c6becfedb9965aee99fea31fc330ac84aeb75..b4ba0b6d794ee7ec06129417000b49ee81194755 100644
--- a/lessons/17-scope.rst
+++ b/lessons/17-scope.rst
@@ -2,131 +2,153 @@
 Lesson 14 - Variable Scope
 ==========================
 
--------------------
 Teaching: 10 min
 Exercises: 10 min
+
 -------------------
 
 .. highlights:: **Highlights**
 
-- Identify local and global variables.
-- Identify parameters as local variables.
-- Read a traceback and determine the file, function, and line number on which the error occurred, the type of error, and the error message.
+  - Identify local and global variables.
+  - Identify parameters as local variables.
+  - Read a traceback and determine the file, function, and line number on which the error occurred, the type of error, and the error message.
 
 .. hint::
 
-- How do function calls actually work?
-- How can I determine where errors occurred?
+  - How do function calls actually work?
+  - How can I determine where errors occurred?
 
 -------------------
 
-## The scope of a variable is the part of a program that can 'see' that variable.
+Variable Scope
+==============
 
+- The scope of a variable is the part of a program that can 'see' that variable.
 - There are only so many sensible names for variables.
-- People using functions shouldn't have to worry about
-  what variable names the author of the function used.
-- People writing functions shouldn't have to worry about
-  what variable names the function's caller uses.
+- People using functions shouldn't have to worry about what variable names the author of the function used.
+- People writing functions shouldn't have to worry about what variable names the function's caller uses.
 - The part of a program in which a variable is visible is called its *scope*.
 
 .. code-block:: python
+  :linenos:
 
-pressure = 103.9
+  pressure = 103.9
 
-def adjust(t):
-    temperature = t * 1.43 / pressure
-    return temperature
+  def adjust(t):
+      temperature = t * 1.43 / pressure
+      return temperature
 
 - `pressure` is a *global variable*.
+
   - Defined outside any particular function.
   - Visible everywhere.
+
 - `t` and `temperature` are *local variables* in `adjust`.
+
   - Defined in the function.
   - Not visible in the main program.
-  - Remember: a function parameter is a variable
-    that is automatically assigned a value when the function is called.
+  - Remember: a function parameter is a variable that is automatically assigned a value when the function is called.
 
 .. code-block:: python
+  :linenos:
 
-print('adjusted:', adjust(0.9))
-print('temperature after call:', temperature)
+  print('adjusted:', adjust(0.9))
+  print('temperature after call:', temperature)
 
 .. code-block::
 
-adjusted: 0.01238691049085659
+  adjusted: 0.01238691049085659
+  Traceback (most recent call last):
+    File "/Users/swcarpentry/foo.py", line 8, in <module>
+      print('temperature after call:', temperature)
+  NameError: name 'temperature' is not defined
 
-.. code-block::
+We get a `NameError` because the variable temperature does not exist outside the function definition. It only exists in the scope of the function. A nested sequence of function calls, each with their respective variable scope is called `call stack`.
 
-Traceback (most recent call last):
-  File "/Users/swcarpentry/foo.py", line 8, in <module>
-    print('temperature after call:', temperature)
-NameError: name 'temperature' is not defined
+.. hint::
 
-.. attention:: ## Local and Global Variable Use
+  Most of the time errors occur in functions of libraries. Python is able to trace back where the last error occurred. However, this usually does not mean that there is a problem in the library, but rather a problem with your function inputs. So you should move up the call stack and slowly trace backwards where things go wrong.
 
-Trace the values of all variables in this program as it is executed.
-(Use '---' as the value of variables before and after they exist.)
+.. attention:: **Local and Global Variable Use**
 
-.. code-block:: python
+  Trace the values of all variables in this program as it is executed.
+  (Use '---' as the value of variables before and after they exist.)
+
+  .. code-block:: python
+    :linenos:
+
+    limit = 100
+    def clip(value):
+        return min(max(0.0, value), limit)
+    measurement = -22.5
+    print(clip(measurement))
+    print("Done!")
+
+  .. raw:: html
+
+    <details>
+      <summary markdown="span"><b>Solution</b></summary>
+
+  .. code-block::
+
+    1: limit = 100, value = -----, measurement = -----
+    2: limit = 100, value = -----, measurement = -----
+    3: limit = 100, value = -----, measurement = -----
+    4: limit = 100, value = -----, measurement = -22.5
+    5: limit = 100, value = -22.5, measurement = -22.5
+    6: limit = 100, value = -----, measurement = -22.5
 
-limit = 100
 
-def clip(value):
-    return min(max(0.0, value), limit)
+  .. raw:: html
 
-value = -22.5
-print(clip(value))
+    </details>
 
 -------------------
 
-.. attention:: ## Reading Error Messages
+.. attention:: **Reading Error Messages**
 
-Read the traceback below, and identify the following:
+  Read the traceback below, and identify the following:
 
-1. How many levels does the traceback have?
-2. What is the file name where the error occurred?
-3. What is the function name where the error occurred?
-4. On which line number in this function did the error occur?
-5. What is the type of error?
-6. What is the error message?
+  1. How many levels does the traceback have?
+  2. What is the file name where the error occurred?
+  3. What is the function name where the error occurred?
+  4. On which line number in this function did the error occur?
+  5. What is the type of error?
+  6. What is the error message?
 
-.. code-block::
+  .. code-block::
 
----------------------------------------------------------------------------
-KeyError                                  Traceback (most recent call last)
-<ipython-input-2-e4c4cbafeeb5> in <module>()
-      1 import errors_02
-----> 2 errors_02.print_friday_message()
+    ---------------------------------------------------------------------------
+    KeyError                                  Traceback (most recent call last)
+    <ipython-input-2-e4c4cbafeeb5> in <module>()
+          1 import errors_02
+    ----> 2 errors_02.print_friday_message()
 
-/Users/ghopper/thesis/code/errors_02.py in print_friday_message()
-     13
-     14 def print_friday_message():
----> 15     print_message("Friday")
+    /Users/ghopper/thesis/code/errors_02.py in print_friday_message()
+        13
+        14 def print_friday_message():
+    ---> 15     print_message("Friday")
 
-/Users/ghopper/thesis/code/errors_02.py in print_message(day)
-      9         "sunday": "Aw, the weekend is almost over."
-     10     }
----> 11     print(messages[day])
-     12
-     13
+    /Users/ghopper/thesis/code/errors_02.py in print_message(day)
+          9         "sunday": "Aw, the weekend is almost over."
+        10     }
+    ---> 11     print(messages[day])
+        12
+        13
 
-KeyError: 'Friday'
+    KeyError: 'Friday'
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-1. Three levels.
-2. `errors_02.py`
-3. `print_message`
-4. Line 11
-5. `KeyError`. These errors occur when we are trying to look up a key that does not exist (usually in a data
-  structure such as a dictionary). We can find more information about the `KeyError` and other built-in exceptions
-  in the [Python docs](https://docs.python.org/3/library/exceptions.html#KeyError).
-6. `KeyError: 'Friday'`
-
-
+  1. Three levels.
+  2. `errors_02.py`
+  3. `print_message`
+  4. Line 11
+  5. `KeyError`. These errors occur when we are trying to look up a key that does not exist (usually in a data structure such as a dictionary). We can find more information about the `KeyError` and other built-in exceptions in the `Python docs <https://docs.python.org/3/library/exceptions.html#KeyError>`__.
+  6. `KeyError: 'Friday'`
 
   .. raw:: html
 
@@ -136,4 +158,4 @@ KeyError: 'Friday'
 
 .. admonition:: **Summary**
 
-- The scope of a variable is the part of a program that can 'see' that variable.
+  - The scope of a variable is the part of a program that can 'see' that variable.
diff --git a/lessons/18-style.rst b/lessons/18-style.rst
index 116a57c3ebf3ba447bdea402ea74fefddadce121..5d256d539195f7fd4e5c0a31c91981b58cf59025 100644
--- a/lessons/18-style.rst
+++ b/lessons/18-style.rst
@@ -2,29 +2,29 @@
 Lesson 15 - Programming Style
 =============================
 
--------------------
 Teaching: 15 min
 Exercises: 15 min
+
 -------------------
 
 .. highlights:: **Highlights**
 
-- Provide sound justifications for basic rules of coding style.
-- Refactor one-page programs to make them more readable and justify the changes.
-- Use Python community coding standards (PEP-8).
+  - Provide sound justifications for basic rules of coding style.
+  - Refactor one-page programs to make them more readable and justify the changes.
+  - Use Python community coding standards (PEP-8).
 
 .. hint::
 
-- How can I make my programs more readable?
-- How do most programmers format their code?
-- How can programs check their own operation?
+  - How can I make my programs more readable?
+  - How do most programmers format their code?
+  - How can programs check their own operation?
 
 -------------------
 
-## Coding style
+Coding Style
+============
 
-A consistent coding style helps others (including our future selves) read and understand code more easily. Code is read much more often than it is written, and as the [Zen of Python](https://www.python.org/dev/peps/pep-0020) states, "Readability counts".
-Python proposed a standard style through one of its first Python Enhancement Proposals (PEP), [PEP8](https://www.python.org/dev/peps/pep-0008).
+A consistent coding style helps others (including our future selves) read and understand code more easily. Code is read much more often than it is written, and as the `Zen of Python <https://www.python.org/dev/peps/pep-0020>`__ states, "Readability counts". Python proposed a standard style through one of its first Python Enhancement Proposals (PEP), `PEP8 <https://www.python.org/dev/peps/pep-0008>`__.
 
 Some points worth highlighting:
 
@@ -32,130 +32,159 @@ Some points worth highlighting:
 - use clear, semantically meaningful variable names
 - use white-space, *not* tabs, to indent lines (tabs can cause problems across different text editors, operating systems, and version control systems)
 
-## Follow standard Python style in your code.
+The PEP-8 Style
+---------------
+
+- Follow standard Python style in your code.
+- `PEP8 <https://www.python.org/dev/peps/pep-0008>`__:
+
+  - a style guide for Python that discusses topics such as
+  - how to name variables,
+  - how to indent your code,
+  - how to structure your `import` statements,
+  - etc.
 
-- [PEP8](https://www.python.org/dev/peps/pep-0008):
-  a style guide for Python that discusses topics such as how to name variables,
-  how to indent your code,
-  how to structure your `import` statements,
-  etc.
-  Adhering to PEP8 makes it easier for other Python developers to read and understand your code, and to understand what their contributions should look like.
-- To check your code for compliance with PEP8, you can use the [pycodestyle application](https://pypi.org/project/pycodestyle/) and tools like the [black code formatter](https://github.com/psf/black) can automatically format your code to conform to PEP8 and pycodestyle (a Jupyter notebook formatter also exists [nb\_black](https://github.com/dnanhkhoa/nb_black)).
-- Some groups and organizations follow different style guidelines besides PEP8. For example, the [Google style guide on Python](https://google.github.io/styleguide/pyguide.html) makes slightly different recommendations. Google wrote an application that can help you format your code in either their style or PEP8 called [yapf](https://github.com/google/yapf/).
+- Adhering to PEP8 makes it easier for other Python developers to read and understand your code, and to understand what their contributions should look like.
+- To check your code for compliance with PEP8, you can use the `pycodestyle application <https://pypi.org/project/pycodestyle/>`__ and tools like the `black code formatter <https://github.com/psf/black>`__ can automatically format your code to conform to PEP8 and pycodestyle (a Jupyter notebook formatter also exists `nb\_black <https://github.com/dnanhkhoa/nb_black>`__).
+- Some groups and organizations follow different style guidelines besides PEP8. For example, the `Google style guide on Python <https://google.github.io/styleguide/pyguide.html>`__ makes slightly different recommendations. Google wrote an application that can help you format your code in either their style or PEP8 called `yapf <https://github.com/google/yapf/>`__.
 - With respect to coding style, the key is *consistency*. Choose a style for your project be it PEP8, the Google style, or something else and do your best to ensure that you and anyone else you are collaborating with sticks to it. Consistency within a project is often more impactful than the particular style used. A consistent style will make your software easier to read and understand for others and for your future self.
 
-## Use assertions to check for internal errors.
 
-Assertions are a simple but powerful method for making sure that the context in which your code is executing is as you expect.
+Defensive Programming
+=====================
+
+Assertions
+----------
+
+- Use assertions to check for internal errors.
+- Assertions are a simple but powerful method for making sure that the context in which your code is executing is as you expect.
 
 .. code-block:: python
+  :linenos:
 
-def calc_bulk_density(mass, volume):
-    '''Return dry bulk density = powder mass / powder volume.'''
-    assert volume > 0
-    return mass / volume
+  def calc_bulk_density(mass, volume):
+      '''Return dry bulk density = powder mass / powder volume.'''
+      assert volume > 0
+      return mass / volume
 
 If the assertion is `False`, the Python interpreter raises an `AssertionError` runtime exception. The source code for the expression that failed will be displayed as part of the error message. To ignore assertions in your code run the interpreter with the '-O' (optimize) switch. Assertions should contain only simple checks and never change the state of the program. For example, an assertion should never contain an assignment.
 
-## Use docstrings to provide builtin help.
+Docstrings
+----------
 
-If the first thing in a function is a character string that is not assigned directly to a variable, Python attaches it to the function, accessible via the builtin help function. This string that provides documentation is also known as a *docstring*.
+Use docstrings to provide builtin help. If the first thing in a function is a character string that is not assigned directly to a variable, Python attaches it to the function, accessible via the builtin help function. This string that provides documentation is also known as a *docstring*.
 
 .. code-block:: python
+  :linenos:
 
-def average(values):
-    "Return average of values, or None if no values are supplied."
+  def average(values):
+      "Return average of values, or None if no values are supplied."
 
-    if len(values) == 0:
-        return None
-    return sum(values) / len(values)
+      if len(values) == 0:
+          return None
+      return sum(values) / len(values)
 
-help(average)
+  help(average)
 
 .. code-block::
 
-Help on function average in module __main__:
+  Help on function average in module __main__:
+
+  average(values)
+      Return average of values, or None if no values are supplied.
+
+Typing Parameters
+-----------------
 
-average(values)
-    Return average of values, or None if no values are supplied.
+Python is a dynamically typed language and therefore the type of a variable is determined at runtime. Other languages, such as the underlying `C` or `Fortran` code are statically typed languages. Therefore, one has to define the type of a variable before using it.
 
-.. hint:: **## Multiline Strings
+As functions usually do complex calculations that are not known to the enduser and it is not easy to find out what data type is expected for each arguments, Python allows for type hints (since Python 3.5). These show the necessary data type for the function arguments in order to work properly. Many IDEs support function inspection (hovering over the function name) to identify the function arguments. This is aided by the typehints.
 
-Often use *multiline strings* for documentation.
-These start and end with three quote characters (either single or double)
-and end with three matching characters.
+Type hints are given after the parameters followed by a colon. The supposed output is given after a `->` symbol.
 
 .. code-block:: python
+  :linenos:
 
-"""This string spans
-multiple lines.
+  def print_date(year:int, month:int, day:int, weekday:str) -> None:
+      joined = str(year) + '/' + str(month) + '/' + str(day)
+      print(joined)
 
-Blank lines are allowed."""
+
+
+.. hint:: **Multiline Strings**
+
+  Often use *multiline strings* for documentation. These start and end with three quote characters (either single or double) and end with three matching characters.
+
+  .. code-block:: python
+    :linenos:
+
+    """
+    This string spans
+    multiple lines.
+
+    Blank lines are allowed.
+    """
 
 -------------------
 
-.. attention:: ## What Will Be Shown?
+.. attention:: **What Will Be Shown?**
 
-Highlight the lines in the code below that will be available as online help.
-Are there lines that should be made available, but won't be?
-Will any lines produce a syntax error or a runtime error?
+  Highlight the lines in the code below that will be available as online help. Are there lines that should be made available, but won't be? Will any lines produce a syntax error or a runtime error?
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-"Find maximum edit distance between multiple sequences."
-# This finds the maximum distance between all sequences.
+    "Find maximum edit distance between multiple sequences."
+    # This finds the maximum distance between all sequences.
 
-def overall_max(sequences):
-    '''Determine overall maximum edit distance.'''
+    def overall_max(sequences):
+        '''Determine overall maximum edit distance.'''
 
-    highest = 0
-    for left in sequences:
-        for right in sequences:
-            '''Avoid checking sequence against itself.'''
-            if left != right:
-                this = edit_distance(left, right)
-                highest = max(highest, this)
+        highest = 0
+        for left in sequences:
+            for right in sequences:
+                '''Avoid checking sequence against itself.'''
+                if left != right:
+                    this = edit_distance(left, right)
+                    highest = max(highest, this)
 
-    # Report.
-    return highest
+        # Report.
+        return highest
 
 -------------------
 
-.. attention:: ## Document This
+.. attention:: **Document This**
 
-Use comments to describe and help others understand potentially unintuitive
-sections or individual lines of code. They are especially useful to whoever
-may need to understand and edit your code in the future, including yourself.
+  Use comments to describe and help others understand potentially unintuitive sections or individual lines of code. They are especially useful to whoever may need to understand and edit your code in the future, including yourself.
 
-Use docstrings to document the acceptable inputs and expected outputs of a method
-or class, its purpose, assumptions and intended behavior. Docstrings are displayed
-when a user invokes the builtin `help` method on your method or class.
+  Use docstrings to document the acceptable inputs and expected outputs of a method or class, its purpose, assumptions and intended behavior. Docstrings are displayed when a user invokes the builtin `help` method on your method or class.
 
-Turn the comment in the following function into a docstring
-and check that `help` displays it properly.
+  Turn the comment in the following function into a docstring and check that `help` displays it properly.
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-def middle(a, b, c):
-    # Return the middle value of three.
-    # Assumes the values can actually be compared.
-    values = [a, b, c]
-    values.sort()
-    return values[1]
+    def middle(a, b, c):
+        # Return the middle value of three.
+        # Assumes the values can actually be compared.
+        values = [a, b, c]
+        values.sort()
+        return values[1]
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-def middle(a, b, c):
-    '''Return the middle value of three.
-    Assumes the values can actually be compared.'''
-    values = [a, b, c]
-    values.sort()
-    return values[1]
+    def middle(a, b, c):
+        '''Return the middle value of three.
+        Assumes the values can actually be compared.'''
+        values = [a, b, c]
+        values.sort()
+        return values[1]
 
   .. raw:: html
 
@@ -163,82 +192,81 @@ def middle(a, b, c):
 
 -------------------
 
-.. attention:: ## Clean Up This Code
+.. attention:: **Clean Up This Code**
 
-1. Read this short program and try to predict what it does.
-2. Run it: how accurate was your prediction?
-3. Refactor the program to make it more readable.
-  Remember to run it after each change to ensure its behavior hasn't changed.
-4. Compare your rewrite with your neighbor's.
-  What did you do the same?
-  What did you do differently, and why?
+  1. Read this short program and try to predict what it does.
+  2. Run it: how accurate was your prediction?
+  3. Refactor the program to make it more readable. Remember to run it after each change to ensure its behavior hasn't changed.
+  4. Compare your rewrite with your neighbor's. What did you do the same? What did you do differently, and why?
 
-.. code-block:: python
+  .. code-block:: python
+    :linenos:
 
-n = 10
-s = 'et cetera'
-print(s)
-i = 0
-while i < n:
-    # print('at', j)
-    new = ''
-    for j in range(len(s)):
-        left = j-1
-        right = (j+1)%len(s)
-        if s[left]==s[right]: new = new + '-'
-        else: new = new + '*'
-    s=''.join(new)
+    n = 10
+    s = 'et cetera'
     print(s)
-    i += 1
+    i = 0
+    while i < n:
+        # print('at', j)
+        new = ''
+        for j in range(len(s)):
+            left = j-1
+            right = (j+1)%len(s)
+            if s[left]==s[right]: new = new + '-'
+            else: new = new + '*'
+        s=''.join(new)
+        print(s)
+        i += 1
 
   .. raw:: html
 
     <details>
       <summary markdown="span"><b>Solution</b></summary>
 
-Here's one solution.
-
-.. code-block:: python
-
-def string_machine(input_string, iterations):
-    """
-    Takes input_string and generates a new string with -'s and *'s
-    corresponding to characters that have identical adjacent characters
-    or not, respectively.  Iterates through this procedure with the resultant
-    strings for the supplied number of iterations.
-    """
-    print(input_string)
-    input_string_length = len(input_string)
-    old = input_string
-    for i in range(iterations):
-        new = ''
-        # iterate through characters in previous string
-        for j in range(input_string_length):
-            left = j-1
-            right = (j+1) % input_string_length  # ensure right index wraps around
-            if old[left] == old[right]:
-                new = new + '-'
-            else:
-                new = new + '*'
-        print(new)
-        # store new string as old
-        old = new
-
-string_machine('et cetera', 10)
-
-.. code-block::
-
-et cetera
-*****-***
-----*-*--
----*---*-
---*-*-*-*
-**-------
-***-----*
---**---**
-*****-***
-----*-*--
----*---*-
+  Here's one solution.
+
+  .. code-block:: python
+    :linenos:
+
+    def string_machine(input_string, iterations):
+        """
+        Takes input_string and generates a new string with -'s and *'s
+        corresponding to characters that have identical adjacent characters
+        or not, respectively.  Iterates through this procedure with the resultant
+        strings for the supplied number of iterations.
+        """
+        print(input_string)
+        input_string_length = len(input_string)
+        old = input_string
+        for i in range(iterations):
+            new = ''
+            # iterate through characters in previous string
+            for j in range(input_string_length):
+                left = j-1
+                right = (j+1) % input_string_length  # ensure right index wraps around
+                if old[left] == old[right]:
+                    new = new + '-'
+                else:
+                    new = new + '*'
+            print(new)
+            # store new string as old
+            old = new
+
+    string_machine('et cetera', 10)
+
+  .. code-block::
+
+    et cetera
+    *****-***
+    ----*-*--
+    ---*---*-
+    --*-*-*-*
+    **-------
+    ***-----*
+    --**---**
+    *****-***
+    ----*-*--
+    ---*---*-
 
   .. raw:: html
 
@@ -248,5 +276,5 @@ et cetera
 
 .. admonition:: **Summary**
 
-- Follow standard Python style in your code.
-- Use docstrings to provide builtin help.
+  - Follow standard Python style in your code.
+  - Use docstrings to provide builtin help.