diff --git a/.gitignore b/.gitignore
index 38ce427..f9ad6cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+#### Python Section
+
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -121,3 +123,48 @@ dmypy.json
# Pyre type checker
.pyre/
+
+# VS Code Settings
+.vscode/
+
+
+#### MacOs Section
+
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+#### Hugo Section
+
+# generated files by hugo
+docs/
+/public/
+/resources/_gen/
+
+# executable may be added to repository
+hugo.exe
+hugo.darwin
+hugo.linux
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..4b26f77
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,19 @@
+Copyright (c) 2013-2018 GitHub, Inc. and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
index f71fc43..8d8fb9c 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,15 @@
-# Front End Masters - Two Day Python Workshop
+# Frontend Masters - Fundamentals & Intermediate Python Courses
-These are the resources for Nina Zakharenko's Two Day Intro to and Intermediate Python Course
+These are the resources for Nina Zakharenko's Python Fundamentals and Intermediate Python Courses
+
+* [Watch Python Fundamentals on Frontend Masters](https://frontendmasters.com/courses/python/)
The majority of the content can be found on the course website.
-* [Course Website - learnpython.netlify.com](https://learnpython.netlify.com)
+* [Course Website - https://www.learnpython.dev](https://www.learnpython.dev)
* [This Repo - git.io/python3](https://git.io/python3)
-* [Follow Nina on Twitter](https://twitter.com/nnja)
\ No newline at end of file
+* [Follow Nina on Twitter](https://twitter.com/nnja)
+
+## License
+
+The content of this project itself is licensed under the [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) license](https://creativecommons.org/licenses/by-nc-nd/4.0/), and the underlying source code used to format and display that content, along with the code exercises is licensed under the [MIT license](LICENSE.md).
\ No newline at end of file
diff --git a/pyworkshop/.DS_Store b/pyworkshop/.DS_Store
deleted file mode 100644
index 3e5c59c..0000000
Binary files a/pyworkshop/.DS_Store and /dev/null differ
diff --git a/pyworkshop/intro_python/chapter2/exercise.py b/pyworkshop/1_intro_python/chapter2/exercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter2/exercise.py
rename to pyworkshop/1_intro_python/chapter2/exercise.py
diff --git a/pyworkshop/intro_python/chapter3/exercise.py b/pyworkshop/1_intro_python/chapter3/exercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter3/exercise.py
rename to pyworkshop/1_intro_python/chapter3/exercise.py
diff --git a/pyworkshop/intro_python/chapter4/dicts_exercise.py b/pyworkshop/1_intro_python/chapter4/dicts_exercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter4/dicts_exercise.py
rename to pyworkshop/1_intro_python/chapter4/dicts_exercise.py
diff --git a/pyworkshop/intro_python/chapter4/lists_exercise.py b/pyworkshop/1_intro_python/chapter4/lists_exercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter4/lists_exercise.py
rename to pyworkshop/1_intro_python/chapter4/lists_exercise.py
diff --git a/pyworkshop/intro_python/chapter4/mutability_exercise.py b/pyworkshop/1_intro_python/chapter4/mutability_exercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter4/mutability_exercise.py
rename to pyworkshop/1_intro_python/chapter4/mutability_exercise.py
diff --git a/pyworkshop/intro_python/chapter4/sets_excercise.py b/pyworkshop/1_intro_python/chapter4/sets_excercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter4/sets_excercise.py
rename to pyworkshop/1_intro_python/chapter4/sets_excercise.py
diff --git a/pyworkshop/intro_python/chapter4/tuples_exercise.py b/pyworkshop/1_intro_python/chapter4/tuples_exercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter4/tuples_exercise.py
rename to pyworkshop/1_intro_python/chapter4/tuples_exercise.py
diff --git a/pyworkshop/intro_python/chapter5/exercise.py b/pyworkshop/1_intro_python/chapter5/exercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter5/exercise.py
rename to pyworkshop/1_intro_python/chapter5/exercise.py
diff --git a/pyworkshop/intro_python/chapter6/exercise.py b/pyworkshop/1_intro_python/chapter6/exercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter6/exercise.py
rename to pyworkshop/1_intro_python/chapter6/exercise.py
diff --git a/pyworkshop/intro_python/chapter7/cities.json b/pyworkshop/1_intro_python/chapter7/cities.json
similarity index 100%
rename from pyworkshop/intro_python/chapter7/cities.json
rename to pyworkshop/1_intro_python/chapter7/cities.json
diff --git a/pyworkshop/1_intro_python/chapter7/exceptions.py b/pyworkshop/1_intro_python/chapter7/exceptions.py
new file mode 100644
index 0000000..07840d1
--- /dev/null
+++ b/pyworkshop/1_intro_python/chapter7/exceptions.py
@@ -0,0 +1,7 @@
+# this will throw an error
+try:
+ int("a")
+except ValueError as e:
+ print("oops, you can't do that!", e)
+
+print("this is the end of my program")
\ No newline at end of file
diff --git a/pyworkshop/intro_python/chapter7/file_exercise.py b/pyworkshop/1_intro_python/chapter7/file_exercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter7/file_exercise.py
rename to pyworkshop/1_intro_python/chapter7/file_exercise.py
diff --git a/pyworkshop/1_intro_python/chapter7/hello.py b/pyworkshop/1_intro_python/chapter7/hello.py
new file mode 100644
index 0000000..5ca4f59
--- /dev/null
+++ b/pyworkshop/1_intro_python/chapter7/hello.py
@@ -0,0 +1,4 @@
+greetings = ["Hello", "Bonjour", "Hola"]
+
+for greeting in greetings:
+ print(f"{greeting}, World!")
\ No newline at end of file
diff --git a/pyworkshop/1_intro_python/chapter7/mystery.py b/pyworkshop/1_intro_python/chapter7/mystery.py
new file mode 100644
index 0000000..3f4e908
--- /dev/null
+++ b/pyworkshop/1_intro_python/chapter7/mystery.py
@@ -0,0 +1,14 @@
+def mystery():
+ num = 10 * 3
+
+ if num == 10:
+ print("condition 10")
+ num = num * 10
+ elif num == 30:
+ print("condition 30")
+ num = num * 30
+
+ print(f"num was {num}")
+ return num
+
+print(mystery())
\ No newline at end of file
diff --git a/pyworkshop/1_intro_python/chapter7/name.py b/pyworkshop/1_intro_python/chapter7/name.py
new file mode 100644
index 0000000..bd85767
--- /dev/null
+++ b/pyworkshop/1_intro_python/chapter7/name.py
@@ -0,0 +1,2 @@
+name = "Nina"
+print(name)
\ No newline at end of file
diff --git a/pyworkshop/1_intro_python/chapter7/name_lib.py b/pyworkshop/1_intro_python/chapter7/name_lib.py
new file mode 100644
index 0000000..484049a
--- /dev/null
+++ b/pyworkshop/1_intro_python/chapter7/name_lib.py
@@ -0,0 +1,8 @@
+def upper_case_name(name):
+ return name.upper()
+
+if __name__ == "__main__":
+ name = "Nina"
+ name_upper = upper_case_name(name)
+ print(f"Upper case name is {name_upper}")
+ print("dunder name", __name__)
\ No newline at end of file
diff --git a/pyworkshop/1_intro_python/chapter7/other_program.py b/pyworkshop/1_intro_python/chapter7/other_program.py
new file mode 100644
index 0000000..2facd7e
--- /dev/null
+++ b/pyworkshop/1_intro_python/chapter7/other_program.py
@@ -0,0 +1,6 @@
+import name_lib
+
+my_name = "Fred"
+upper_name = name_lib.upper_case_name(my_name)
+
+print(f"In my own code, upper name is {upper_name}")
\ No newline at end of file
diff --git a/pyworkshop/1_intro_python/chapter8/dog.py b/pyworkshop/1_intro_python/chapter8/dog.py
new file mode 100644
index 0000000..4e30f25
--- /dev/null
+++ b/pyworkshop/1_intro_python/chapter8/dog.py
@@ -0,0 +1,13 @@
+import requests
+
+api_url = "http://shibe.online/api/shibes?count=1"
+
+params = {"count": 10}
+response = requests.get(api_url, params=params)
+
+status_code = response.status_code
+print("status code: ", status_code)
+
+response_json = response.json()
+print(response_json)
+print(response.url)
\ No newline at end of file
diff --git a/pyworkshop/intro_python/chapter8/exercise.py b/pyworkshop/1_intro_python/chapter8/exercise.py
similarity index 100%
rename from pyworkshop/intro_python/chapter8/exercise.py
rename to pyworkshop/1_intro_python/chapter8/exercise.py
diff --git a/pyworkshop/2_intermediate_python/chapter1/exercise.py b/pyworkshop/2_intermediate_python/chapter1/exercise.py
new file mode 100644
index 0000000..e0b0f11
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter1/exercise.py
@@ -0,0 +1,25 @@
+my_string = str(100)
+my_string
+type(my_string)
+
+my_int = int(my_string)
+print(my_int)
+type(my_int)
+
+float("3.1415")
+int(3.1415)
+
+# Converting between lists and strings
+my_list = list("hello")
+print(my_list)
+str(my_list)
+
+# String join method
+''.join(my_list)
+','.join(my_list)
+'-'.join(my_list)
+
+# String split
+my_string = "the,quick,brown,fox"
+my_string.split(",")
+
diff --git a/pyworkshop/2_intermediate_python/chapter2/exercise.py b/pyworkshop/2_intermediate_python/chapter2/exercise.py
new file mode 100644
index 0000000..645c974
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter2/exercise.py
@@ -0,0 +1,27 @@
+# Comprehensions
+my_list = [num for num in range(0, 100) if num % 2 == 0]
+print(my_list)
+
+import random
+my_dict = {num:random.randint(0, 100) for num in my_list}
+print(my_dict)
+
+my_set = {num for num in my_dict.values()}
+print(my_set)
+
+# Slicing
+my_list = [num for num in range(0, 100)]
+my_slice = my_list[30:70:2]
+print(my_slice)
+
+my_backwards_slice = my_slice[::-1]
+print(my_backwards_slice)
+
+# Zip
+names = ["Nina", "Max", "Floyd", "Lloyd"]
+scores = [random.randint(0, 100) for name in names]
+print(scores)
+
+for name, score in zip(names, scores):
+ print(f"{name} got a score of {score}")
+
diff --git a/pyworkshop/2_intermediate_python/chapter3/exercise_part1.py b/pyworkshop/2_intermediate_python/chapter3/exercise_part1.py
new file mode 100644
index 0000000..972d222
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter3/exercise_part1.py
@@ -0,0 +1,13 @@
+class Vehicle:
+
+ def __init__(self, make, model, fuel="gas"):
+ self.make = make
+ self.model = model
+ self.fuel = fuel
+
+daily_driver = Vehicle("Subaru", "Crosstrek")
+# By default, this is how python represents our object:
+print(daily_driver)
+
+# The variables we saved to the instance are available like this:
+print(f"I drive a {daily_driver.make} {daily_driver.model}. It runs on {daily_driver.fuel}.")
diff --git a/pyworkshop/2_intermediate_python/chapter3/exercise_part2.py b/pyworkshop/2_intermediate_python/chapter3/exercise_part2.py
new file mode 100644
index 0000000..f030092
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter3/exercise_part2.py
@@ -0,0 +1,19 @@
+class Vehicle:
+
+ number_of_wheels = 4
+
+ def __init__(self, make, model, fuel="gas"):
+ self.make = make
+ self.model = model
+ self.fuel = fuel
+
+daily_driver = Vehicle("Subaru", "Crosstrek")
+
+daily_driver.number_of_wheels = 3
+
+# Instance variables
+print(f"I drive a {daily_driver.make} {daily_driver.model}. It runs on {daily_driver.fuel}.")
+print(f"My {daily_driver.model} has {daily_driver.number_of_wheels} wheels.")
+
+# Class variable
+print(f"Most vehicles have {Vehicle.number_of_wheels} wheels.")
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter3/exercise_part3.py b/pyworkshop/2_intermediate_python/chapter3/exercise_part3.py
new file mode 100644
index 0000000..110db1e
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter3/exercise_part3.py
@@ -0,0 +1,37 @@
+class Vehicle:
+
+ def __init__(self, make, model, fuel="gas"):
+ self.make = make
+ self.model = model
+ self.fuel = fuel
+
+
+class Car(Vehicle):
+
+ number_of_wheels = 4
+
+
+class Truck(Vehicle):
+
+ number_of_wheels = 6
+
+ def __init__(self, make, model, fuel="diesel"):
+ super().__init__(make, model, fuel)
+
+
+daily_driver = Car("Subaru", "Crosstrek")
+print(f"I drive a {daily_driver.make} {daily_driver.model}. "
+ f"It uses {daily_driver.fuel} and has {daily_driver.number_of_wheels} wheels.")
+
+truck = Truck("Ford", "F350")
+print(f"I also have a {truck.make} {truck.model}. "
+ f"It uses {truck.fuel} and has {truck.number_of_wheels} wheels.")
+
+print(
+ f"My daily driver is a {type(daily_driver)} and my truck is a {type(truck)}")
+
+print(f"Is my daily driver a car? {isinstance(daily_driver, Car)}")
+print(f"Is my truck a Vehicle? {isinstance(truck, Vehicle)}")
+print(f"Is my truck a Car? {isinstance(truck, Car)}")
+
+print(f"Is a Truck a subclass of Vehicle? {issubclass(Truck, Vehicle)}")
diff --git a/pyworkshop/2_intermediate_python/chapter3/gh_models.py b/pyworkshop/2_intermediate_python/chapter3/gh_models.py
new file mode 100644
index 0000000..5d4d301
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter3/gh_models.py
@@ -0,0 +1,12 @@
+class GitHubRepo:
+
+ def __init__(self, name, language, num_stars):
+ self.name = name
+ self.language = language
+ self.num_stars = num_stars
+
+ def __str__(self):
+ return f"-> {self.name} is a {self.language} repo with {self.num_stars} stars."
+
+ def __repr__(self):
+ return f'GitHubRepo({self.name}, {self.language}, {self.num_stars})'
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter4/exercise_part1_indentationerrors.py b/pyworkshop/2_intermediate_python/chapter4/exercise_part1_indentationerrors.py
new file mode 100644
index 0000000..e77f338
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter4/exercise_part1_indentationerrors.py
@@ -0,0 +1,2 @@
+def my_function():
+print("Hello!")
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter4/exercise_part2_syntaxerrors.py b/pyworkshop/2_intermediate_python/chapter4/exercise_part2_syntaxerrors.py
new file mode 100644
index 0000000..a59cb73
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter4/exercise_part2_syntaxerrors.py
@@ -0,0 +1,2 @@
+a = [4,
+x = 5
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter4/exercise_part3_zerodivision.py b/pyworkshop/2_intermediate_python/chapter4/exercise_part3_zerodivision.py
new file mode 100644
index 0000000..71de68b
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter4/exercise_part3_zerodivision.py
@@ -0,0 +1,2 @@
+# Will cause a ZeroDivisionError
+a = 1 / 0
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter4/exercise_part4_custom_exceptions.py b/pyworkshop/2_intermediate_python/chapter4/exercise_part4_custom_exceptions.py
new file mode 100644
index 0000000..467722d
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter4/exercise_part4_custom_exceptions.py
@@ -0,0 +1,6 @@
+class MyException(Exception):
+ def __init__(self, message):
+ new_message = f"!!!ERROR!!! {message}"
+ super().__init__(new_message)
+
+raise MyException("Something went wrong!")
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter4/exercise_part5_try_except.py b/pyworkshop/2_intermediate_python/chapter4/exercise_part5_try_except.py
new file mode 100644
index 0000000..c529758
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter4/exercise_part5_try_except.py
@@ -0,0 +1,11 @@
+try:
+ my_dict = {"hello": "world"}
+ print(my_dict["foo"])
+except KeyError:
+ print("Oh no! That key doesn't exist")
+
+try:
+ my_dict = {"hello": "world"}
+ print(my_dict["foo"])
+except KeyError as key_error:
+ print(f"Oh no! The key {key_error} doesn't exist!")
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter4/exercise_part6_reraising.py b/pyworkshop/2_intermediate_python/chapter4/exercise_part6_reraising.py
new file mode 100644
index 0000000..560aaf0
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter4/exercise_part6_reraising.py
@@ -0,0 +1,8 @@
+while True:
+ for divisor in range(5, -1, -1):
+ try:
+ quotient = 10 / divisor
+ print(f"10 / {divisor} = {quotient}")
+ except ZeroDivisionError:
+ print("Oops! We tried to divide by zero!")
+ raise RuntimeError
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter4/gh_exceptions.py b/pyworkshop/2_intermediate_python/chapter4/gh_exceptions.py
new file mode 100644
index 0000000..125fe87
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter4/gh_exceptions.py
@@ -0,0 +1,13 @@
+"""
+GitHub API Application: Custom Exception Classes
+"""
+
+class GitHubApiException(Exception):
+
+ def __init__(self, status_code):
+ if status_code == 403:
+ message = "Rate limit reached. Please wait a minute and try again."
+ else:
+ message = f"HTTP Status Code was: {status_code}."
+
+ super().__init__(message)
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter5/exercise.py b/pyworkshop/2_intermediate_python/chapter5/exercise.py
new file mode 100644
index 0000000..c420d4c
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter5/exercise.py
@@ -0,0 +1,22 @@
+
+# Using os
+import os
+
+my_folder = os.getcwd()
+print(f"Here are the files in {my_folder}:")
+
+with os.scandir(my_folder) as folder:
+ for entry in folder:
+ print(f" - {entry.name}")
+
+# Using sys
+
+import sys
+
+arguments = sys.argv
+print(f"We received the following arguments:")
+
+for arg in arguments:
+ print(f" - {arg}")
+
+print(f"We are running on a '{sys.platform}' machine")
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter6/cli_exercise.py b/pyworkshop/2_intermediate_python/chapter6/cli_exercise.py
new file mode 100644
index 0000000..8dd2d72
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter6/cli_exercise.py
@@ -0,0 +1,5 @@
+import sys
+
+args = sys.argv
+
+print(args)
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter6/cli_exercise_input.py b/pyworkshop/2_intermediate_python/chapter6/cli_exercise_input.py
new file mode 100644
index 0000000..975979e
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter6/cli_exercise_input.py
@@ -0,0 +1,5 @@
+name = input("Hello, what is your name? ")
+
+birthday_string = input(f"Hello {name.strip()}. Please enter your birthday in MM/DD/YYYY format: ")
+
+print(f"Hello {name}. Your birthday is on {birthday_string}.")
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter6/optional_adv_exercise.py b/pyworkshop/2_intermediate_python/chapter6/optional_adv_exercise.py
new file mode 100755
index 0000000..6479c0a
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter6/optional_adv_exercise.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+
+"""
+A small command line Python program that uses the GitHub search API to list
+the top projects by language, based on stars.
+
+GitHub Search API documentation: https://developer.github.com/v3/search/
+
+Requests to this endpoint are rate limited to 10 requests per
+minute per IP address.
+"""
+
+import sys
+import requests
+
+
+GITHUB_API_URL = "https://api.github.com/search/repositories"
+
+
+class GitHubApiException(Exception):
+
+ def __init__(self, status_code):
+ if status_code == 403:
+ message = "Rate limit reached. Please wait a minute and try again."
+ else:
+ message = f"HTTP Status Code was: {status_code}."
+
+ super().__init__("A GitHub API Error Occurred: " + message)
+
+
+class GitHubRepo:
+ """
+ A class used to represent a single GitHub Repository.
+ """
+
+ def __init__(self, name, language, num_stars):
+ self.name = name
+ self.language = language
+ self.num_stars = num_stars
+
+ def __str__(self):
+ return f"-> {self.name} is a {self.language} repo with {self.num_stars} stars."
+
+ def __repr__(self):
+ return f'GitHubRepo({self.name}, {self.language}, {self.num_stars})'
+
+
+def create_query(languages, min_stars):
+ """
+ Create the query string for the GitHub search API,
+ based on the minimum amount of stars for a project, and
+ the provided programming languages.
+ """
+ # Notice we are calling .strip() on each language, to clear it of leading
+ # and trailing whitespace
+ query = " ".join(f"language:{language.strip()}" for language in languages)
+ query = query + f" stars:>{min_stars}"
+ return query
+
+
+def repos_with_most_stars(languages, min_stars=50000, sort="stars", order="desc"):
+ query = create_query(languages, min_stars)
+ parameters = {"q": query, "sort": sort, "order": order}
+ response = requests.get(GITHUB_API_URL, params=parameters)
+
+ if response.status_code != 200:
+ raise GitHubApiException(response.status_code)
+
+ response_json = response.json()
+ items = response_json["items"]
+ return [GitHubRepo(item["name"], item["language"], item["stargazers_count"]) for item in items]
+
+
+if __name__ == "__main__":
+ # Accept an optional argument for minimum number of stars from the command line
+ # $ ./gh_api 100000 # means an input of 100,000 minimum stars.
+ script_arguments = sys.argv
+ min_stars = 50000
+
+ if len(script_arguments) >= 2:
+ try:
+ min_stars = int(script_arguments[1])
+ except ValueError:
+ sys.exit("Error: Command line argument must be a valid number.")
+
+ # Accept the list of languages from the user, or provide a default list.
+ languages = input(
+ "Enter a comma separated list of programming languages (or press ENTER for defaults): "
+ ).strip()
+ if not languages:
+ languages = ["python", "javascript", "ruby"]
+ else:
+ languages = languages.split(",")
+
+ # Get the results
+ result_repos = repos_with_most_stars(languages=languages, min_stars=min_stars)
+ if not result_repos:
+ print("No Results Found.")
+ else:
+ for repo in result_repos:
+ print(repo)
diff --git a/pyworkshop/2_intermediate_python/chapter7/divisible.py b/pyworkshop/2_intermediate_python/chapter7/divisible.py
new file mode 100644
index 0000000..0927c9e
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter7/divisible.py
@@ -0,0 +1,2 @@
+def divisible_by(check_number, divisor):
+ return check_number % divisor == 0
diff --git a/pyworkshop/2_intermediate_python/chapter7/test_divisible.py b/pyworkshop/2_intermediate_python/chapter7/test_divisible.py
new file mode 100644
index 0000000..ccf3a5b
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter7/test_divisible.py
@@ -0,0 +1,12 @@
+import unittest
+from divisible import divisible_by
+
+class TestCase(unittest.TestCase):
+
+ def test_divisible_by(self):
+ self.assertTrue(divisible_by(10, 2))
+ self.assertTrue(divisible_by(10, 3))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pyworkshop/2_intermediate_python/chapter8/app.py b/pyworkshop/2_intermediate_python/chapter8/app.py
new file mode 100644
index 0000000..ad8ad9d
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter8/app.py
@@ -0,0 +1,40 @@
+"""
+A Simple Flask Web Application interface
+For viewing popular GitHub Repos sorted by stars using the
+GitHub Search API.
+
+To run:
+(env) $ python -m pip install -r requirements.txt
+(env) $ export FLASK_ENV=development; python3 -m flask run
+"""
+from flask import Flask, render_template, request
+
+from repos.api import repos_with_most_stars
+from repos.exceptions import GitHubApiException
+
+app = Flask(__name__)
+
+available_languages = ["Python", "JavaScript", "Ruby", "Java"]
+
+
+@app.route('/', methods=['POST', 'GET'])
+def index():
+ if request.method == 'GET':
+ # Use the list of all languages
+ selected_languages = available_languages
+ elif request.method == 'POST':
+ # Use the languages we selected in the request form
+ selected_languages = request.form.getlist("languages")
+
+ results = repos_with_most_stars(selected_languages)
+
+ return render_template(
+ 'index.html',
+ selected_languages=selected_languages,
+ available_languages=available_languages,
+ results=results)
+
+
+@app.errorhandler(GitHubApiException)
+def handle_api_error(error):
+ return render_template('error.html', message=error)
diff --git a/pyworkshop/2_intermediate_python/chapter8/repos/__init__.py b/pyworkshop/2_intermediate_python/chapter8/repos/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pyworkshop/2_intermediate_python/chapter8/repos/api.py b/pyworkshop/2_intermediate_python/chapter8/repos/api.py
new file mode 100644
index 0000000..20837f4
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter8/repos/api.py
@@ -0,0 +1,34 @@
+import requests
+
+from repos.exceptions import GitHubApiException
+from repos.models import GitHubRepo
+
+
+GITHUB_API_URL = "https://api.github.com/search/repositories"
+
+
+def create_query(languages, min_stars):
+ """
+ Create the query string for the GitHub search API,
+ based on the minimum amount of stars for a project, and
+ the provided programming languages.
+ """
+ # Notice we are calling .strip() on each language,
+ # to clear it of leading and trailing whitespace
+ query = " ".join(f"language:{language.strip()}" for language in languages)
+ query = query + f" stars:>{min_stars}"
+ return query
+
+
+def repos_with_most_stars(languages, min_stars=40000, sort="stars", order="desc"):
+ query = create_query(languages, min_stars)
+ parameters = {"q": query, "sort": sort, "order": order}
+ print(parameters)
+ response = requests.get(GITHUB_API_URL, params=parameters)
+
+ if response.status_code != 200:
+ raise GitHubApiException(response.status_code)
+
+ response_json = response.json()
+ items = response_json["items"]
+ return [GitHubRepo(item["name"], item["language"], item["stargazers_count"]) for item in items]
diff --git a/pyworkshop/2_intermediate_python/chapter8/repos/exceptions.py b/pyworkshop/2_intermediate_python/chapter8/repos/exceptions.py
new file mode 100644
index 0000000..0ba8343
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter8/repos/exceptions.py
@@ -0,0 +1,13 @@
+"""
+GitHub API Application: Custom Exception Classes
+"""
+
+class GitHubApiException(Exception):
+
+ def __init__(self, status_code):
+ if status_code == 403:
+ message = "Rate limit reached. Please wait a minute and try again."
+ else:
+ message = f"HTTP Status Code was: {status_code}."
+
+ super().__init__("A GitHub API Error Occurred: " + message)
diff --git a/pyworkshop/2_intermediate_python/chapter8/repos/models.py b/pyworkshop/2_intermediate_python/chapter8/repos/models.py
new file mode 100644
index 0000000..cdee487
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter8/repos/models.py
@@ -0,0 +1,19 @@
+"""
+GitHub API Application: Custom Model Classes
+"""
+
+class GitHubRepo:
+ """
+ A class used to represent a single GitHub Repository.
+ """
+
+ def __init__(self, name, language, num_stars):
+ self.name = name
+ self.language = language
+ self.num_stars = num_stars
+
+ def __str__(self):
+ return f"-> {self.name} is a {self.language} repo with {self.num_stars} stars."
+
+ def __repr__(self):
+ return f'GitHubRepo({self.name}, {self.language}, {self.num_stars})'
diff --git a/pyworkshop/2_intermediate_python/chapter8/requirements.txt b/pyworkshop/2_intermediate_python/chapter8/requirements.txt
new file mode 100644
index 0000000..5eaf725
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter8/requirements.txt
@@ -0,0 +1,2 @@
+flask
+requests
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter8/static/favicon.png b/pyworkshop/2_intermediate_python/chapter8/static/favicon.png
new file mode 100644
index 0000000..42c4379
Binary files /dev/null and b/pyworkshop/2_intermediate_python/chapter8/static/favicon.png differ
diff --git a/pyworkshop/2_intermediate_python/chapter8/static/style.css b/pyworkshop/2_intermediate_python/chapter8/static/style.css
new file mode 100644
index 0000000..0a0b219
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter8/static/style.css
@@ -0,0 +1,229 @@
+/*
+CSS Styles for GitHub Repos by Stars exercise site.
+Sorry for the bad css! I'm a "full-stack" developer.
+*/
+
+/*
+Checkbox Style From: https://codepen.io/wilder_taype/pen/pNXwMW
+*/
+
+@import url(https://fonts.googleapis.com/css?family=Roboto:400,700);
+*{font-family: 'Roboto', sans-serif;}
+
+.option-input {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ -o-appearance: none;
+ appearance: none;
+ position: relative;
+ top: 13.33333px;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 40px;
+ width: 40px;
+ transition: all 0.15s ease-out 0s;
+ background: #cbd1d8;
+ border: none;
+ color: #fff;
+ cursor: pointer;
+ display: inline-block;
+ margin-right: 0.5rem;
+ outline: none;
+ position: relative;
+ z-index: 1000;
+}
+.option-input:hover {
+ background: #9faab7;
+}
+.option-input:checked {
+ background: #40e0d0;
+}
+.option-input:checked::before {
+ height: 40px;
+ width: 40px;
+ position: absolute;
+ content: '✔';
+ display: inline-block;
+ font-size: 26.66667px;
+ text-align: center;
+ line-height: 40px;
+}
+.option-input:checked::after {
+ background: #40e0d0;
+ content: '';
+ display: block;
+ position: relative;
+ z-index: 100;
+}
+
+body {
+ display: -webkit-box;
+ display: -moz-box;
+ display: -ms-flexbox;
+ display: box;
+ background: #e8ebee;
+ color: #9faab7;
+ font-family: "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
+ text-align: center;
+}
+body div {
+ padding: 5rem;
+}
+body label {
+ display: block;
+ line-height: 40px;
+ text-align: left;
+}
+
+/*
+Button Style From: https://codepen.io/wilder_taype/pen/LeoQEb
+*/
+
+button {
+ display: block;
+ border-radius: 3px;
+ margin: 15px 5px;
+ color: #fff;
+ cursor: pointer;
+ box-shadow: 3px 7px 7px 4px rgba(237,80,83,.5), 1px 0 0 rgba(0,0,0,.15);
+ background: #ed5053;
+ padding: 20px;
+ flex-grow: 1;
+ width: 100px;
+ border: none;
+ font-size: 1.6rem;
+ line-height: 1.6;
+}
+
+/*
+Table Style From: https://codepen.io/gmb/pen/xVGYZw
+*/
+
+html {
+ font-size: 62.5%;
+}
+
+body {
+ font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 1.6rem;
+ line-height: 1.6;
+ color: #20262e;
+ background-color: #28b1de;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.container {
+ max-width: 800px;
+}
+
+h1 {
+ text-align: center;
+ font-size: 3rem;
+ color: rgba(255, 255, 255, .8);
+ text-transform: uppercase;
+ line-height: 1.375;
+ margin: 0 0 2.4rem 0;
+ letter-spacing: 1px
+}
+
+h2 {
+ text-align: right;
+ font-size: 1.2rem;
+ text-transform: uppercase;
+ line-height: 1.375;
+ margin: 0;
+ letter-spacing: 1px
+}
+
+table {
+ width: 100%;
+ min-width: 300px;
+ margin-bottom: 2.4rem;
+ background-color: #20262e;
+ color: #fff;
+ overflow: hidden;
+}
+
+table tr:nth-child(even) {
+ background-color: rgb(46, 53, 62);
+}
+
+table th,
+table td:before {
+ color: #28b1de;
+}
+
+table th {
+ display: none;
+}
+
+table th,
+table td {
+ margin: .5rem 2rem;
+ text-align: left;
+}
+
+table td {
+ display: block;
+ font-size: 90%;
+}
+
+table td:first-child {
+ padding-top: 1rem;
+}
+
+table td:last-child {
+ padding-bottom: 1rem;
+}
+
+table td:before {
+ content: attr(data-th) ':\00a0';
+ font-weight: bold;
+ min-width: 8rem;
+ display: inline-block;
+}
+
+
+@media (min-width: 600px) {
+ table td:before {
+ display: none;
+ }
+ table th,
+ table td {
+ display: table-cell;
+ }
+ table th,
+ table td,
+ table td:first-child,
+ table td:last-child {
+ padding: 1.5rem 2rem;
+ }
+}
+
+/*
+Error Page Style From: https://codepen.io/akashrajendra/pen/JKKRvQ
+*/
+
+#error{
+ font-family: 'Lato', sans-serif;
+ color: #888;
+ margin: 0;
+ display: table;
+ width: 100%;
+ height: 100vh;
+ text-align: center;
+}
+
+.fof{
+ display: table-cell;
+ vertical-align: middle;
+}
+
+.fof h1{
+ font-size: 50px;
+ display: inline-block;
+ padding-right: 12px;
+}
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter8/static_files.zip b/pyworkshop/2_intermediate_python/chapter8/static_files.zip
new file mode 100644
index 0000000..707f131
Binary files /dev/null and b/pyworkshop/2_intermediate_python/chapter8/static_files.zip differ
diff --git a/pyworkshop/2_intermediate_python/chapter8/templates/error.html b/pyworkshop/2_intermediate_python/chapter8/templates/error.html
new file mode 100644
index 0000000..0e0b73c
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter8/templates/error.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
{{message}}
+
+
+
+
+
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter8/templates/index.html b/pyworkshop/2_intermediate_python/chapter8/templates/index.html
new file mode 100644
index 0000000..89c1844
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter8/templates/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ Learn Python: Popular GitHub Repos (by ⭐️) With Flask
+
+
+
+
+
+
+
+
+
Languages
+
+
+
+
+
Popular GitHub Repos (by ⭐️)
+
+
+
+ {% if not results %}
+ No Results.
+ {% else %}
+
+
Name
+
Language
+
Number Stars
+
+ {% endif %}
+
+ {% for result in results %}
+
+
{{result.name}}
+
{{result.language}}
+
{{result.num_stars}}
+
+ {% endfor %}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pyworkshop/2_intermediate_python/chapter8/test.py b/pyworkshop/2_intermediate_python/chapter8/test.py
new file mode 100644
index 0000000..bb9176e
--- /dev/null
+++ b/pyworkshop/2_intermediate_python/chapter8/test.py
@@ -0,0 +1,32 @@
+import repos.api
+import repos.exceptions
+
+import unittest
+
+class TestCreateQuery(unittest.TestCase):
+
+ def test_create_query(self):
+ test_languages = ["Python", "Ruby", "Java"]
+ test_min_stars = 10000
+
+ expected = "language:Python language:Ruby language:Java stars:>10000"
+ result = repos.api.create_query(test_languages, test_min_stars)
+
+ self.assertEqual(result, expected, "Unexpected result from create_query")
+
+
+class TestGitHubApiException(unittest.TestCase):
+
+ def test_exception_403(self):
+ status_code = 403
+ exception = repos.exceptions.GitHubApiException(status_code)
+ self.assertTrue("Rate limit" in str(exception), "'Rate limit' not found")
+
+ def test_exception_500(self):
+ status_code = 500
+ exception = repos.exceptions.GitHubApiException(status_code)
+ self.assertTrue(str(status_code) in str(exception))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/pyworkshop/2_intermediate_python/chapter8/tests/__init__.py b/pyworkshop/2_intermediate_python/chapter8/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pyworkshop/2_intermediate_python/chapter8/tests/tests.py b/pyworkshop/2_intermediate_python/chapter8/tests/tests.py
new file mode 100644
index 0000000..e69de29
diff --git a/pyworkshop/intro_python/.DS_Store b/pyworkshop/intro_python/.DS_Store
deleted file mode 100644
index cb5e79c..0000000
Binary files a/pyworkshop/intro_python/.DS_Store and /dev/null differ
diff --git a/website/.gitattributes b/website/.gitattributes
new file mode 100644
index 0000000..3c63f3b
--- /dev/null
+++ b/website/.gitattributes
@@ -0,0 +1,10 @@
+*.png filter=lfs diff=lfs merge=lfs -text
+*.jpeg filter=lfs diff=lfs merge=lfs -text
+*.jpg filter=lfs diff=lfs merge=lfs -text
+*..eot filter=lfs diff=lfs merge=lfs -text
+*.svg filter=lfs diff=lfs merge=lfs -text
+*.eot filter=lfs diff=lfs merge=lfs -text
+*.ttf filter=lfs diff=lfs merge=lfs -text
+*.woff filter=lfs diff=lfs merge=lfs -text
+*.woff2 filter=lfs diff=lfs merge=lfs -text
+*.pdf filter=lfs diff=lfs merge=lfs -text
diff --git a/website/README.md b/website/README.md
new file mode 100644
index 0000000..895e4f0
--- /dev/null
+++ b/website/README.md
@@ -0,0 +1,27 @@
+Local setup instructions:
+
+1. Install Hugo version v0.54.0 for the site to render properly
+1. clone hugo learn theme from https://github.com/matcornic/hugo-theme-learn, update `themesdir` in config.toml
+1. `cd python` then, `hugo server -D` to run local server
+
+Note: the file and folder names don't contribute to order, and may not always be accurate. Sorted by chapter and individual page weight.
+
+If you move things around or delete chapters, you may have to restart the server for the chapters to appear correctly.
+
+How to use the theme: https://learn.netlify.com/en/cont/
+More about theme shortcodes: https://learn.netlify.com/en/shortcodes/
+
+use `git lfs` for tracking binary types.
+
+```bash
+$ git lfs track "*.psd"
+git add .gitattributes
+```
+
+Currently tracked types:
+ - .png
+ - .jpg
+ - .jpeg
+ - .pdf
+ - .mp4
+ - .gif
\ No newline at end of file
diff --git a/website/archetypes/default.md b/website/archetypes/default.md
new file mode 100644
index 0000000..00e77bd
--- /dev/null
+++ b/website/archetypes/default.md
@@ -0,0 +1,6 @@
+---
+title: "{{ replace .Name "-" " " | title }}"
+date: {{ .Date }}
+draft: true
+---
+
diff --git a/website/config.toml b/website/config.toml
new file mode 100644
index 0000000..45280b7
--- /dev/null
+++ b/website/config.toml
@@ -0,0 +1,52 @@
+baseURL = "/"
+languageCode = "en-US"
+defaultContentLanguage = "en"
+
+title = "Nina Zakharenko - Introduction and Intermediate Python"
+theme = "nnja-theme-learn"
+themesdir = "themes"
+metaDataFormat = "yaml"
+defaultContentLanguageInSubdir= true
+ignoreFiles = [ "\\.pyc$", "__pycache__"]
+publishDir = "docs"
+googleAnalytics = "UA-138644383-1"
+
+[params]
+ # Change default color scheme with a variant one. Can be "red", "blue", "green".
+ themeVariant = "blue"
+ editURL = "https://github.com/nnja/python/edit/master/website/content/"
+ defaultSocialImage = "images/twittercard.png"
+ description = "Free Learn Python Course by Nina Zakharenko - An intensive two day introduction and intermediate course on Python. Video course published on Frontend Masters."
+ author = "Nina Zakharenko"
+ showVisitedLinks = true
+ disableBreadcrumb = false
+ disableNextPrev = false
+ disableInlineCopyToClipBoard = true
+ twitter = "nnja"
+
+
+# For search functionality
+[outputs]
+home = [ "HTML", "RSS", "JSON"]
+
+[Languages]
+[Languages.en]
+title = "Learn Python by Nina Zakharenko"
+weight = 1
+languageName = "English"
+
+[[Languages.en.menu.shortcuts]]
+name = " Github repo"
+identifier = "ds"
+url = "https://git.io/python3"
+weight = 10
+
+[[Languages.en.menu.shortcuts]]
+name = " Follow Nina on Twitter"
+url = "https://twitter.com/nnja"
+weight = 20
+
+[[Languages.en.menu.shortcuts]]
+name = " Credits"
+url = "/credits"
+weight = 30
\ No newline at end of file
diff --git a/website/content/01-introduction/00-who.md b/website/content/01-introduction/00-who.md
new file mode 100644
index 0000000..0d4fb4a
--- /dev/null
+++ b/website/content/01-introduction/00-who.md
@@ -0,0 +1,38 @@
+---
+date: 2019-01-24T09:14:14-08:00
+title: About Your Instructor
+weight: 1
+draft: false
+---
+
+# @nnja
+
+{{< figure src="/images/me.jpg" width="20%" title="Nina Zakharenko" >}}
+
+
+Hi, I'm Nina Zakharenko. I'm a Senior Cloud Developer Advocate at Microsoft, focusing on Python. Before Microsoft, I wrote code for other cool companies like Reddit, Meetup, and HBO. In my spare time I like drinking scotch and tinkering with wearable electronics. I enjoy hiking and snowboarding from my home base in Portland, Oregon. I change my hair color regularly.
+
+
+I've been involved in the Python community for approximately 6 years. During that time I've spoken at multiple Python conferences on a variety of topics, including PyCon US, PyParis, DjangoCon, and even PyCon Russia in Moscow.
+
+#### Stay in touch
+- @nnja
+- LinkedIn
+- GitHub where you can find the code for this site and my other projects
+
+I occasionally blog on Medium at @nnja. You can find out more about me on my homepage [nnja.io](https://nnja.io).
+
+#### Frontend Masters
+
+Check out my other [Frontend Masters courses](https://frontendmasters.com/teachers/nina-zakharenko/), such as [Git In-Depth](https://frontendmasters.com/courses/git-in-depth/).
+
+#### My Conference Talks
+
+You can watch a selection of my talks on YouTube
+such as:
+
+- [Elegant Solutions For Everyday Python Problems](https://www.youtube.com/watch?v=WiQqqB9MlkA) - PyCon 2018
+- [Memory Management in Python - The Basics](https://www.youtube.com/watch?v=F6u5rhUQ6dU) - PyCon 2016
+- [Code Review Skills for Pythonistas](https://www.youtube.com/watch?v=6L3ZVLtSeo8) - EuroPython 2018
+- [Five Things You Didn't Know Python Can Do](https://www.youtube.com/watch?v=WlGkBqBRsik) - All Things Open 2018
+- [Technical Debt - The Code Monster In Your Closet](https://www.youtube.com/watch?v=JKYktDRoRxw) - PyCon 2015
\ No newline at end of file
diff --git a/website/content/01-introduction/02-navigating-course.md b/website/content/01-introduction/02-navigating-course.md
new file mode 100644
index 0000000..d13dadd
--- /dev/null
+++ b/website/content/01-introduction/02-navigating-course.md
@@ -0,0 +1,80 @@
+---
+title: "Navigating The Course"
+date: 2019-02-04T00:13:49-08:00
+draft: false
+weight: 2
+---
+
+The course content is free and open source, and hosted on GitHub. A link will be available *after the class* so that edits can be suggested via Pull Requests.
+
+### Suggesting Changes to The Course Material
+
+If you notice a typo or any other issues with the material *after the class*, please open a pull request on GitHub by clicking the "Edit this page" button on the top right corner of every course page.
+
+
+
+
+
+### Clearing History of Read Pages
+
+Once a section is completed, a check mark will appear to the right of that section. To clear it, hit the "Clear History" button at the bottom of the table of contents on the left hand side.
+
+
+
+### Copying Code
+
+To copy code and commands, click on the clipboard icon (() on the top right of any code box.
+
+{{% notice tip %}}
+You can even copy code intended for the Python REPL. The prompt characters won't be copied over.
+{{% /notice %}}
+
+Try it now by coping the code below.
+
+```python
+def greeting():
+ print("Hello, World!")
+```
+
+### Searching
+
+The whole course is searchable via the search bar on the top left.
+
+
+
+### Navigation
+
+View the course contents in the side bar on the left.
+
+#### Arrows
+
+The course can be navigated sequentially to the page before or after via the left and right arrows on each page.
+
+
+
+{{% notice tip %}}
+You can also use the left and right keyboard arrows to navigate between pages.
+{{% /notice %}}
+
+#### Breadcrumbs
+
+Use the breadcrumb links on the floating header of each page to jump between related sections.
+
+
+
+#### Table of Contents
+
+Click on the "Table of Contents" Icon in the page header to jump between sections in the page.
+
+
+
+### Expanding Sections
+
+Sometimes additional instructions or information will be available via the expand section arrow. Keep an eye out for this icon:
+
+
+
+
+{{% notice info %}}
+The course material is heavy in information and links. I suggest that you follow the links **after class** as a source of additional information, so that you don't get distracted from the important material.
+{{% /notice %}}
\ No newline at end of file
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/01-getting-started.md b/website/content/01-introduction/02-requirements/05-vs-code/01-getting-started.md
new file mode 100644
index 0000000..8194018
--- /dev/null
+++ b/website/content/01-introduction/02-requirements/05-vs-code/01-getting-started.md
@@ -0,0 +1,130 @@
+---
+title: "Getting Started"
+draft: false
+weight: 1
+---
+
+Visual Studio Code (commonly called VS Code) is a free, open source, lightweight cross platform code editor. A fresh installation is bare bones -- the power of VS Code comes via the extensions. There are useful extensions for every programming languages you can think of, but the choice of which ones to install and how to configure your editor is up to you.
+
+## Installation
+
+You should have installed the editor and the Python extension as part of the pre-requisites for the course. If you haven't, please do so now as we'll be using VS Code to edit our Python for the rest of the course.
+
+ {{% button href="https://code.visualstudio.com/download" icon="fas fa-download" %}}Download Visual Studio Code{{% /button %}}
+
+ {{% button href="https://marketplace.visualstudio.com/items?itemName=ms-python.python" icon="fas fa-download" %}}Python Extension for Visual Studio Code{{% /button %}}
+
+## Advantages
+
+You'll want to use a code editor for your Python projects as your projects grow in scope. It'll allow you to easily manage projects with multiple files and modules. The Python extension also offers a built-in REPL, so that we can quickly and easily test out snippets of code and instantly see the results.
+
+Code editors also offer syntax highlighting, syntax checking, auto-completion, and more.
+
+## Keyboard Bindings
+
+If you prefer keyboard shortcuts from a different editor, such as sublime, vim, or emacs, you can install a [key-map extension](https://code.visualstudio.com/docs/getstarted/keybindings#_keymap-extensions) to remap your keybindings, preferably at the next break.
+
+## Choices
+
+We'll be using VS Code for this course so I can show you the ropes, but after class is over you're free to switch to any editor of your choice.
+
+{{% notice info %}}
+Note: For the rest of the course, instructions for Mac/Linux terminal and Windows PowerShell terminal should be the same.
+{{% /notice %}}
+
+## Opening Your First Project
+
+#### Setting Up a Project Structure
+
+Now that VS Code is downloaded and installed, it's time to open our project.
+
+On Mac OS and Linux, follow these steps in you terminal application, or follow the steps manually in the program you use to manage files.
+
+{{% notice tip %}}
+Remember, you can copy all the commands by clicking on the
+clipboard icon () located in the top right section of a code block.
+{{% /notice %}}
+
+```bash
+# change to our home directory
+$ cd
+
+# enter the directory of the pyworkshop folder from the last step
+$ cd pyworkshop
+```
+
+For Mac / Linux:
+
+```bash
+# activate the virtual environment
+$ source env/bin/activate
+
+# make a new empty python file called project.py
+(env) $ touch project.py
+```
+
+For Windows:
+
+```powershell
+# activate the virtual environment
+> env\scripts\activate
+
+# make a new empty python file called project.py
+(env)> fc > project.py
+```
+
+
+
+#### Opening a Project in VS Code From The Command Line
+
+First, navigate to your project directory and activate your virtual environment. If you followed the previous steps, you should already be in the right directory.
+
+```bash
+# Open VS Code using the current directory as a project.
+(env) $ code .
+```
+
+This will open a project in the current directory. This is the last time we'll be using the terminal outside of VS Code for the duration of the course.
+
+
+
+
+## Keyboard Shortcuts
+
+To get started with VS Code, you only need to remember two keyboard shortcuts.
+
+### #1 : Show Command Palette
+
+Open the command palette with `Ctrl+Shift+P` on Windows and Linux, and `⌘⇧P` (command + shift + P) on Mac OS.
+
+The command palette lets you search and run any of the commands available within VS Code. If you don't know how to do something, the command palette will usually point you in the right direction.
+
+The command palette is how you'll navigate VS Code.
+
+
+
+### #2 : Quick Open, Go to File
+
+Open Quick Open with `Ctrl+P` on Windows and Linux, and `⌘P`(command + P) on Mac OS.
+
+Quick open is how you'll navigate your codebase and files.
+
+**To dismiss either dialog, press the Escape key.**
+
+{{% notice tip %}}
+Write these two shortcuts down, because we'll be using them frequently for the rest of the course.
+{{% /notice %}}
\ No newline at end of file
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/03-working-with-python.md b/website/content/01-introduction/02-requirements/05-vs-code/03-working-with-python.md
new file mode 100644
index 0000000..82ce500
--- /dev/null
+++ b/website/content/01-introduction/02-requirements/05-vs-code/03-working-with-python.md
@@ -0,0 +1,68 @@
+---
+title: "Working With Python"
+draft: false
+weight: 3
+---
+
+{{% notice info %}}
+Once we open our first Python file in VS Code, we'll see some configuration pop-ups. For the time being, **don't** dismiss them.
+{{% /notice %}}
+
+
+
+## Configuring VS Code for Python
+
+### Open the `project.py` file
+
+If you haven't created a `project.py` file in the `pyworkshop` directory, now is the time to do so.
+You can make a new file (Ctrl+N or ⌘P) and then save it (Ctrl+S or ⌘S).
+
+Now that you've learned the necessary keyboard shortcuts, use Quick Open with `Ctrl+P` or `⌘P` to open the `project.py` file.
+
+{{% notice info %}}
+If you haven't installed the Python extension at this point, you'll get a pop-up recommending you install it for this type of file. Please [install it now](https://marketplace.visualstudio.com/items?itemName=ms-python.python&WT.mc_id=pyclass-docs-ninaz), and reload VS Code when prompted to load the extension.
+{{% /notice %}}
+
+## Configuring The Interpreter
+
+Many operating systems include Python, but unfortunately it's usually a few versions behind.
+
+We _never_ want to use Python2 for new Python projects, and we want to make sure we select the latest version of Python3 that we installed in the pre-requisites.
+
+Luckily, VS Code is smart. If you launch it from a directory with an *activated* virtual environment, it'll automatically pick up the correct interpreter.
+
+{{%expand "In case you need to configure the interpreter manually, follow these (optional) instructions." %}}
+
+Click on the Select Python Interpreter button in the popup.
+
+
+
+
+{{%expand "Note: If dismissed the popup, open the command palette and select Python: Select Interpreter. Expand this section for more details." %}}
+
+{{% /expand%}}
+
+Select the version of Python 3 in your virtual environment `env` folder as your interpreter. If you open a Python file in a new directory, you may need to select your interpreter again.
+
+{{% /expand%}}
+
+{{% notice tip %}}
+You can always see what your interpreter is set to on the left side of the status bar at the bottom of the window.
+{{% /notice %}}
+
+
+
+## Setting Up a Linter
+
+Per the [VS Code documentation](https://code.visualstudio.com/docs/python/linting?WT.mc_id=pyclass-docs-ninaz), linting highlights syntactical and stylistic problems in your Python source code. A linter will give you code hints about a variety of different types of problems. For example, when you have subtle errors in your code, like trying to use a variable you haven't defined yet. A linter will also show you when you're not following Python style convention called PEP8. PEP8 is a set of defined rules for how Python code should look. We'll cover it in more depth later, but what you need to know right now is that PEP8 warnings are not syntax errors. If your code doesn't adhere to the PEP8 standard, it will still run.
+
+By default the linter will run every time you save a file, so it's good practice to save often with (Ctrl+S or ⌘S).
+
+Let's go ahead and click install on the Linter popup.
+
+
+
+{{%expand "Note: If you accidentally dismissed the popup, open the command palette and search for Python: Select Linter, then select pylint. The popup will now reappear, and you can hit install. Expand this section for more details." %}}
+
+
+{{% /expand%}}
\ No newline at end of file
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/04-the-repl-in-vscode.md b/website/content/01-introduction/02-requirements/05-vs-code/04-the-repl-in-vscode.md
new file mode 100644
index 0000000..2f3d529
--- /dev/null
+++ b/website/content/01-introduction/02-requirements/05-vs-code/04-the-repl-in-vscode.md
@@ -0,0 +1,128 @@
+---
+title: "Using The REPL"
+date: 2019-02-10T18:11:15-08:00
+draft: false
+weight: 4
+---
+
+## The REPL
+
+REPL stands for Read, Evaluate, Print, Loop. The REPL is how you interact with the Python Interpreter.
+
+Unlike running a file containing Python code, in the REPL you can type commands and instantly see the output printed out. You can also use the REPL to print out help for methods and objects in Python, list out what methods are available, and much more.
+
+## Open The REPL
+
+To start the REPL in VS code, open the command palette and search for and select "Start REPL". The advantage to starting the REPL from inside VS Code is that it respects the environment you already set up, that is the version of Python you chose earlier.
+
+
+
+Note: If you'd like to start the REPL from the command line outside of the editor, type `python` in your shell,
+
+Running this command should bring up a new pane at the bottom of your editor that you can type into. A great feature of the REPL is that we can instantly see the result of commands being run.
+
+
+
+{{% notice note %}}
+Note, in the REPL three arrows >>> indicate a line of input given at the prompt.
+If you see these arrows in example code, don't copy them into your own REPL.
+Later, when we run out Python code from files, you will no longer see the triple arrows.
+{{% /notice %}}
+
+Let's get familiar with the REPL.
+
+- `#` - comments start with `#`. They will be ignored.
+- `>>>` - this is the prompt. In example code, lines starting with `>>>` means they are **input**
+- lines that don't start with either of these are **output** that was produced by running input from the prompt
+
+ by typing these line of code at the `>>>` prompt, and press enter after each line.
+
+```python
+# My REPL. Don't copy the >>> symbols, that means the code was entered
+# into the prompt.
+#
+# If the line does not start with >>>, that means it is output,
+# not input
+>>> name = "Nina"
+>>> name
+'Nina'
+```
+
+In the REPL, we can see the value of any variable just by entering it into the prompt.
+
+You can copy and paste code into the REPL, even multiple lines of code at once. Copy the three lines below and paste them into your REPL. What's the result?
+
+```python
+x = 5
+y = 33.5
+x * y
+```
+
+{{%expand "See the result." %}}
+```python
+>>> x = 5
+>>> y = 33.5
+>>> x * y
+167.5
+```
+{{% /expand%}}
+
+The REPL allows us to gather information in real time about our Python program, which makes it a great learning tool.
+
+## Using `type()`, `dir()`, and `help()`
+
+We can use three very useful methods in the REPL to help us understand our Python programs.
+
+Pass in an object into the `type()` Use type to find the type of an object in Python.
+
+{{% notice info %}}
+If you're not sure what a variable or an object is, don't worry. We'll cover it later in the day.
+{{% /notice %}}
+
+For example, in the REPL, let's make a new variable `name`, and check its `type`.
+
+```python
+>>> name = "Nina"
+>>> type(name)
+
+```
+
+We'll see that the type is `str`, Python's version of a string. Now that we know this object's type, we can
+pass the type into other methods.
+
+The first one is `dir()` which stands for directory. If we check the directory of `str` (notice, no quotes here)) in the REPL, we'll see all the methods available on strings in Python. Don't worry about these for now, we'll use them later in the day.
+
+```Python
+>>> dir(str)
+['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
+```
+
+The next useful method is `help()`. You can pass a type, method, or other object to help to instantly see available documentation about the method, the parameters it expects, and what it returns.
+
+Let's try this in the REPL, and look up the documentation for the `isupper` method in String. We access it with the period symbol (`.`). This is called dot-notation.
+
+```python
+>>> help(str.isupper)
+```
+
+Will show:
+
+```text
+isupper(self, /)
+ Return True if the string is an uppercase string, False otherwise.
+
+ A string is uppercase if all cased characters in the string are uppercase and
+ there is at least one cased character in the string.
+```
+
+{{% notice note %}}
+Press the 'q' key to exit this screen.
+{{% /notice %}}
+
+Keep note of these three helpful methods, and don't be afraid to use them throughout class.
+
+## Why Use The REPL?
+
+In this class, we'll be working with a mix of the REPL and running code in files, like we'll see in the next section. You'll want to store code for reuse in files, while you can consider the REPL more of a scratch area. It's the place where you can instantly play around and try out Python code. The REPL is a handy tool for both beginner and advanced Python programmers.
+
+We'll use the REPL for the majority of Day 1, and move on to running Python files in Day 2.
\ No newline at end of file
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/05-running-code.md b/website/content/01-introduction/02-requirements/05-vs-code/05-running-code.md
new file mode 100644
index 0000000..3ebac63
--- /dev/null
+++ b/website/content/01-introduction/02-requirements/05-vs-code/05-running-code.md
@@ -0,0 +1,63 @@
+---
+title: "Running Code"
+date: 2019-02-10T18:14:45-08:00
+draft: false
+weight: 5
+---
+
+VS Code provides a built in terminal that allows us to easily run our programs. We'll mostly be working with Python program files at the end of Day 1, and for most of Day 2.
+
+### Creating Python Files with the `*.py` extension
+
+You know a file is a Python program when it ends with a `.py` extension.
+
+#### Creating New Python Files
+
+To create a new file in VS Code, hit `Ctrl+N` on Windows and Linux, and `⌘N` (command + N) on Mac OS.
+
+This will open a new file. Next, save the file with a `.py` extension.
+
+{{% notice info %}}
+Create a new simple Python program in a file called `hello.py` in your `pyworkshop` directory with the following contents:
+{{% /notice %}}
+
+```python
+# in file: hello.py
+greetings = ["Hello", "Bonjour", "Hola"]
+
+for greeting in greetings:
+ print(f"{greeting}, World!")
+```
+
+#### Opening The VS Code Terminal Window
+
+Next, you'll need to open your terminal if you don't have it open already. The quick keyboard shortcut to do that is Ctrl - `
+
+{{% notice note %}}
+If you already had your Python REPL open, you'll need to select a terminal with a shell in it (generally, the one labeled with `1:`).
+{{% /notice %}}
+
+
+
+
+#### Running The File
+
+Once you've opened your `hello.py` file and selected your new terminal window, open the VS Code command palette.
+
+{{% notice note %}}
+Open the command palette with `Ctrl+Shift+P` on Windows and Linux, and `⌘⇧P` (command + shift + P) on Mac OS.
+{{% /notice %}}
+
+Select Python: Run Python File in Terminal
+
+
+
+You should see:
+
+```bash
+Hello, World!
+Bonjour, World!
+Hola, World!
+```
+
+How easy was that? 🎉
\ No newline at end of file
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/_index.md b/website/content/01-introduction/02-requirements/05-vs-code/_index.md
new file mode 100644
index 0000000..721f959
--- /dev/null
+++ b/website/content/01-introduction/02-requirements/05-vs-code/_index.md
@@ -0,0 +1,12 @@
++++
+title = "Setting Up VS Code"
+date = 2019-01-25T15:05:31-06:00
+weight = 57
+chapter = true
++++
+
+
+
+# Visual Studio Code
+
+Visual Studio Code (or VS Code) is a free and open source graphical code editor. VS Code offers plenty of extensions to customize your experience. We'll be using the powerful Python extension to edit our code because it's jam-packed with helpful features.
\ No newline at end of file
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/command-palette.png b/website/content/01-introduction/02-requirements/05-vs-code/images/command-palette.png
new file mode 100644
index 0000000..eeb469f
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/command-palette.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/install-circuitpy.mp4 b/website/content/01-introduction/02-requirements/05-vs-code/images/install-circuitpy.mp4
new file mode 100644
index 0000000..9dcad87
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/install-circuitpy.mp4 differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/install-pylint.png b/website/content/01-introduction/02-requirements/05-vs-code/images/install-pylint.png
new file mode 100644
index 0000000..ad759f6
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/install-pylint.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/interpreter.1.png b/website/content/01-introduction/02-requirements/05-vs-code/images/interpreter.1.png
new file mode 100644
index 0000000..df53ee0
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/interpreter.1.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/interpreter.2.png b/website/content/01-introduction/02-requirements/05-vs-code/images/interpreter.2.png
new file mode 100644
index 0000000..1a3dc65
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/interpreter.2.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/open-folder.png b/website/content/01-introduction/02-requirements/05-vs-code/images/open-folder.png
new file mode 100644
index 0000000..fcf065d
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/open-folder.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/popups copy.png b/website/content/01-introduction/02-requirements/05-vs-code/images/popups copy.png
new file mode 100644
index 0000000..98c52db
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/popups copy.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/popups.png b/website/content/01-introduction/02-requirements/05-vs-code/images/popups.png
new file mode 100644
index 0000000..75398ff
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/popups.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/repl-start.png b/website/content/01-introduction/02-requirements/05-vs-code/images/repl-start.png
new file mode 100644
index 0000000..1b10fea
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/repl-start.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/repl.png b/website/content/01-introduction/02-requirements/05-vs-code/images/repl.png
new file mode 100644
index 0000000..0c7e4ed
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/repl.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/select-linter.png b/website/content/01-introduction/02-requirements/05-vs-code/images/select-linter.png
new file mode 100644
index 0000000..0eb22ba
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/select-linter.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/select-pylint.png b/website/content/01-introduction/02-requirements/05-vs-code/images/select-pylint.png
new file mode 100644
index 0000000..486428e
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/select-pylint.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/selected-interpreter.png b/website/content/01-introduction/02-requirements/05-vs-code/images/selected-interpreter.png
new file mode 100644
index 0000000..04e7ba2
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/selected-interpreter.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/vs-code-icon.png b/website/content/01-introduction/02-requirements/05-vs-code/images/vs-code-icon.png
new file mode 100644
index 0000000..002be7d
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/vs-code-icon.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/vs-code-logo.png b/website/content/01-introduction/02-requirements/05-vs-code/images/vs-code-logo.png
new file mode 100644
index 0000000..3245933
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/vs-code-logo.png differ
diff --git a/website/content/01-introduction/02-requirements/05-vs-code/images/welcome-page.png b/website/content/01-introduction/02-requirements/05-vs-code/images/welcome-page.png
new file mode 100644
index 0000000..c0cb7b2
Binary files /dev/null and b/website/content/01-introduction/02-requirements/05-vs-code/images/welcome-page.png differ
diff --git a/website/content/01-introduction/02-requirements/_index.md b/website/content/01-introduction/02-requirements/_index.md
new file mode 100644
index 0000000..e11a85a
--- /dev/null
+++ b/website/content/01-introduction/02-requirements/_index.md
@@ -0,0 +1,157 @@
+---
+title: Requirements
+weight: 3
+---
+
+{{% notice warning %}}
+You'll need a few mandatory prerequisites for successfully participating in the course.
+{{% /notice %}}
+
+- A Linux, Mac OS, a Windows 10 Machine
+- Python3.7
+- Visual Studio Code
+- The Python Extension for Visual Studio Code
+
+
+#### Additional Instructions For Windows Users
+
+- Note: Always select "Allow this app to make changes to your device" during the installation process.
+- On the last page of the Python 3 installer, select "Disable Path Limit" before hitting close.
+- In the start menu, right click on "Windows PowerShell". Select "Run as an administrator"
+- In the PowerShell terminal window, that opens, type:
+
+(`>` means prompt, don't type that in)
+
+```powershell
+> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
+```
+
+Then, type `Y` for Yes.
+
+{{% notice info %}}
+Keep this window open for the following steps to create a virtual environment!
+{{% /notice %}}
+
+### Downloads
+
+{{% button href="https://www.python.org/downloads/" icon="fas fa-download" %}}Download Python3{{% /button %}}
+{{% button href="https://code.visualstudio.com/download" icon="fas fa-download" %}}Download Visual Studio Code{{% /button %}}
+{{% button href="https://marketplace.visualstudio.com/items?itemName=ms-python.python" icon="fas fa-download" %}}Python Extension for Visual Studio Code{{% /button %}}
+
+## Making sure you're ready
+
+To make sure you have all the prerequisites properly installed:
+
+### Checking for Python 3.7
+
+#### For Windows
+
+Using the same PowerShell window from earlier, type:
+
+```powershell
+> py -3.7
+```
+
+This should open a REPL window with a prompt.
+
+{{% notice tip %}}
+Press `Ctrl + Z` followed by Enter to exit this screen and go back to your prompt.
+{{% /notice %}}
+
+#### For Mac / Linux
+
+Type the following on your terminal.
+```bash
+$ python3 --version
+```
+
+You should see
+```bash
+Python 3.7.2
+```
+
+{{% notice info %}}
+If you don't see a Python version greater than 3.7, please follow the instructions for [installing Python3](https://www.python.org/downloads/) again.
+{{% /notice %}}
+
+### Creating a Virtual Environment and The Project Folder
+
+A Virtual Environment in Python is a self-contained directory that contains a Python installation for a particular version of the language.
+
+It's a very useful way to make sure that we're using the right Python version when we're working on a particular project.
+
+Let's create a project directory and a Python 3.7 Virtual Environment.
+
+#### For Windows
+
+Using the same PowerShell terminal from earlier, type the following commands in one by one:
+
+```powershell
+> cd $home
+> mkdir pyworkshop
+> cd pyworkshop
+> py -3 -m venv env
+> env\scripts\activate
+```
+
+Your prompt should now look like this, but with your own username.
+
+```powershell
+(env) PS C:\Users\nina\pyworkshop>
+```
+
+{{% notice tip %}}
+`env\scripts\activate` is how you *activate* your virtual environment in Windows. You'll want to do that each time you enter this Python project directory from a new shell.
+{{% /notice %}}
+
+#### For Mac / Linux
+
+Open a terminal window. Type the following.
+
+(Do not type the `$` character, that signifies a prompt.)
+
+```bash
+$ cd
+$ mkdir pyworkshop
+$ cd pyworkshop
+$ python3.7 -m venv env
+$ source env/bin/activate
+```
+
+{{% notice tip %}}
+`source env/bin/activate` is how you *activate* your virtual environment on Mac or Linux. You'll want to do that each time you enter this Python project directory from a new shell.
+{{% /notice %}}
+
+Your prompt will look like this to indicate that the virtual environment is active.
+
+```bash
+(env) $
+```
+
+{{% notice info %}}
+You are expected to work from this project folder for the duration of the class, with an activated virtual environment.
+{{% /notice %}}
+
+### Checking VS Code
+
+Look for VS Code in your Applications, or type the following in your Mac/Linux *or* Powershell terminal.
+
+```text
+$ code --version
+```
+
+You should see something like:
+
+```text
+1.32.3
+a3db5be9b5c6ba46bb7555ec5d60178ecc2eaae4
+x64
+```
+
+{{% notice info %}}
+If you don't see VS Code, please follow the instructions for [installing VS Code](https://code.visualstudio.com/download) again.
+{{% /notice %}}
+
+{{% notice note %}}
+Note that after the course you can use the IDE of your choice to continue on your Python adventure.
+{{% /notice %}}
diff --git a/website/content/01-introduction/_index.md b/website/content/01-introduction/_index.md
new file mode 100644
index 0000000..303f7fb
--- /dev/null
+++ b/website/content/01-introduction/_index.md
@@ -0,0 +1,24 @@
+---
+title: Course Introduction
+weight: 1
+chapter: true
+---
+
+# Learn Python
+
+Welcome to Introduction and Intermediate Python, recorded for Frontend Masters.
+
+Created by Nina Zakharenko. @nnja.
+
+Watch the accompanying screencast via subscription on [FrontendMasters.com](https://frontendmasters.com/teachers/nina-zakharenko/).
+
+{{< figure src="/images/fem.png" title="Watch the course" link="https://frontendmasters.com/courses/python/">}}
+
+Stay up to date with me on LinkedIn, or to contact me directly, please send me an email at learnpython@nnja.io.
+
+You can find the content and exercises on the Github repo.
+
+
+The course content is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
+
+I'd like to encourage learning individuals to learn Python from the course material for free, but if you'd like to use the material for your own workshop or for commercial use, please consider supporting my work and [watch the screencast](https://frontendmasters.com/teachers/nina-zakharenko/) instead.
\ No newline at end of file
diff --git a/website/content/01-introduction/images/arrows.png b/website/content/01-introduction/images/arrows.png
new file mode 100644
index 0000000..19ec202
Binary files /dev/null and b/website/content/01-introduction/images/arrows.png differ
diff --git a/website/content/01-introduction/images/clear_history.png b/website/content/01-introduction/images/clear_history.png
new file mode 100644
index 0000000..cec447f
Binary files /dev/null and b/website/content/01-introduction/images/clear_history.png differ
diff --git a/website/content/01-introduction/images/copy.png b/website/content/01-introduction/images/copy.png
new file mode 100644
index 0000000..61cbd87
Binary files /dev/null and b/website/content/01-introduction/images/copy.png differ
diff --git a/website/content/01-introduction/images/edit-page.png b/website/content/01-introduction/images/edit-page.png
new file mode 100644
index 0000000..c2a7886
Binary files /dev/null and b/website/content/01-introduction/images/edit-page.png differ
diff --git a/website/content/01-introduction/images/expand-section.png b/website/content/01-introduction/images/expand-section.png
new file mode 100644
index 0000000..d497e4d
Binary files /dev/null and b/website/content/01-introduction/images/expand-section.png differ
diff --git a/website/content/01-introduction/images/header.png b/website/content/01-introduction/images/header.png
new file mode 100644
index 0000000..402114a
Binary files /dev/null and b/website/content/01-introduction/images/header.png differ
diff --git a/website/content/01-introduction/images/search.png b/website/content/01-introduction/images/search.png
new file mode 100644
index 0000000..ce48024
Binary files /dev/null and b/website/content/01-introduction/images/search.png differ
diff --git a/website/content/01-introduction/images/toc.png b/website/content/01-introduction/images/toc.png
new file mode 100644
index 0000000..9b07833
Binary files /dev/null and b/website/content/01-introduction/images/toc.png differ
diff --git a/website/content/02-introduction-to-python/010-Best-Practices/01-anatomy-of-a-python-program.md b/website/content/02-introduction-to-python/010-Best-Practices/01-anatomy-of-a-python-program.md
new file mode 100644
index 0000000..d5a761e
--- /dev/null
+++ b/website/content/02-introduction-to-python/010-Best-Practices/01-anatomy-of-a-python-program.md
@@ -0,0 +1,171 @@
+---
+title: "Anatomy of a Python Program"
+date: 2019-03-08T23:25:07-08:00
+draft: false
+weight: 4
+---
+
+Let's take a very quick look at a Python program that uses the GitHub Search API to display a list of popular repositories in three different programming languages, sorted by the amount of stars that they have. It may be hard to believe, but by the end of the day, you'll be able to write a program just like this.
+
+### A Python program for using the GitHub search API
+
+```python
+"""
+A small Python program that uses the GitHub search API to list
+the top projects by language, based on stars.
+"""
+
+import requests
+
+GITHUB_API_URL = "https://api.github.com/search/repositories"
+
+
+def create_query(languages, min_stars=50000):
+ query = f"stars:>{min_stars} "
+
+ for language in languages:
+ query += f"language:{language} "
+
+ # a sample query looks like: "stars:>50 language:python language:javascript"
+ return query
+
+
+def repos_with_most_stars(languages, sort="stars", order="desc"):
+ query = create_query(languages)
+ params = {"q": query, "sort": sort, "order": order}
+
+ response = requests.get(GITHUB_API_URL, params=params)
+ status_code = response.status_code
+
+ if status_code != 200:
+ raise RuntimeError(f"An error occurred. HTTP Code: {status_code}.")
+ else:
+ response_json = response.json()
+ return response_json["items"]
+
+
+if __name__ == "__main__":
+ languages = ["python", "javascript", "ruby"]
+ results = repos_with_most_stars(languages)
+
+ for result in results:
+ language = result["language"]
+ stars = result["stargazers_count"]
+ name = result["name"]
+
+ print(f"-> {name} is a {language} repo with {stars} stars.")
+```
+
+### Step by step
+
+{{%expand "Expand this section to walk through the program step-by-step." %}}
+
+There's some helpful information about the program in a comment at the top. The comment is separated with triple quotes `"""`.
+
+The first thing we're doing is importing the popular `requests` library. We'll be using it to make API calls.
+
+```python
+import requests
+```
+
+Next, we define the URL for the GitHub search API, which we found in the documentation for that endpoint in a variable called `GITHUB_API_URL`. This variable is named in all upper case because it's a constant. A value that we don't expect to change over the course of our program.
+
+```python
+GITHUB_API_URL = "https://api.github.com/search/repositories"
+```
+
+First, the code in the `main` method will run. This code defines three different languages we'd like to see results for.
+
+```python
+ languages = ["python", "javascript", "ruby"]
+```
+
+Then calls the `repos_with_most_stars` method with that language list, and gets back a list of results.
+
+```python
+ results = repos_with_most_stars(languages)
+```
+
+Let's jump to the `repos_with_most_stars` function. We know it's a function because of the `def` keyword, followed by a function name, parameters -- both required and optional in parenthesis, and finally, a colon `:`. It accepts a list of languages to sort by, as well as some *optional* parameters for how we want to sort the list. By default, we sort it by the number of stars the repo has, in descending order.
+
+```python
+def repos_with_most_stars(languages, sort="stars", order="desc"):
+```
+
+Next, we need to create a query string that this particular API understands. We do that in the `create_query` function. This function takes the languages as a required parameter, and the minimum number of stars we'd like to query for as an optional parameter.
+
+```python
+def create_query(languages, min_stars=50000):
+```
+
+In this function, we create a query string that looks like this `stars:>50000 language:python language:javascript language:ruby". These parameters of this query string are defined by the expectations of the API that we're working with. We return this value.
+
+Since the query is a little confusing, there's a comment in the code that explains what it does. The comment starts with `# `.
+
+Back in the `repos_with_most_stars` function, we use our query string as part of the parameters that we'll be passing in to the API, along with the URL that we'll be using.
+
+We declare them in a dictionary called `params`, like this:
+
+```python
+# looks like: q=stars:>50000+language:python+language:javascript+language:ruby+&sort=stars&order=desc'
+params = {"q": query, "sort": sort, "order": order}
+```
+
+These params map to part of a URL that will look like this: `q=stars:>50000+language:python+language:javascript+language:ruby+&sort=stars&order=desc`
+
+Next, we use these parameters as well as the URL defined in `GITHUB_API_URL` to call the API using the requests library.
+
+```python
+ response = requests.get("https://api.github.com/search/repositories", params=params)
+```
+
+We're requesting the data at this URL: [https://api.github.com/search/repositories?q=stars:>50000+language:python+language:javascript+language:ruby+&sort=stars&order=desc](https://api.github.com/search/repositories?q=stars:>50000+language:python+language:javascript+language:ruby+&sort=stars&order=desc)
+
+We're using the requests library because it allows us to quickly and easily work with the data returned from an API.
+
+Next, we quickly check the HTTP status code to make sure that it was 200. If it wasn't, that means that something went wrong with our request and we'll want to throw an exception to quit our program.
+
+```python
+ if status_code != 200:
+ raise RuntimeError(f"An error occurred. HTTP Code: {status_code}.")
+```
+
+If everything went OK, we get the JSON from the response as a Python dictionary, using `response.json()`, next, we `return` the data in the "items" key back to the main method. In this case, we don't care about the additional data that the API returned.
+
+```python
+ response_json = response.json()
+ return response_json["items"]
+```
+
+
+Next, back in the main method, we go through each result, and print out a line with the name of the repo, the language, and the amount of stars it has.
+
+```python
+ print(f"-> {name} is a {language} repo with {stars} stars.")
+```
+
+{{% /expand %}}
+
+
+### End Result
+
+The end result looks something like this:
+
+```bash
+(env) $ python dayone.py
+
+-> freeCodeCamp is a JavaScript repo with 298046 stars.
+-> bootstrap is a JavaScript repo with 131403 stars.
+-> vue is a JavaScript repo with 130099 stars.
+-> react is a JavaScript repo with 123969 stars.
+-> d3 is a JavaScript repo with 82932 stars.
+-> javascript is a JavaScript repo with 82514 stars.
+-> react-native is a JavaScript repo with 74810 stars.
+-> create-react-app is a JavaScript repo with 64725 stars.
+-> awesome-python is a Python repo with 63693 stars.
+... and more results
+```
+
+{{% notice note %}}
+Don't worry if you didn't understand this code, we'll be building this program in the workshop today.
+{{% /notice %}}
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/010-Best-Practices/02-brief-history.md b/website/content/02-introduction-to-python/010-Best-Practices/02-brief-history.md
new file mode 100644
index 0000000..6f7ee74
--- /dev/null
+++ b/website/content/02-introduction-to-python/010-Best-Practices/02-brief-history.md
@@ -0,0 +1,59 @@
+---
+title: "About Python 🐍"
+date: 2019-01-24T11:58:21-08:00
+draft: false
+weight: 2
+---
+
+### What is Python?
+
+Python is a programming language created by Guido Van Rossum in the late 1980s.
+
+You might be surprised to learn that Python is 30 years old!
+
+### Where is it used?
+
+A common misconception is that Python is a scripting language. It's used at companies from Reddit, to Netflix, to Dropbox.
+
+### What's all this about Python 2 vs Python 3
+
+This part is a bit of a stain on Python's history. Python 3 was released 2008, and its adoption was slow. First and foremost because it took popular packages a fair amount of time to port over their code.
+
+This debate is now over. Python 2 will reach end-of-life in 2020, meaning that important updates - including security updates - will stop being released. That's why this course focuses on *Python3 only*.
+
+### Why Python?
+
+Python the language is [open source](https://github.com/python/cpython).
+
+Python has a wide variety of applications such as:
+
+- AI/ML
+ - SciPi
+ - NumPy
+ - Pandas
+ - PyTorch
+- Hardware & Micro-controllers
+ - Raspberry Pi
+ - MicroPython
+ - CircuitPython
+- Web Development
+ - Django
+ - Flask
+- Scripting
+ - DevOps
+ Configuration scripts
+
+Python has an incredibly rich fully featured [standard library](https://docs.python.org/3/library/), as well as the [PyPI Package Index](https://pypi.org/) for 3rd party packages, which as of February 2019 contains 167,107 packages.
+
+Python is considered to be a "batteries included" language, because the standard library contains a majority of the libraries and packages you'll need in a standard application.
+
+
+### Continuing In The Community
+
+The absolute best part about Python is the incredibly supportive community.
+
+The biggest yearly conference is [PyCon US](https://us.pycon.org) with approximately 4000 attendees.
+
+There are many local user groups worldwide, with many listed on [this wiki](https://wiki.python.org/moin/LocalUserGroups).
+
+There are many supportive groups for women and non-binary developers, such as [PyLadies](https://www.pyladies.com/) and [DjangoGirls](https://djangogirls.org/). These organizations have chapters in most major cities.
diff --git a/website/content/02-introduction-to-python/010-Best-Practices/03-conventions.md b/website/content/02-introduction-to-python/010-Best-Practices/03-conventions.md
new file mode 100644
index 0000000..688bcdd
--- /dev/null
+++ b/website/content/02-introduction-to-python/010-Best-Practices/03-conventions.md
@@ -0,0 +1,10 @@
++++
+title = "Conventions"
+date = 2019-02-10T21:46:57-08:00
+weight = 3
+draft = false
++++
+
+### PEP8
+
+[PEP8](https://pep8.org/) is a Python coding standard, that sets guidelines for how our Python code should look like.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/010-Best-Practices/_index.md b/website/content/02-introduction-to-python/010-Best-Practices/_index.md
new file mode 100644
index 0000000..022f813
--- /dev/null
+++ b/website/content/02-introduction-to-python/010-Best-Practices/_index.md
@@ -0,0 +1,14 @@
++++
+title = "Why Choose Python"
+date = 2019-01-25T15:21:05-06:00
+weight = 10
+chapter = true
+draft = false
+pre = "1. "
++++
+
+### Chapter 1
+
+# Why Python?
+
+Let's talk about the anatomy of a Python program, what makes Python unique, and cover some best practices.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/020-basic-data-types/00-variables.md b/website/content/02-introduction-to-python/020-basic-data-types/00-variables.md
new file mode 100644
index 0000000..8f33146
--- /dev/null
+++ b/website/content/02-introduction-to-python/020-basic-data-types/00-variables.md
@@ -0,0 +1,104 @@
+---
+title: "Variables and Types"
+date: 2019-02-03T23:26:13-08:00
+draft: false
+weight: 1
+---
+
+### Naming Variables
+
+Python variables can't start with a number. In general, they're named all lower case, separated by underscores. Unlike other languages, that name their variables with camelCase.
+
+You don't want to name your variables the same as the *types* that we'll be working with. For example **don't** name your variables `int`, `list`, `dict`. Etc.
+
+### Open The REPL
+
+Learn about variables by typing along in the Python REPL with me.
+
+{{% notice tip %}}
+Open the REPL from VS Code by opening the command palette (ctrl + shift + P on Windows, or cmd + shift + P on Mac) and selecting Python: Start REPL
+{{% /notice %}}
+
+Any Python code that starts with the `>>>` symbols indicates that it was typed into a REPL.
+
+You can then use ctrl + ` (backtick) to open and close the VS Code terminal on Mac, or ctrl + ' (single quote) on Windows. You won't lose your work in the REPL unless you close VS Code.
+
+{{% notice info %}}
+If you'd like to save the contents of your REPL as class goes on, you can right click, select all, and paste it into a new file.
+{{% /notice %}}
+
+## Variables
+
+Variables in Python allow us to store information and give it a label that we can use to retrieve that information later. We can use variables to store numbers, strings (a sequence of characters), or even more complex data types like lists and dictionaries.
+
+We assign _values_ to _variables_ by putting the _value_ to the right of an equal sign.
+
+Because Python is a *dynamic* language, we don't need to declare the type of the variables before we store data in them.
+
+That means that this is valid Python code:
+
+```python
+>>> x = 42
+```
+
+Unlike typed languages, the type of what's contained in Python variables can change at any time.
+
+For example, the below is perfectly valid Python code:
+
+```python
+>>> x = 42
+>>> x = "hello"
+```
+
+Here, the value of the variable `x` changed from a number to a string.
+
+When creating variables, there are a few best practices you should follow.
+
+#### Naming Variables
+
+Convention says that variables should be named in lowercase, with whole words separated by underscores.
+
+{{% notice note %}}
+If you want to learn more about Python naming conventions look at [PEP8](https://www.python.org/dev/peps/pep-0008/#naming-conventions) during a break.
+{{% /notice %}}
+
+Because Python is a dynamic language and you don't have type hints to explain what's stored inside a variable while reading code, you should do your best naming your variables to describe what is stored inside of them.
+
+It's ok to be _verbose_. For example, `n` is a poor variable name, while `numbers` is a better one. If you're storing a collection of items, name your variable as a plural.
+
+{{% notice note %}}
+Learn more about great naming practices for dynamic types by watching this 30-minute [talk by Brandon Rhodes](https://www.youtube.com/watch?v=YklKUuDpX5c).
+{{% /notice %}}
+
+#### Naming Gotchas
+
+There are some things that you can't name your variables, such as `and`, `if`, `True`, or `False`. That's because Python uses these names for program control structure.
+
+You can't start your variable name with a digit, although your variable name can end in a digit. Your variable name can't contain special characters, such as `!`, `@`, `#`, `$`, `%` and more.
+
+{{% notice warning %}}
+💣 Python will let you override built-in methods and types without a warning so don't name your Python variables things like `list`, `str`, or `int`.
+{{% /notice %}}
+
+If you notice your program behaving oddly and you can't find the source of the bug, double check the list of [built-in functions](https://docs.python.org/3/library/functions.html) and [built-in types](https://docs.python.org/3/library/stdtypes.html) to make sure that your variable names don't conflict.
+
+## Types
+
+Python has a very easy way of determining the type of something, with the `type()` function.
+
+```python
+>>> num = 42
+>>> type(num)
+
+```
+
+### No-Value, `None`, or Null Value
+
+There's a special type in Python that signifies no value at all. In other languages, it might be called Null. In Python, it's called `None`.
+
+If you try to examine a variable on the REPL that's been set to `None`, you won't see any output. We'll talk more about the `None` type later in the class.
+
+```python
+>>> x = None
+>>> x
+```
diff --git a/website/content/02-introduction-to-python/020-basic-data-types/02-numbers.md b/website/content/02-introduction-to-python/020-basic-data-types/02-numbers.md
new file mode 100644
index 0000000..bbaea7a
--- /dev/null
+++ b/website/content/02-introduction-to-python/020-basic-data-types/02-numbers.md
@@ -0,0 +1,58 @@
+---
+title: "Numbers"
+date: 2019-02-03T23:14:25-08:00
+draft: false
+weight: 2
+---
+
+First, open up the REPL.
+
+{{% notice tip %}}
+Remember, you'll learn best if you type along with me.
+{{% /notice %}}
+
+There are three different types of numbers in Python: `int` for Integer, Float, and Complex.
+
+```python
+# These are all integers
+x = 4
+y = -193394
+z = 0
+```
+
+```python
+# These are all floats
+x = 5.0
+y = -3983.2
+z = 0.
+```
+
+```python
+# This is a complex number
+x = 42j
+```
+
+In Python, Integers and other simple data types are just objects under the hood. That means that you can create new ones by calling methods. You can provide either a number, or a string. This will come in handy later on in the course.
+
+```python
+x = int(4)
+y = int('4')
+z = float(5.0)
+```
+
+Python also provides a `decimal` library, which has certain benefits over the `float` datatype. For more information, refer to the [Python documentation](https://docs.python.org/3/library/decimal.html).
+
+## Mathematical Operations
+
+Numbers can be added together. If you add a `float` and an `int`, the resulting type will be a `float`.
+
+If you divide two `int`s (integers), the result will be of type `float`.
+
+## Boolean Types
+
+In Python, Booleans are of type `bool`. Surprisingly, the boolean types `True` and `False` are also numbers under the hood.
+
+* `True` is `1` under the hood.
+* `False` is `0` under the hood.
+
+That means you can do silly things, like add two Boolean numbers together, but I'll cover why this is a useful Python feature later in the course.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/020-basic-data-types/03-strings.md b/website/content/02-introduction-to-python/020-basic-data-types/03-strings.md
new file mode 100644
index 0000000..19790e7
--- /dev/null
+++ b/website/content/02-introduction-to-python/020-basic-data-types/03-strings.md
@@ -0,0 +1,75 @@
+---
+title: "Strings"
+date: 2019-02-03T23:14:35-08:00
+draft: false
+weight: 3
+---
+
+#### Representing Strings
+
+Strings in Python can be enclosed either with single quotes like `'hello'` or double quotes, like `"hello"`.
+
+Strings can also be **concatenated** (added together) using the `+` operator to combine an arbitrary number of Strings. For example:
+
+
1334
+
+```python
+salutation = "Hello "
+name = "Nina"
+greeting = salutation + name
+# The value of greeting will be "Hello Nina"
+```
+
+To use the same type of quote within a string, that quote needs to be **escaped** with a `\` - backwards slash.
+
+```python
+greeting = 'Hello, it\'s Nina'
+```
+
+Alternately, mixed quotes can be present in a Python string without escaping.
+
+```python
+# Notice that the single quote ' is surrounded by
+# double quotes, ""
+greeting = "Hello, it's Nina"
+```
+
+Long multi-line strings can be represented in between `"""` (triple quotes), but the whitespace will be part of the string.
+
+```python
+long_greeting = """
+ Greetings and salutations, dear Nina.
+ I'm superfluous with my words,
+ and require more space to say Hello!"
+ """
+```
+
+#### Printing Strings
+
+Strings can be printed out using the `print()` function in Python. While you're working the REPL, you'll see that variables are displayed for you. When you move on to writing standalone Python programs, that will no longer be the case.
+
+To use the `print()` function, call it with a regular or formatted string.
+
+```python
+>>> print("Hello")
+Hello
+>>> name = "Nina"
+>>> print(name)
+Nina
+```
+
+#### String Formatting
+
+There are several types of string formatting in Python.
+
+If you're using Python 3.7 and above (remember to check with `python --version` on the command line) you can use my favorite type of string formatting, and the one I'll be using for the course called f-strings.
+
+```python
+>>> name = "Nina"
+>>> greeting = f"Hello, {name}"
+
+>>> print(greeting)
+Hello, Nina
+```
+
+f-strings allow you to simply and easily reference variables in your code, and as a bonus, they're *much* faster.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/020-basic-data-types/05-common-string-and-number-errors.md b/website/content/02-introduction-to-python/020-basic-data-types/05-common-string-and-number-errors.md
new file mode 100644
index 0000000..0e6e4c0
--- /dev/null
+++ b/website/content/02-introduction-to-python/020-basic-data-types/05-common-string-and-number-errors.md
@@ -0,0 +1,72 @@
+---
+title: "Common Mistakes"
+date: 2019-02-10T18:33:43-08:00
+draft: false
+weight: 5
+---
+
+There are a few common errors that you'll encounter when working with Strings and numbers. Remember, in Python program errors are called Exceptions. By going over what they are, you'll be able to recognize them immediately.
+
+### Scenario 1: Mismatched string quotes
+
+{{% notice info %}}
+Mismatched string quotes will result in a `SyntaxError`
+{{% /notice %}}
+
+When we try to start a String with one type of quote, and end with another, we'll see a syntax error.
+
+For example, starting the string Hello with a double quote, and ending in a single quote, like this:
+
+#### Input: **"**Hello**'**
+
+For example, in the REPL:
+
+```python
+>>> name = 'Hello"
+ File "", line 1
+ name = "Hello'
+ ^
+SyntaxError: EOL while scanning string literal
+```
+
+**Solution:** use matching quote types for defining your strings. Either single quotes `'Hello'` or double quotes `"Hello"`.
+
+### Scenario 2: Trying to print a String and a number with concatenation using the "+" symbol.
+
+{{% notice info %}}
+Trying to add or concatenate a String and a number will result in a `TypeError`
+{{% /notice %}}
+
+If you add try to add (or concatenate) a String and a number, you'll get an error saying that adding the two types together isn't possible.
+
+#### Input: 3 + "Three"
+
+In the REPL:
+```python
+>>> print(3 + " Three")
+Traceback (most recent call last):
+ File "", line 1, in
+TypeError: unsupported operand type(s) for +: 'int' and 'str'
+```
+
+**Solutions:**
+
+There are two possible solutions here, for two different scenarios.
+
+In the first scenario, you'd like to add a number to a string via concatenation. In order to do that, you must first convert the number to a string via the `str()` method.
+
+In the REPL:
+```python
+>>> my_num = 3
+>>> print(str(my_num) + " Three")
+3 Three
+```
+
+In the second scenario, you'd like to a convert a number that's contained in a string (ex: `"3"`) into an Integer, so you can use it like any other number. In this case, you'd like to convert it to an Integer, with the `int()` method.
+
+In the REPL:
+```python
+>>> str_num = "3"
+>>> print(int(str_num) + 5)
+8
+```
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/020-basic-data-types/10-exercise.md b/website/content/02-introduction-to-python/020-basic-data-types/10-exercise.md
new file mode 100644
index 0000000..6125d85
--- /dev/null
+++ b/website/content/02-introduction-to-python/020-basic-data-types/10-exercise.md
@@ -0,0 +1,76 @@
+---
+title: "Practice"
+date: 2019-02-04T15:28:32-08:00
+draft: false
+weight: 10
+pre: "⭐️ "
+---
+
+## Basic Data Types, Strings and Numbers
+
+### Types
+
+List the type of the following variables using the `type()` function.
+
+```python
+>>> x = 42
+>>> y = 3 / 4
+>>> z = int('7')
+>>> a = float(5)
+>>> name = "Nina"
+```
+
+### Numbers
+
+Calculate the amount of rent you pay daily, by taking your monthly rent and diving it by 30.
+
+```python
+>>> rent = 480
+>>> per_day = rent / 30
+>>> print(per_day)
+16.0
+```
+
+### Strings
+
+Try printing some things to your REPL:
+
+```python
+>>> print("Hello world")
+Hello world
+>>> name = "Nina"
+>>> print("My name is", name)
+My name is Nina
+```
+
+There are three different ways to format strings in Python3. You may run into %-formatting and `str.format()` in older code. These are still common in Python but no longer recommended, due to readability concerns.
+
+```python
+>>> name = "Nina"
+>>> print("Hello, my name is %s" % name)
+Hello, my name is Nina
+```
+
+The current recommended way to format string is with f-Strings. f-Strings are much more readable and easier to maintain than the previous methods. With f-Strings, your string is prepended with the letter `f` and your variables or expressions to interpret are placed in `{brackets}`.
+
+```python
+>>> name = "Nina"
+>>> print(f"Hello, my name is {name} and I pay ${rent / 30} in rent per day")
+Hello, my name is Nina and I pay $16.0 in rent per day
+```
+
+### Helper Functions
+
+Python has a few built-in functions to help you if you get stuck. `type()` tells you what an object's type is, for example a string (`str`) or integer (`int`). `dir()` returns a list of valid attributes for an object, so you can quickly see what variables an object has or what functions you can call on it. `help()` brings up helpful documentation on any object. You can also type `help()` on its own to bring an interactive help console.
+
+```python
+>>> x = 42
+>>> y = 3 / 4
+>>> name = "Nina"
+>>> type(x)
+
+>>> type(y)
+
+>>> type(name)
+
+```
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/020-basic-data-types/_index.md b/website/content/02-introduction-to-python/020-basic-data-types/_index.md
new file mode 100644
index 0000000..9215a11
--- /dev/null
+++ b/website/content/02-introduction-to-python/020-basic-data-types/_index.md
@@ -0,0 +1,14 @@
++++
+title = "Basic Data Types"
+date = 2019-01-25T15:02:51-06:00
+weight = 20
+chapter = true
+draft = false
+pre = "2. "
++++
+
+### Chapter 2
+
+# Basic Data Types
+
+Working with numbers, strings, and more.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/060-functions/10-defining-functions.md b/website/content/02-introduction-to-python/060-functions/10-defining-functions.md
new file mode 100644
index 0000000..6d0befa
--- /dev/null
+++ b/website/content/02-introduction-to-python/060-functions/10-defining-functions.md
@@ -0,0 +1,207 @@
+---
+title: "Defining Functions"
+date: 2019-02-10T18:16:01-08:00
+draft: false
+weight: 10
+---
+
+The purpose of functions in Python are to create reusable code. If we find ourselves copying and pasting the same code multiple times, that's a good sign that a function might help!
+
+### Anatomy of a function
+
+This is the recipe for defining a Python function:
+
+1. `def`: the `def` keyword, telling Python we're about to start a function definition
+1. a name for the function
+1. `(`: opening parenthesis
+1. (optional) the **names** of one or more arguments, separated with `,`
+1. (optional) the **names** and **values** of one or more default arguments, separated with (`,`) *note: we'll see these in the next section*
+1. `)` closing parenthesis
+1. `:` a colon
+
+A function in Python is defined with the `def` keyword, followed by the function names, zero or more argument names contained in parenthesis `()`, and a colon `:` to indicate the start of the function.
+
+The contents of the function then follow.
+
+Then, an *optional* `return` statement can follow, if the function plans on passing data back to the caller.
+
+```python
+# A Basic Function that accepts no arguments and returns nothing.
+def hello_world():
+ print("Hello, World!")
+
+
+# A Function that accepts two arguments, and returns the value of
+# those numbers added together.
+def add_numbers(x, y):
+ return x + y
+```
+
+{{% notice tip %}}
+If you **forget** the recipe while trying to create a function, Python will help you remember with a `SyntaxError`.
+{{% /notice %}}
+
+For example, trying to create a function without the colon `:`:
+
+```python
+>>> def hello_world()
+ File "", line 1
+ def hello_world()
+ ^
+SyntaxError: invalid syntax
+```
+
+And trying to create a function without the parenthesis `()`:
+
+```python
+>>> def hello_world:
+ File "", line 1
+ def hello_world:
+ ^
+SyntaxError: invalid syntax
+```
+
+### Function Contents
+
+The recipe for function contents:
+
+1. a new line
+1. indentation (press tab on your keyboard)
+1. one or more lines
+1. (optional) a `return` statement
+
+#### `return` statement
+
+A `return` statement is a way to "short-circuit" the function.
+
+Using a `return` statement, you can optionally pass back data to the caller of your function.
+
+##### with no `return` statement
+
+If a function doesn't have a return statement, it implicitly returns `None`.
+
+```python
+>>> def foo():
+... x = 5
+...
+>>> val = foo()
+>>> type(val)
+
+```
+
+##### with a `return` statement, but no value
+
+If a function has a return statement, but no value, it also returns `None`. This is typically used to control the flow of a program.
+
+```python
+>>> def foo():
+... x = 5
+... return
+...
+>>> val = foo()
+>>> type(val)
+
+```
+
+##### with a `return` statement and a value
+
+To return a value from a function, just type it after the `return` statement. You can return anything from a Python function, including other functions! For today, we'll focus on simple and complex data types.
+
+```python
+>>> def foo():
+... x = 5
+... return x
+...
+>>> val = foo()
+>>> val
+5
+```
+
+As we explore simple functions, our `return` statements will usually be at the end of the function, but that's not the only way they can be used. A function can have multiple `return` statements, and those `return` statements can be used to help control the flow of the program.
+
+{{% notice note %}}
+Note: Because it's syntactically correct to have multiple return statements in a function, it's up to you to use them correctly. If you use a linter for your code files and you place additional code in a function **after** a return statement, the
+linter will give you a helpful hint about the rest of the code being unreachable.
+{{% /notice %}}
+
+#### Indentation
+
+One of the most important aspects of functions is indentation. Remember, Python doesn't use curly braces to figure out what's inside a function like other languages you've seen like JavaScript or Java.
+
+Python knows what code is related to a function by how it's indented. Anything that's indented one level deep under the function declaration is part of the function, no matter how many spaces there are between lines.
+
+To add a level of indentation, just press the **Tab** key on your keyboard after entering a new line.
+
+If you're using the REPL, once you're done entering your function, you'll need to press enter an additional time, to mark the end of the function. You know you're done defining your function when you see the 3 input arrows `>>>` again.
+
+Let's try it together. Type the following code in your REPL. Note that the 3 dots '...' indicate that those lines are *indented* in the REPL. If you type your code in a Python file, you won't see the `...` dots.
+
+```python
+>>> def add_numbers(x, y):
+... return x + y
+...
+```
+
+{{%expand "See an error? Expand this section." %}}
+Note: If you get an `IndentationError`, that means that you didn't correctly indent your code after your function definition. Try typing your function into the REPL one more time.
+
+```python
+# The error you'll see if you didn't indent your function correctly.
+>>> def add_numbers(x, y):
+... return x + y
+File "", line 2
+ return x + y
+ ^
+IndentationError: expected an indented block
+```
+{{% /expand%}}
+
+### Calling Functions
+
+#### With no arguments
+
+Once you've defined a function, you can call it from your Python code as many times as you'd like.
+
+To call a Python function, type in it's name, along with parenthesis, and any *required* arguments to the function. Let's try it now, with a function that doesn't require arguments.
+
+```python
+>>> def hello_world():
+... print("Hello, World!")
+...
+>>> hello_world()
+Hello, World!
+```
+
+#### With arguments
+
+Let's try it again, this time with a function that does accept arguments.
+
+{{% notice tip %}}
+Here, note that the function accepts **names** for the arguments. But, when we call the function, we're passing in **values**.
+{{% /notice %}}
+
+```python
+>>> def add_numbers(x, y):
+... return x + y
+...
+>>> add_numbers(3, 5)
+8
+>>>
+```
+
+#### Storing the `return`ed value of a function.
+
+Storing the `return`ed value of a function is easy. All you need to do is assign it to a variable.
+
+Let's try it now.
+
+```python
+>>> def add_numbers(x, y):
+... return x + y
+...
+>>> new_number = add_numbers(3, 5)
+>>> new_number
+8
+```
+
+The variable `new_number` now contains the result of running our `add_numbers` function with our arguments `3` and `5`.
diff --git a/website/content/02-introduction-to-python/060-functions/30-function-arguments.md b/website/content/02-introduction-to-python/060-functions/30-function-arguments.md
new file mode 100644
index 0000000..a422696
--- /dev/null
+++ b/website/content/02-introduction-to-python/060-functions/30-function-arguments.md
@@ -0,0 +1,192 @@
+---
+title: "Function Arguments"
+date: 2019-02-10T18:16:14-08:00
+draft: false
+weight: 30
+---
+
+### Arguments in Practice
+
+### Positional arguments are required
+
+Positional arguments are all required, and must be given in the order they are declared.
+
+For example, this function doesn't do what we expected, because we passed in our arguments in the wrong order.
+
+In the REPL:
+
+```python
+>>> def say_greeting(name, greeting):
+... print(f"{greeting}, {name}.")
+...
+>>> say_greeting("Hello!", "Nina")
+Nina, Hello!.
+```
+
+### Keyword arguments with default values
+
+Functions can accept two types of named arguments, ones without default values, and ones with default values. Arguments that have default values are called **keyword arguments**. The nice thing about defaults is they can be overridden when needed.
+
+Let's see this in practice, by writing two functions that print out a greeting. One function will have a default argument to make things easier for us.
+
+```python
+# No default arguments
+def say_greeting(greeting, name):
+ print(f"{greeting}, {name}.")
+
+# Default argument - greeting will always be
+# Hello, if one isn't provided.
+def say_greeting_with_default(name, greeting="Hello", punctuation="!"):
+ print(f"{greeting}, {name}{punctuation}")
+```
+
+#### Without default arguments
+
+Now, in the REPL, let's try calling our function with no default arguments:
+
+```python
+>>> # No Default arguments
+>>> def say_greeting(greeting, name):
+... print(f"{greeting}, {name}.")
+...
+>>> say_greeting("Good Day", "Nina")
+Good Day, Nina.
+```
+
+#### Using default arguments
+
+Let's make a new function, `say_greeting_with_default` that accepts two arguments -- `name`, and a now **optional** argument, `greeting`. If `greeting` is not passed in, it will default to `Hello`.
+
+In the REPL:
+
+```python
+>>> # With Default Arguments
+>>> def say_greeting_with_default(name, greeting="Hello", punctuation="!"):
+... print(f"{greeting}, {name}{punctuation}")
+...
+>>> say_greeting_with_default("Nina")
+Hello, Nina!
+>>> say_greeting_with_default("Nina", "Good Day")
+Good Day, Nina!
+```
+
+#### Order matters!
+
+A function can accept all of one type or the other, but arguments need to go in a specific order.
+
+All of the *required arguments go first*. They are then *followed by the optional keyword arguments*.
+
+What happens when we try to define our arguments out of order? If you guessed a `SyntaxError`, you're correct!
+
+```python
+>>> def say_greeting_bad(greeting="Hello", name):
+... print("Oops, this won't work!")
+...
+ File "", line 1
+SyntaxError: non-default argument follows default argument
+```
+
+### Calling functions with arguments
+
+There are a few important things to know about calling functions with arguments.
+
+#### Arguments without defaults are **required**!
+
+Arguments without default values are **required** by Python. Otherwise your function wouldn't know what to do! If you don't pass in all the required arguments, you'll get a `TypeError`.
+
+In the REPL:
+```python
+>>> def say_greeting(name, greeting):
+... print(f"{greeting}, {name}.")
+...
+>>> say_greeting("Nina")
+Traceback (most recent call last):
+ File "", line 1, in
+TypeError: say_greeting() missing 1 required positional argument: 'greeting'
+```
+
+#### You can pass in none, some, or all of the keyword arguments.
+
+If your function takes keyword arguments, you can provide zero, one, or all of them when you call it. You don't need to pass these arguments in order either.
+
+```python
+>>> def create_query(language="JavaScipt", num_stars=50, sort="desc"):
+... return f"language:{language} num_stars:{num_stars} sort:{sort}"
+...
+>>> create_query()
+'language:JavaScipt num_stars:50 sort:desc'
+>>> create_query(language="Ruby")
+'language:Ruby num_stars:50 sort:desc'
+>>> create_query(num_stars=1, language="Python", sort="asc")
+'language:Python num_stars:1 sort:asc'
+```
+
+#### You can pass in required parameters by keyword.
+
+Even if your function arguments don't have keyword arguments with defaults, you can still pass values in to the function by name. This is especially helpful if you want to be extra clear about what you're passing in.
+
+```python
+>>> def say_greeting(name, greeting):
+... print(f"{greeting}, {name}.")
+...
+>>> say_greeting("Nina", "Hello")
+Hello, Nina.
+>>> say_greeting(name="Max", greeting="Bonjour")
+Bonjour, Max.
+```
+
+### Arguments Danger Zone
+
+{{% notice warning %}}
+Never use mutable types, like `list`s as a default argument.
+{{% /notice %}}
+
+We'll talk more about `list`s and mutability in the coming chapter, but for the time being remember to never use an empty list as a default value to a function.
+
+Why? Because it won't work like you'd expect it to.
+
+```python
+>>> # Don't do this!
+>>> def add_five_to_list(my_list=[]):
+... my_list.append(5)
+... return my_list
+...
+>>> # This works like we expected it to.
+>>> add_five_to_list()
+[5]
+>>> # Huh?
+>>> add_five_to_list()
+[5, 5]
+>>> # We see that the original `my_list` is still being modified.
+>>> add_five_to_list()
+[5, 5, 5]
+```
+
+If you need to use a mutable type, like a `list` as a default, use a *marker* instead. We'll cover this technique when we talk about `list`s in the next chapter.
+
+In Python, default arguments are evaluated only once -- when the function is defined. Not each time the function is called. That means if you use a value that can be changed, it won't behave like you'd expect it to.
+
+### Naming Functions and Arguments
+
+Because Python is a dynamic language (sometimes called duck-typed) we use names as cues for what our function does, the arguments it accepts, and the values it returns.
+
+This is especially important because we generally don't declare *types* for our programs when we're first starting out. *Note: Python does support Type hinting, but it's more of an intermediate feature. Make sure you have the basics down before learning more about it.*
+
+{{% notice tip %}}
+Try to avoid single character names for your functions and variables, unless they have meaning in math.
+{{% /notice %}}
+
+For example, in this function, `x` and `y` are common names used when referring to points, so it's OK to use single-letter names in this scenario.
+
+```python
+def add_points(x1, y1, x2, y2):
+ return x1 + x2, y1 + y2
+```
+
+For sequences, like `list`s, it's appropriate to name them in the plural.
+
+For example, I'd expect a variable called `name` to be a single string, and a variable called `names` to be a list of strings.
+
+{{% notice tip %}}
+A great resource to help you figure out the best naming conventions to use in your production Python code is a talk by Brandon Rhodes, called ["The Naming of Ducks: Where Dynamic Types Meet Smart Conventions"](https://www.youtube.com/watch?v=YklKUuDpX5c).
+{{% /notice %}}
diff --git a/website/content/02-introduction-to-python/060-functions/40-scope.md b/website/content/02-introduction-to-python/060-functions/40-scope.md
new file mode 100644
index 0000000..f03f527
--- /dev/null
+++ b/website/content/02-introduction-to-python/060-functions/40-scope.md
@@ -0,0 +1,76 @@
+---
+title: "Function Scope"
+date: 2019-02-10T18:30:11-08:00
+draft: false
+weight: 40
+---
+
+### Scope inside a function
+
+Inside of a function in Python, the **scope** changes.
+
+Think about it this way: scoping in Python happens with whitespace. When we delineate the code a function contains by indenting it under a function definition, it's scope **changes** to a new internal scope. It has access to the variables defined outside of it, but it can't change them.
+
+Once the function is done running, its scope goes away, as do its defined variables.
+
+Let's double check this in the REPL:
+
+```python
+>>> def twitter_info():
+... twitter_account = "nnja"
+... print(f"Account inside function: {twitter_account}")
+...
+>>> twitter_info()
+Account inside function: nnja
+>>> print(f"Account outside of function: {twitter_account}")
+Traceback (most recent call last):
+ File "", line 1, in
+NameError: name 'twitter_account' is not defined
+```
+
+We get a `NameError` when trying to access the `twitter_account` variable outside of the function. That's because it's out of scope, exactly like we expected it to be.
+
+
+### Using variables defined outside of the function
+
+Generally, we want to be careful when using variables defined outside of our function.
+
+Note, that if we try to change the value of a variable defined outside of our function, we'll see the changes in the function, but not outside of it.
+
+You can't change variables defined outside of the function inside of the function. If you try to, your changes will only apply while the function is running. Once the function is done running, the value goes back to what it was before your function ran.
+
+A little confusing, but let's see it in action:
+
+```python
+>>> name = "Nina"
+>>> print(f"Name outside of function: {name}")
+Name outside of function: Nina
+>>>
+>>> def try_change_name():
+... name = "Max"
+... print(f"Name inside of function: {name}")
+...
+>>> try_change_name()
+Name inside of function: Max
+>>> print(f"Name outside of function: {name}")
+Name outside of function: Nina
+```
+
+If we didn't know what to look for, the program might not behave how we'd expect it to. A good rule of thumb is to name our variables clearly, and minimize how many variables we declare outside of functions and classes, which you'll learn about in day two.
+
+
+{{% notice tip %}}
+An appropriate use is when using a constant, a variable defined in all caps, with the words separated by underscores. A constant is a value that we expect to use several times within our program, but we never expect to change it programmatically.
+{{% /notice %}}
+
+For example:
+
+```python
+>>> ROOT_API_URL = "https://api.github.com"
+>>> def api_search_repos_url():
+... return f"{ROOT_API_URL}/search/repositories"
+...
+>>> api_search_repos_url()
+'https://api.github.com/search/repositories'
+>>>
+```
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/060-functions/50-exercise.md b/website/content/02-introduction-to-python/060-functions/50-exercise.md
new file mode 100644
index 0000000..4c5aeb0
--- /dev/null
+++ b/website/content/02-introduction-to-python/060-functions/50-exercise.md
@@ -0,0 +1,129 @@
+---
+title: "Practice"
+date: 2019-03-02T00:00:00-08:00
+draft: false
+weight: 50
+pre: "⭐️ "
+---
+
+## Functions
+
+Let's try creating a basic function. Use tab to indent the second line, and press enter on an empty line to finish the function.
+
+```python
+>>> def add_numbers(x, y):
+... return x + y
+... # Press Enter
+```
+
+Now let's try our new function. Type this into your REPL:
+
+```python
+>>> add_numbers(1, 2)
+# Let's use the string formatting we learned in the last chapter
+>>> print(f"The sum of 1 and 2 is {add_numbers(1, 2)}")
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> add_numbers(1, 2)
+3
+# Let's use the string formatting we learned in the last chapter
+>>> print(f"The sum of 1 and 2 is {add_numbers(1, 2)}")
+The sum of 1 and 2 is 3
+```
+
+{{% /expand%}}
+
+
+## The Importance of Whitespace
+
+Here's an error that you'll become very familiar with during your career as a Pythonista, the `IndentationError`. Whitespace is important for defining function scope in python, so missing or extra indentations or spaces will cause the runtime to throw this error. Let's redefine our `add_numbers` function, but we'll forget to indent the second line, `return x + y`. Notice that the second line is directly under (at the same indentation level) as the `def`:
+
+```python
+>>> def add_numbers(x, y):
+... return x + y
+ File "", line 2
+ return x + y
+ ^
+IndentationError: expected an indented block
+```
+
+Notice how the runtime tells us the line that failed (`line 2`), gives you a copy of the line with an arrow pointing to the offending error (`return x + y`), and then tells you the error (`IndentationError`) with additional information (`expected an indented block`).
+
+## Function Scope
+
+As we saw earlier, scoping in Python happens with whitespace. Let's see this in action:
+
+```python
+>>> x = 1
+>>> y = 2
+>>> def add_numbers(x, y):
+... print(f"Inside the function, x = {x} and y = {y}")
+... return x + y
+...
+>>> print(f"Outside the function, x = {x} and y = {y}")
+>>> print(f"The sum of 5 and 6 is {add_numbers(5, 6)}")
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> x = 1
+>>> y = 2
+>>> def add_numbers(x, y):
+... print(f"Inside the function, x = {x} and y = {y}")
+... return x + y
+...
+>>> print(f"Outside the function, x = {x} and y = {y}")
+Outside the function, x = 1 and y = 2
+>>>
+>>> print(f"The sum of 5 and 6 is {add_numbers(5, 6)}")
+Inside the function, x = 5 and y = 6
+The sum of 5 and 6 is 11
+```
+
+{{% /expand%}}
+
+
+## Positional Arguments vs Keyword Arguments
+
+The `x` and `y` arguments for our `add_numbers()` function are called positional arguments. Python also lets us declare *keyword* arguments. Keyword arguments are great for setting default values, because passing them is optional. Just remember that keyword arguments must come *after* any positional arguments. Let's make a more generic function for doing math:
+
+```python
+>>> def calculate_numbers(x, y, operation="add"):
+... if operation == "add":
+... return x + y
+... elif operation == "subtract":
+... return x - y
+...
+# Let's try our new function. Remember, if we don't pass the operation keyword argument, the default is "add"
+>>> calculate_numbers(2, 3)
+# You can pass a keyword argument as a normal positional argument
+>>> calculate_numbers(2, 3, "subtract")
+# You can also use the argument's keyword. This helps with readability
+>>> calculate_numbers(2, 3, operation="subtract")
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> def calculate_numbers(x, y, operation="add"):
+... if operation == "add":
+... return x + y
+... elif operation == "subtract":
+... return x - y
+...
+# Let's try our new function. Remember, if we don't pass the operation keyword argument, the default is "add"
+>>> calculate_numbers(2, 3)
+5
+# You can pass a keyword argument as a normal positional argument
+>>> calculate_numbers(2, 3, "subtract")
+-1
+# You can also use the argument's keyword. This helps with readability
+>>> calculate_numbers(2, 3, operation="subtract")
+-1
+```
+
+{{% /expand%}}
diff --git a/website/content/02-introduction-to-python/060-functions/_index.md b/website/content/02-introduction-to-python/060-functions/_index.md
new file mode 100644
index 0000000..513ec15
--- /dev/null
+++ b/website/content/02-introduction-to-python/060-functions/_index.md
@@ -0,0 +1,14 @@
++++
+title = "Functions"
+date = 2019-01-25T15:03:48-06:00
+weight = 60
+chapter = true
+draft = false
+pre = "3. "
++++
+
+### Chapter 3
+
+# Functions
+
+Functions help us organize our code in a way that's reusable, and accept arguments and defaults where needed.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/080-advanced-datatypes/10-lists.md b/website/content/02-introduction-to-python/080-advanced-datatypes/10-lists.md
new file mode 100644
index 0000000..3416806
--- /dev/null
+++ b/website/content/02-introduction-to-python/080-advanced-datatypes/10-lists.md
@@ -0,0 +1,205 @@
+---
+title: "Lists, Part 1"
+date: 2019-02-10T18:20:32-08:00
+draft: false
+weight: 10
+---
+
+Lists are one of the most powerful data types in Python. Generally, they're container objects used to store related items together.
+
+### `list` cheat sheet
+
+| type | `list` |
+|------------------ |--------------------------------------------------------------------------------------- |
+| use | Used for storing similar items, and in cases where items need to be added or removed. |
+| creation | `[]` or `list()` for empty list, or `[1, 2, 3]` for a list with items. |
+| search methods | `my_list.index(item)` or `item in my_list` |
+| search speed | Searching in an item in a large list is slow. Each item must be checked. |
+| common methods | `len(my_list)`, `append(item)` to add, `insert(index, item)` to insert in the middle, `pop()` to remove. |
+| order preserved? | Yes. Items can be accessed by index. |
+| mutable? | Yes |
+| in-place sortable? | Yes. `my_list.sort()` will sort the list in-place. `my_list.sort(reverse=True)` will sort the list in-place in *descending* order. `my_list.reverse()` will *reverse the items* in `my_list` in-place. |
+
+### In Practice
+
+Let's create a few lists to see how they work.
+
+An empty list can be created in two ways. The first, by calling the `list()` method. More commonly, it's created with two empty brackets `[]`. Don't forget to check the type of the list with the `type` built-in function.
+
+```python
+>>> list()
+[]
+>>> []
+[]
+>>> type(list())
+
+>>> type([])
+
+```
+
+Let's create our list with a few items in it. Let's say we want to keep track of a list of names. We add items to our list, and separate them with commas `,`.
+
+```python
+>>> names = ["Nina", "Max", "Jane"]
+```
+
+We can check its length with the built-in `len()` method, like so:
+
+```python
+>>> len(names)
+3
+```
+
+### Indexes and Indices
+
+Lists retain the order of the items in them. In the next section, you'll learn about some data structures that don't.
+
+In order to *access* items in a list, we'll need to use an *index*. (Multiple indexes are sometimes also called indices). The index for the item you want to access is *an integer* put in *square brackets* after the list.
+
+{{% notice tip %}}
+**Indexes start at 0** in Python and most other programming languages.
+{{% /notice %}}
+
+```python
+>>> names = ["Nina", "Max", "Jane"]
+>>> names[0]
+'Nina'
+>>> names[1]
+'Max'
+>>> names[2]
+'Jane'
+```
+
+#### Updating an item in a list
+
+To update a particular item in a `list` use square-bracket notion and assign a new value. `my_list[pos] = new_value`
+
+```python
+>>> names = ["Nina", "Max", "Jane"]
+>>> names[2] = "Floyd"
+>>> names
+['Nina', 'Max', 'Floyd']
+```
+
+{{% notice info %}}
+If you try to access an index that is greater than or equal to (>=) the length of the list, you'll get an `IndexError`.
+{{% /notice %}}
+
+```python
+>>> names = ["Nina", "Max", "Jane"]
+>>> len(names)
+3
+>>> names[3]
+Traceback (most recent call last):
+ File "", line 1, in
+IndexError: list index out of range
+```
+
+#### Formatting
+
+{{% notice tip %}}
+We can *optionally* add new lines after the commas. This helps with readability for more complex list items.
+{{% /notice %}}
+
+Notice that we can also *optionally* add a trailing comma after the last item. A trailing comma isn't required to create a valid list, but it does help minimize version control differences when working on a Python codebase with a team.
+
+```python
+>>> names = [
+... "Nina",
+... "Max",
+... "Jane",
+... ]
+```
+
+
+### Common Gotchas
+
+{{% notice info %}}
+If you forget to include commas between your items, you'll get a `SyntaxError`.
+{{% /notice %}}
+
+```python
+>>> numbers = [1, 2 3]
+ File "", line 1
+ numbers = [1, 2 3]
+ ^
+SyntaxError: invalid syntax
+```
+
+The REPL makes it difficult to forget the closing bracket, but if you forget it while writing code in a Python file, you'll see a `SyntaxError` with a different name. It'll say: `SyntaxError: unexpected EOF while parsing` or `SyntaxError: invalid syntax`.
+
+For example:
+
+```python
+# Python file: program.py
+names = ["Nina",
+x = 5
+```
+
+Notice how the `SyntaxError` points to a completely valid line of Python code. In these cases, you also need to check the line of code **before** the line with the `SyntaxError`. There, we'll notice that we forgot the closing bracket of our `names` list.
+
+```bash
+# In a shell
+(env) $ python program.py
+ File "/Users/nina/Desktop/program.py", line 2
+ x = 5
+ ^
+SyntaxError: invalid syntax
+```
+
+### Sorting
+
+Sorting sounds complicated, but in practice, it's just one method call away!
+
+#### Sorting a Copy Of Your List
+
+If you'd like sort to return a brand new copy of your list, instead of modifying your original copy, you can use the built-in `sorted(my_list)` function on your list to return a *new* `list`, sorted in increasing (ascending) order. Or use `sorted(my_list, reverse=True)` to create a new `list` sorted backwards, in decreasing (or descending) order. This operation will **not modify** the underlying list.
+
+Either of these operations will return a *new* list.
+
+```python
+>>> lottery_numbers = [1, 4, 32423, 2, 45, 11]
+>>> sorted(lottery_numbers)
+[1, 2, 4, 11, 45, 32423]
+>>> lottery_numbers
+[1, 4, 32423, 2, 45, 11]
+>>> sorted(lottery_numbers, reverse=True)
+[32423, 45, 11, 4, 2, 1]
+>>> lottery_numbers
+[1, 4, 32423, 2, 45, 11]
+```
+
+#### Sorting the list in-place
+
+You can call `my_list.sort()` on your list to sort it in increasing (ascending) order, or `my_list.sort(reverse=True)` on the list to sort it backwards, in decreasing (or descending) order. This operation will modify the underlying list, and *doesn't return a value*.
+
+```python
+>>> lottery_numbers = [1, 4, 32423, 2, 45, 11]
+>>> lottery_numbers.sort()
+>>> lottery_numbers
+[1, 2, 4, 11, 45, 32423]
+
+>>> lottery_numbers.sort(reverse=True)
+>>> lottery_numbers
+[32423, 45, 11, 4, 2, 1]
+
+>>> words = ["Umbrella", "Fox", "Apple"]
+>>> words.sort()
+>>> words
+['Apple', 'Fox', 'Umbrella']
+```
+
+#### Reverse the list in-place
+
+To reverse the items of a list in-place, call `my_list.reverse()` on it.
+
+```python
+>>> lottery_numbers = [1, 4, 32423, 2, 45, 11]
+>>> lottery_numbers.reverse()
+>>> lottery_numbers
+[11, 45, 2, 32423, 4, 1]
+```
+
+### Finding Methods
+
+Remember, if you ever forget which methods are available on `list`, just call `dir` on it. Ignore the methods that start with underscores. If you need help remembering what a method does, you can call `help()` on it. For example, for append, call `help(list.append)`.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/080-advanced-datatypes/20-adding-removing-finding.md b/website/content/02-introduction-to-python/080-advanced-datatypes/20-adding-removing-finding.md
new file mode 100644
index 0000000..f922e28
--- /dev/null
+++ b/website/content/02-introduction-to-python/080-advanced-datatypes/20-adding-removing-finding.md
@@ -0,0 +1,240 @@
++++
+title = "Lists, Part 2"
+date = 2019-03-03T17:22:06-08:00
+weight = 20
+draft = false
++++
+
+### Adding, Removing, Changing, and Finding Items in `list`s cheat sheet
+
+| action | method | returns | possible errors |
+|-------------------------------------------------- |--------------------------------------- |------------------- |-------------------------------------------- |
+| check length | `len(my_list)` | `int` | |
+| **add:** to the end | `my_list.append(item)` | - | |
+| **insert:** at position | `my_list.insert(pos, item)` | - | |
+| **update:** at position | `my_list[pos] = item` | - - | `IndexError` if `pos` is >= `len(my_list)` |
+| **extend:** add items from another list | `my_list.extend(other_list)` | - | |
+| is item in list? | `item in my_list` | `True` or `False` | |
+| **index** of item | `my_list.index(item)` | `int` | `ValueError` if `item` is not in `my_list` |
+| **count** of item | `my_list.count(item)` | `int` | |
+| **remove** an item | `my_list.remove(item)` | - | `ValueError` if `item` not in `my_list` |
+| **remove** the last item, or an item at an index | `my_list.pop()` or `my_list.pop(pos)` | `item` | `IndexError` if `pos` >= `len(my_list)` |
+
+
+#### Checking Length
+
+Before we add or remove items, it's usually a good idea to check a list's length. We do that with the `len` built in function. We can even use the `len` built in function to check the lengths of other types, like strings.
+
+Let's see it in action on a names `list` with two items, and a name `str`ing with four characters.
+
+```python
+>>> len(names)
+2
+>>> name = "Nina"
+>>> len(name)
+4
+```
+
+### Adding Items
+
+Let's start with a list of two names.
+
+```python
+>>> names = ["Nina", "Max"]
+```
+
+##### `my_list.append(item)` adds to the end of `my_list`
+
+We can use `my_list.append(item)` to add an additional item to the end of the list.
+
+```python
+>>> names.append("John")
+>>> names
+['Nina', 'Max', 'John']
+```
+
+##### `my_list.insert(pos, item)` inserts an item into `my_list` at the given position
+
+Use `my_list.insert(pos, item)` to insert items in an arbitrary position in the list. If the position is 0, we'll insert at the beginning of the list.
+
+```python
+>>> names.insert(0, "Rose")
+>>> names
+['Rose', 'Nina', 'Max', 'John']
+```
+
+You can call `dir()` on our names list to verify that it's actually of type `list`. If you forget which order insert is called in, don't forget you can always use the `help()` function on the REPL. **Remember: Press `q` to quit the help screen.** Let's try it now:
+
+```python
+>>> type(names)
+
+>>> help(names.insert)
+
+Help on method_descriptor:
+
+insert(self, index, object, /)
+ Insert object before index.
+```
+
+You can also call help on `names.insert`. Because `names` is already of type `list`, it achieves the same result.
+
+##### `my_list.extend(other_list)` adds all the contents of `other_list` to `my_list`
+
+```python
+>>> names = ["Nina", "Max"]
+>>> colors = ["Red", "Blue"]
+>>> names
+['Nina', 'Max']
+>>> names.extend(colors)
+>>> names
+['Nina', 'Max', 'Red', 'Blue']
+```
+
+### Looking for Items
+
+Looking for items in a list is *slow*. Each item needs to be checked in order to find a match.
+
+This doesn't matter much when you're just getting started, unless your data set is large, or if you're building high-performance systems. If you want to quickly search for an item, you'll need to use a `set` or a `dict`ionary instead.
+
+There are a few ways to determine if an item is in the list, and at which position. Let's try this on our list of names.
+
+```python
+names = ["Nina", "Max", "Phillip", "Nina"]
+```
+
+##### Use the `in` keyword to determine if an item is present or not.
+
+```python
+>>> "Nina" in names
+True
+>>> "Rose" in names
+False
+```
+
+##### Use the `my_list.index(item)` method to find the **first** index of a potential match.
+
+Notice that only the *first* index of the string `"Nina"` is returned. We'll learn more about what an index is in the next chapter.
+
+{{% notice info %}}
+If the item we're looking for *is not* in the list, Python will throw a `ValueError`.
+{{% /notice %}}
+
+You'll learn how to deal with exceptions later. For now, you can use the `in` operator to check if an item is present in the list before finding its index.
+
+```python
+>>> names.index("Nina")
+0
+>>> names.index("Rose")
+Traceback (most recent call last):
+ File "", line 1, in
+ValueError: 'Rose' is not in list
+```
+
+##### Use the `my_list.count(item)` method to find out how many times an item appears in a list.
+
+```python
+>>> names.count("Nina")
+2
+>>> names.count("Rose")
+0
+```
+
+### Updating Items
+
+To update items in a list, use the *position* of the item you'd like to change using square bracket `[]` syntax. Like: `my_list[pos] = new_item`
+
+For example:
+
+```python
+>>> names = ["Nina", "Max"]
+>>> names[0] = "Rose"
+>>> names
+['Rose', 'Max']
+```
+
+Or, when used with `my_list.index(item)`:
+
+```python
+>>> names = ["Nina", "Max"]
+>>> pos = names.index("Max")
+>>> names[pos] = "Rose"
+>>> names
+['Nina', 'Rose']
+```
+
+{{% notice info %}}
+You'll see a `IndexError: list assignment index out of range` if you try to update an item in a position that doesn't exist, that is *if the position is greater than or equal to `>=` the length of the list*.
+{{% /notice %}}
+
+```python
+>>> names = ["Nina", "Max"]
+>>> len(names)
+2
+>>> names[2] = "Rose"
+Traceback (most recent call last):
+ File "", line 1, in
+IndexError: list assignment index out of range
+```
+
+### Removing Items
+
+There are a few ways to remove items from a list.
+
+##### Use `my_list.remove(item)` to remove the *first* instance of the item
+
+Be careful. `remove()` only removes the first instance of the item from the list, which isn't always what we want to do.
+
+```python
+>>> names = ["Nina", "Max", "Rose"]
+>>> names.remove("Nina")
+>>> names
+['Max', 'Rose']
+>>>
+>>>
+>>> names = ["Nina", "Max", "Nina"]
+>>> names.remove("Nina")
+>>> names
+['Max', 'Nina']
+```
+
+{{% notice info %}}
+If we try to remove an item that's not in the list, we'll get a `ValueError: list.remove(x): x not in list`.
+{{% /notice %}}
+
+```python
+>>> names = ["Nina"]
+>>> names.remove("Max")
+Traceback (most recent call last):
+ File "", line 1, in
+ValueError: list.remove(x): x not in list
+```
+
+##### Use `my_list.pop()` to remove the last item, or `my_list.pop(index)` to remove the item at that index
+
+Using `pop()` will also **return** the item that was in that position. That's useful if we want to save the item.
+
+```python
+>>> names = ["Nina", "Max", "Rose"]
+>>> names.pop()
+'Rose'
+>>> names
+['Nina', 'Max']
+>>> names.pop(1)
+'Max'
+>>> names
+['Nina']
+```
+
+{{% notice info %}}
+If we try to pop an item from an index that is longer than or equal to the length of the list, we'll get an `IndexError: pop index out of range`.
+{{% /notice %}}
+
+```python
+>>> names = ["Nina"]
+>>> len(names)
+1
+>>> names.pop(1)
+Traceback (most recent call last):
+ File "", line 1, in
+IndexError: pop index out of range
+```
diff --git a/website/content/02-introduction-to-python/080-advanced-datatypes/30-tuples.md b/website/content/02-introduction-to-python/080-advanced-datatypes/30-tuples.md
new file mode 100644
index 0000000..405dd71
--- /dev/null
+++ b/website/content/02-introduction-to-python/080-advanced-datatypes/30-tuples.md
@@ -0,0 +1,124 @@
+---
+title: "Tuples"
+date: 2019-02-10T18:20:45-08:00
+draft: false
+weight: 30
+---
+
+Tuples are light-weight collections used to keep track of related, but different items. Tuples are **immutable**, meaning that once a tuple has been created, the items in it can't change.
+
+You might ask, why tuples when Python already has lists? Tuples are different in a few ways. While lists are generally used to store collections of similar items together, tuples, by contrast, can be used to contain a snapshot of data. They can't be continually changed, added or removed from like you could with a list.
+
+### `tuple` cheat sheet
+
+| type | `tuple` |
+|-------------------- |--------------------------------------------------------------------------------------------------------- |
+| use | Used for storing a snapshot of related items when we don't plan on modifying, adding, or removing data. |
+| creation | `()` or `tuple()` for empty tuple. `(1, )` for one item, or `(1, 2, 3)` for a tuple with items. |
+| search methods | `my_tuple.index(item)` or `item in my_tuple` |
+| search speed | Searching for an item in a large tuple is slow. Each item must be checked. |
+| common methods | Can't add or remove from tuples. |
+| order preserved? | Yes. Items can be accessed by index. |
+| mutable? | **No** |
+| in-place sortable? | **No** |
+
+### Uses
+
+A good use of a `tuple` might be for storing the information for a *row* in a spreadsheet. That data is information only. We don't necessarily care about updating or manipulating that data. We just want a read-only snapshot.
+
+
+Tuples are an interesting and powerful datatype, and one of the more unique aspects of Python. Most other programming languages have ways of representing lists and dictionaries, but only a small subset contain tuples. Use them to your advantage.
+
+### Examples
+
+#### Empty and one-item `tuple`s
+
+One important thing to note about tuples, is there's a quirk to their creation. Let's check the type of an empty `tuple` created with `()`.
+```python
+>>> a = ()
+>>> type(a)
+
+```
+
+That looks like we'd expect it to. What about if we *tried* to create a one-item `tuple` using the same syntax?
+
+```python
+>>> b = (1)
+>>> type(b)
+
+```
+
+It didn't work! `type((1))` is an `int`eger. In order to create a one-item tuple, you'll need to include a trailing comma.
+
+```python
+>>> c = (1, )
+>>> type(c)
+
+```
+
+{{% notice tip %}}
+If you're creating a one-item tuple, you **must** include a trailing comma, like this: `(1, )`
+{{% /notice %}}
+
+#### Creation
+
+Let's say we have a spreadsheet of students, and we'd like to represent each row as a tuple.
+
+```python
+>>> student = ("Marcy", 8, "History", 3.5)
+```
+
+#### Access by index
+
+We can access items in the `tuple` by index, but we **can't change them**.
+
+```python
+>>> student = ("Marcy", 8, "History", 3.5)
+>>> student[0]
+'Marcy'
+>>> student[0] = "Bob"
+Traceback (most recent call last):
+ File "", line 1, in
+TypeError: 'tuple' object does not support item assignment
+```
+
+{{% notice info %}}
+We'll see `TypeError: 'tuple' object does not support item assignment` if we try to change the items in a tuple.
+{{% /notice %}}
+
+`tuple`s also don't have an `append` or `extend` method available on them like lists do, because they can't be changed.
+
+### `tuple` unpacking.
+
+Sounds like a lot of work for not a lot of benefit, right? Not so. `tuple`s are great when you depend on your data staying unchanged. Because of this guarantee, we can use `tuples` in other types of containers like `set`s and `dict`ionaries.
+
+It's also a great way to quickly consolidate information.
+
+You can also use `tuples` for something called unpacking. Let's see it in action:
+
+```python
+>>> student = ("Marcy", 8, "History", 3.5)
+>>>
+>>> name, age, subject, grade = student
+>>> name
+'Marcy'
+>>> age
+8
+>>> subject
+'History'
+>>> grade
+3.5
+```
+
+You can return tuples from functions, and use unpacking.
+
+```python
+>>> def http_status_code():
+... return 200, "OK"
+...
+>>> code, value = http_status_code()
+>>> code
+200
+>>> value
+'OK'
+```
diff --git a/website/content/02-introduction-to-python/080-advanced-datatypes/50-sets.md b/website/content/02-introduction-to-python/080-advanced-datatypes/50-sets.md
new file mode 100644
index 0000000..2cad7ab
--- /dev/null
+++ b/website/content/02-introduction-to-python/080-advanced-datatypes/50-sets.md
@@ -0,0 +1,236 @@
+---
+title: "Sets"
+date: 2019-02-10T18:20:42-08:00
+draft: false
+weight: 50
+---
+
+Sets are a datatype that allows you to store other **immutable** types in an unsorted way. An item can only be contained in a set once. There are no duplicates allowed. The benefits of a set are: very fast membership testing along with being able to use powerful set operations, like `union`, `difference`, and `intersection`.
+
+### `set` cheat sheet
+
+| type | `set` |
+|-------------------- |------------------------------------------------------------------------------------------------------------------------------- |
+| use | Used for storing immutable data types uniquely. Easy to compare the items in `set`s. |
+| creation | `set()` for an empty set (`{}` makes an empty `dict`) and `{1, 2, 3}` for a set with items in it |
+| search methods | `item in my_set` |
+| search speed | Searching for an item in a large set is very fast. |
+| common methods | `my_set.add(item)`, `my_set.discard(item)` to remove the item if it's present, `my_set.update(other_set)` |
+| order preserved? | **No**. Items *can't* be accessed by index. |
+| mutable? | **Yes**. Can add to or remove from `set`s. |
+| in-place sortable? | **No**, because items aren't ordered. |
+### Examples
+
+#### Empty `set`s
+
+Let's create our first few sets.
+
+The first thing we might try to do is create an empty set with `{}`, but we'll come across a hurdle.
+
+```python
+>>> my_new_set = {}
+>>> type(my_new_set)
+
+>>> my_set = set()
+>>> type(my_set)
+
+```
+
+{{% notice info %}}
+You can't create an empty `set` with `{}`. That creates a `dict`. Create an empty set with `set()` instead.
+{{% /notice %}}
+
+{{% notice tip %}}
+While you're learning Python, it's useful to use `type()`, `dir()` and `help()` as often as possible.
+{{% /notice %}}
+
+#### `set`s with items
+
+Now, let's make a new set with some items in it, and test out important set concepts.
+
+#### `set`s can't contain duplicate values
+
+```python
+>>> names = {"Nina", "Max", "Nina"}
+>>> names
+{'Max', 'Nina'}
+>>> len(names)
+2
+```
+
+#### `set`s can't contain mutable types
+
+The way that `set`s allow you to quickly check if an item is contained in them or not is with an algorithm called a hash. I won't cover the details, but an algorithm is a way of representing an immutable data type with a unique numerical representation. In Python, there's a built-in `hash()` function.
+
+The `hash()` function only works on immutable data types. That means, data types where the contents can't be changed after creation.
+
+```python
+>>> hash("Nina")
+3509074130763756174
+>>> hash([])
+Traceback (most recent call last):
+ File "", line 1, in
+TypeError: unhashable type: 'list'
+```
+
+{{% notice info %}}
+You'll see a `TypeError: unhashable type: 'list'` if you try to add a mutable data type (like a `list`) to a set.
+{{% /notice %}}
+
+If you try to add a mutable data type (like a `list`) to a set, you'll see the same `TypeError`, complaining about an `unhashable type`.
+
+```python
+>>> {"Nina"}
+{'Nina'}
+>>> {[]}
+Traceback (most recent call last):
+ File "", line 1, in
+TypeError: unhashable type: 'list'
+```
+
+#### `set`s can be used to de-duplicate the items in a list
+
+Tip: *If you don't care about order*, you can quickly de-duplicate the items in a `list` by passing the `list` into the `set` constructor.
+
+```python
+>>> colors = ["Red", "Yellow", "Red", "Green", "Green", "Green"]
+>>> set(colors)
+{'Red', 'Green', 'Yellow'}
+```
+
+#### `set`s don't have an order
+
+Sets don't have an order. That means that when you print them, the items won't be displayed in the order they were entered in the list.
+
+```python
+>>> my_set = {1, "a", 2, "b", "cat"}
+>>> my_set
+{1, 2, 'cat', 'a', 'b'}
+```
+
+It also means that you *can't* access items in the `set` by position in subscript `[]` notation.
+
+```python
+>>> my_set = {"Red", "Green", "Blue"}
+>>> my_set[0]
+Traceback (most recent call last):
+ File "", line 1, in
+TypeError: 'set' object does not support indexing
+```
+
+{{% notice info %}}
+You'll see `TypeError: 'set' object does not support indexing` if you try to access the items in a `set` by index with `my_set[pos]`
+{{% /notice %}}
+
+Tip: If your set contains items of the same type, and you want to sort the items, you'll need to convert the `set` to a `list` first. Or, you can use the built-in `sorted(sequence)` method, which will do the conversion for you.
+
+```python
+>>> my_set = {"a", "b", "cat", "dog", "red"}
+>>> my_set
+{'b', 'red', 'a', 'cat', 'dog'}
+>>> sorted(my_set)
+['a', 'b', 'cat', 'dog', 'red']
+```
+
+#### adding to and removing from `set`s
+
+Since a set has no order, we can't add or remove items to it by index. We need to call the operations with the item itself.
+
+##### Add items to a set with `my_set.add(item)`.
+
+```python
+>>> colors = {"Red", "Green", "Blue"}
+>>> colors.add("Orange")
+>>> colors
+{'Orange', 'Green', 'Blue', 'Red'}
+```
+
+##### Remove items with `my_set.discard(item)`
+
+You can remove an item from a `set` if it's present with `my_set.discard(item)`. If the set doesn't contain the item, no error occurs.
+
+```python
+>>> colors = {"Red", "Green", "Blue"}
+>>> colors.discard("Green")
+>>> colors
+{'Blue', 'Red'}
+>>> colors.discard("Green")
+>>> colors
+{'Blue', 'Red'}
+```
+
+You can also remove items from a `set` with `my_set.remove(item)`, which will raise a `KeyError` if the item doesn't exist.
+
+
+##### Update a set with another sequence using `my_set.update(sequence)`
+
+You can update a `set` by passing in another sequence, meaning another `set`, `list`, or `tuple`.
+
+```python
+>>> colors = {"Red", "Green"}
+>>> numbers = {1, 3, 5}
+>>> colors.update(numbers)
+>>> colors
+{1, 3, 'Red', 5, 'Green'}
+```
+
+{{% notice info %}}
+Be careful passing in a `str`ing to `my_set.update(sequence)`. That's because a `str`ing is *also* a sequence. It's a sequence of characters.
+{{% /notice %}}
+
+```python
+>>> numbers = {1, 3, 5}
+>>> numbers.update("hello")
+>>> numbers
+{1, 3, 'h', 5, 'o', 'e', 'l'}
+```
+
+Your set will update with each character of the `str`ing, which was probably not your intended result.
+
+### `set` operations
+
+`sets` allow quick and easy operations to compare items between two sets.
+
+#### `set` operations cheat sheet
+
+ method operation | symbol operation | result |
+|--------------------- |------------------ |------------------------------------------------------------------------------- |
+| `s.union(t)` | s | t | creates a new set with all the items **from both `s` and `t`** |
+| `s.intersection(t)` | `s & t` | creates a new set containing *only* items that are **both in `s` and in `t`** |
+| `s.difference(t)` | `s ^ t` | creates a new set containing items that are **not in both `s` and in `t`** |
+
+#### examples
+
+Let's see it in action.
+
+We have two sets, `rainbow_colors`, which contain the colors of the rainbow, and `favorite_colors`, which contain my favorite colors.
+
+```python
+>>> rainbow_colors = {"Red", "Orange", "Yellow", "Green", "Blue", "Violet"}
+>>> favorite_colors = {"Blue", "Pink", "Black"}
+```
+
+First, let's combine the sets and create a new `set` that contains all of the items from `rainbow_colors` and `favorite_colors` using the union operation. You can use the `my_set.union(other_set)` method, or you can just use the symbol for union `|=` from the table above.
+
+```python
+>>> rainbow_colors | favorite_colors
+{'Orange', 'Red', 'Yellow', 'Green', 'Violet', 'Blue', 'Black', 'Pink'}
+```
+
+Next, let's find the intersection. We'll create a new `set` with *only* the items in both `set`s.
+
+```python
+>>> rainbow_colors & favorite_colors
+{'Blue'}
+```
+
+Lastly, We can also find the difference. Create a new set with the items that are in in one, but not the other. We'll see that `"Blue"` is missing from the list.
+
+```python
+>>> rainbow_colors ^ favorite_colors
+{'Orange', 'Red', 'Yellow', 'Green', 'Violet', 'Black', 'Pink'}
+```
+
+There are other useful operations available on `set`s, such as checking if one set is a subset, a superset, and more, but I don't have time to cover them all. Python also has a `frozenset` type, if you need the functionality of a `set` in an immutable package (meaning that the contents can't be changed after creation).
+
+Find out more by reading the [documentation](https://docs.python.org/3/library/stdtypes.html#set), or calling `help()` on `set`.
diff --git a/website/content/02-introduction-to-python/080-advanced-datatypes/60-dictionaries.md b/website/content/02-introduction-to-python/080-advanced-datatypes/60-dictionaries.md
new file mode 100644
index 0000000..d216da3
--- /dev/null
+++ b/website/content/02-introduction-to-python/080-advanced-datatypes/60-dictionaries.md
@@ -0,0 +1,202 @@
+---
+title: "Dictionaries"
+date: 2019-02-10T18:20:37-08:00
+draft: false
+weight: 60
+---
+
+Dictionaries are a useful type that allow us to store our data in key, value pairs. Dictionaries themselves are **mutable**, *but*, dictionary keys can only be **immutable** types.
+
+We use dictionaries when we want to be able to quickly access additional data associated with a particular key. A great practical application for dictionaries is memoization. Let's say you want to save computing power, and store the result for a function called with particular arguments. The arguments could be the key, with the result stored as the value. Next time someone calls your function, you can check your dictionary to see if the answer is pre-computed.
+
+Looking for a key in a large dictionary is extremely fast. Unlike lists, we don't have to check every item for a match.
+
+### `dict`ionary cheat sheet
+
+| type | `dict` |
+|-------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------- |
+| use | Use for storing data in key, value pairs. Keys used must be **immutable** data types. |
+| creation | `{}` or `dict()` for an empty `dict`. `{1: "one", 2: "two"}` for a `dict` with items. |
+| search methods | `key in my_dict` |
+| search speed | Searching for a key in a large dictionary is fast. |
+| common methods | `my_dict[key]` to get the value by `key`, and throw a `KeyError` if `key` is not in the dictionary. Use `my_dict.get(key)` to fail silently if `key` is not in `my_dict`. `my_dict.items()` for all key, value pairs, `my_dict.keys()` for all keys, and `my_dict.values()` for all values. |
+| order preserved? | **Sort of**. As of Python 3.6 a `dict` is sorted by insertion order. Items *can't* be accessed by index, only by key. |
+| mutable? | **Yes**. Can add or remove keys from `dict`s. |
+| in-place sortable? | **No**. `dict`s don't have an index, only keys. |
+
+### Examples
+
+#### Empty `dict`s
+
+We already learned one of the methods of creating an empty `dict` when we tried (and failed) to create an empty set with `{}`. The other way is to use the `dict()` method.
+
+```python
+>>> my_dict = {}
+>>> type(my_dict)
+
+
+>>> my_dict = dict()
+>>> type(my_dict)
+
+```
+
+#### Creating `dict`s with items
+
+If we want to create `dict`s with items in them, we need to pass in key, value pairs. A `dict` is declared with curly braces `{}`, followed by a key and a value, separated with a colon `:`. Multiple key and value pairs are separated with commas `,`.
+
+
+We can call familiar methods on our dictionary, like finding out how many key / value pairs it contains with the built-in `len(my_dict)` method.
+
+```python
+>>> nums = {1: "one", 2: "two", 3: "three"}
+
+>>> len(nums)
+3
+```
+
+#### Side note: What can be used as keys?
+
+Any type of object, mutable or immutable, can be used as a value but just like `set`s, `dict`ionaries can only use immutable types as keys. That means you can use `int`, `str`, or even `tuple` as a key, but **not** a `set`, `list`, or other `dict`ionary.
+
+The follow is OK:
+
+```python
+>>> my_dict = {1: 1}
+>>> my_dict = {1: []}
+```
+
+{{% notice info %}}
+You'll see a `TypeError: unhashable type: 'list'` if you try to use a mutable type, like a `list` as a `dict`ionary key.
+{{% /notice %}}
+
+```python
+>>> my_dict = {[]: 1}
+Traceback (most recent call last):
+ File "", line 1, in
+TypeError: unhashable type: 'list'
+```
+
+#### Accessing
+
+Our `dict` contains `key`, `value` pairs. Because a `dict`ionary isn't ordered, we *can't access the items in it by position*. Instead, to access the items in it, we use square-bracket `my_dict[key]` notation, similar to how we access items in a list with square bracket notation containing the position.
+
+```python
+>>> nums = {1: "one", 2: "two", 3: "three"}
+>>> nums[1]
+'one'
+>>> nums[2]
+'two'
+```
+
+Q: What happens when we try to access a key in a `dict`ionary with square bracket notation, but the key isn't present?
+
+{{% notice info %}}
+We'll get a `KeyError: key` if we try to access `my_dict[key]` with square bracket notation, but `key` isn't in the dictionary.
+{{% /notice %}}
+
+```python
+>>> nums = {1: "one", 2: "two", 3: "three"}
+>>> nums[4]
+
+Traceback (most recent call last):
+ File "", line 1, in
+KeyError: 4
+```
+
+One way to get around this is to use the `my_dict.get(key)` method. Using this method, if the key isn't present, no error is thrown, and no value (aka the `None` type) is returned.
+
+```python
+>>> nums = {1: "one", 2: "two", 3: "three"}
+>>> nums.get(4)
+
+>>> result = nums.get(4)
+>>> type(result)
+
+```
+
+If we want to provide a *default value* if the key is missing, we also pass an *optional* argument to the `my_dict.get(key)` method like so: `my_dict.get(key, default_val)`
+
+```python
+>>> nums = {1: "one", 2: "two", 3: "three"}
+>>> nums.get(4, "default")
+'default'
+```
+
+#### Adding, Removing
+
+To add a new key value pair to the dictionary, you'll use square-bracket notation.
+
+If you try to put a key into a dictionary that's already there, you'll just end up replacing it. To avoid subtle bugs, you can check if a particular key is in a dictionary with the `in` keyword. We'll cover that technique in Chapter 6 - Control Statements and Looping.
+
+```python
+>>> nums = {1: "one", 2: "two", 3: "three"}
+>>> nums[8] = "eight"
+
+>>> nums
+{1: 'one', 2: 'two', 3: 'three', 8: 'eight'}
+
+>>> nums[8] = "oops, overwritten"
+>>> nums
+{1: 'one', 2: 'two', 3: 'three', 8: 'oops, overwritten'}
+>>> 8 in nums
+True
+```
+
+#### Updating
+
+Just like with `list`s an `set`s, you can update the items in a dictionary with the items from another dictionary.
+
+```python
+>>> colors = {"r": "Red", "g": "Green"}
+>>> numbers = {1: "one", 2: "two"}
+>>> colors.update(numbers)
+>>> colors
+{'r': 'Red', 'g': 'Green', 1: 'one', 2: 'two'}
+```
+
+### Complex Dictionaries
+
+One incredibly useful scenario for dictionaries is storing the values in a `list` or other sequence. Going into too much detail is outside of the scope of the class, but I'll show you a quick example:
+
+```python
+>>> colors = {"Green": ["Spinach"]}
+>>> colors
+{'Green': ['Spinach']}
+>>> colors["Green"].append("Apples")
+>>> colors
+{'Green': ['Spinach', 'Apples']}
+```
+
+### Working with `items`, `keys`, and `values`
+
+There are three useful methods you need to remember about `dict`ionary access:
+
+1. `my_dict.keys()`
+2. `my_dict.values()`
+3. `my_dict.items()`
+
+#### 1. `my_dict.keys()` Getting all the keys in a dictionary
+
+```python
+>>> nums = {1: 'one', 2: 'two', 3: 'three', 8: 'eight'}
+>>> nums.keys()
+dict_keys([1, 2, 3, 8])
+```
+
+#### 2. `my_dict.values()` Getting all the values in a dictionary.
+
+```python
+>>> nums = {1: 'one', 2: 'two', 3: 'three', 8: 'eight'}
+>>> nums.values()
+dict_values(['one', 'two', 'three', 'eight'])
+```
+
+#### 3. `my_dict.items()` Getting all the items (key, value pairs) in a dictionary
+
+Notice that `my_dict.items()` returns a type that looks like a list. It contains two-item `tuple`s containing the key, value pairs.
+
+```python
+>>> nums = {1: 'one', 2: 'two', 3: 'three', 8: 'eight'}
+>>> nums.items()
+dict_items([(1, 'one'), (2, 'two'), (3, 'three'), (8, 'eight')])
+```
diff --git a/website/content/02-introduction-to-python/080-advanced-datatypes/65-mutability.md b/website/content/02-introduction-to-python/080-advanced-datatypes/65-mutability.md
new file mode 100644
index 0000000..c54e793
--- /dev/null
+++ b/website/content/02-introduction-to-python/080-advanced-datatypes/65-mutability.md
@@ -0,0 +1,31 @@
+---
+title: "Mutability Cheat Sheet"
+date: 2019-03-03T13:06:52-08:00
+draft: false
+weight: 65
+---
+
+### Mutability
+
+Mutability, simply put: the contents of a mutable object can be changed, while the contents of an immutable object cannot be.
+
+### Simple Types
+
+All of the simple data types we covered first are **immutable**
+
+| type | use | mutable? |
+|--------------------------- |------------------------- |---------- |
+| `int`, `float`, `decimal` | store numbers | **no** |
+| `str` | store strings | **no** |
+| `bool` | store `True` or `False` | **no** |
+
+### Container Types
+
+For the mutability of the container types we covered in this chapter, check this helpful list:
+
+| container type | use | mutable? |
+|---------------- |--------------------------------------------------------------------------------------------------------- |---------- |
+| `list` | ordered group of items, accessible by position | **yes** |
+| `set` | mutable unordered group consisting only of immutable items. useful for set operations (membership, intersection, difference, etc) | **yes** |
+| `tuple` | contain ordered groups of items in an **immutable** collection | **no** |
+| `dict` | contains key value pairs | **yes** |
diff --git a/website/content/02-introduction-to-python/080-advanced-datatypes/70-exercise.md b/website/content/02-introduction-to-python/080-advanced-datatypes/70-exercise.md
new file mode 100644
index 0000000..5b3a7bc
--- /dev/null
+++ b/website/content/02-introduction-to-python/080-advanced-datatypes/70-exercise.md
@@ -0,0 +1,358 @@
+---
+title: "Practice"
+date: 2019-02-04T15:28:32-08:00
+draft: false
+weight: 70
+pre: "⭐️ "
+---
+
+## Lists, Dictionaries, Tuples, and Sets
+
+### Lists
+
+Lists are great for storing an ordered sequence of objects. Remember that you can see the current state of your list at any time by typing the name of your list by itself. Check your list after every operation to see if it has changed.
+
+```python
+>>> my_list = ["h", "e", "l", "l", "o"]
+# Let's look at our list:
+>>> my_list
+# Let's add to my_list:
+>>> my_list.append("!")
+# Now let's see it again:
+>>> my_list
+```
+
+Let's play with slices. How do we get the last two elements of our list?
+
+```python
+# We know the number of items in our list is 6...
+>>> len(my_list)
+6
+# So the last two indexes are 4 and 5. Since the first number in the slice is inclusive, and the second number is exclusive, we can ask for everything between index 4 and 6
+>>> my_list[4:6]
+# We can also say "Give me everything after index 4
+>>> my_list[4:]
+# Or, we can ask for just the last two items without caring how big the list is. This means "give me everything starting from two before the end":
+>>> my_list[-2:]
+```
+
+There are many other ways to interact with our lists as well:
+
+```python
+# Remove the first L:
+>>> my_list.remove("l")
+# Let's put it back at index 2
+>>> my_list.insert(2, "l")
+
+# Delete any element
+>>> del my_list[0]
+# Remove and return the last element. Useful for queues!
+>>> last_item = my_list.pop()
+>>> last_item
+
+# We can also look at individual items my using an index:
+>>> my_list[2]
+# Or we can see if a certain value exists in the list:
+>>> "!" in my_list
+# Let's sort our list in reverse order
+>>> my_list.sort(reverse=True)
+>>> my_list
+# Note that sort() doesn't return anything, it sorts the list in-place
+# You can also use the sorted() function to return a new, sorted list without modifying the old one
+>>> sorted(my_list, reverse=False)
+>>> my_list
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> my_list = ["h", "e", "l", "l", "o"]
+>>> my_list
+['h', 'e', 'l', 'l', 'o']
+>>> my_list.append("!")
+>>> my_list
+['h', 'e', 'l', 'l', 'o', '!']
+
+>>> len(my_list)
+6
+>>> my_list[4:6]
+['o', '!']
+>>> my_list[4:]
+['o', '!']
+>>> my_list[-2:]
+['o', '!']
+
+>>> my_list.remove("l")
+>>> my_list.insert(2, "l")
+>>> del my_list[0]
+>>> last_item = my_list.pop()
+>>> last_item
+'o'
+>>> my_list[2]
+'l'
+>>> "!" in my_list
+False
+>>> my_list.sort(reverse=True)
+>>> my_list
+['o', 'l', 'l', 'h', 'e', '!']
+>>> sorted(my_list, reverse=False)
+['!', 'e', 'h', 'l', 'l', 'o']
+```
+
+{{% /expand%}}
+
+### Sets
+
+
+Sets are a great data type for storing unique data - you can only have one of any given object in a set. Sets are unordered, thus you can't access them with `[]` indexing syntax, but they do have some handy functions.
+
+Let's play with some set operations:
+
+```python
+# Create an empty set
+>>> my_set = {}
+>>> type(my_set)
+# Gotcha: using {} actually creates an empty dictionary. To create an empty set, use set()
+>>> my_set = set()
+>>> my_set
+
+# Let's create a non-empty set
+>>> my_set = {1, 2, 3}
+# We can add and remove items from the set
+>>> my_set.add(4)
+>>> my_set.remove(2)
+# We can test if an item exists in the set
+>>> 2 in my_set
+
+# Unlike lists, every item in a set must be unique
+>>> my_set
+>>> my_set.add(3)
+>>> my_set
+# There is still only one 3 in the set
+
+>>> my_set
+# my_set should equal {1, 3, 4}
+>>> my_other_set = {1, 2, 3}
+# We can combine two sets
+>>> my_set.union(my_other_set)
+# We can get the intersection of two sets
+>>> my_set.intersection(my_other_set)
+# We can get the difference of two sets
+>>> my_set.difference(my_other_set)
+
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> my_set = {}
+>>> type(my_set)
+
+>>> my_set = set()
+>>> type(my_set)
+
+
+>>> my_set = {1, 2, 3}
+>>> my_set.add(4)
+>>> my_set.remove(2)
+>>> 2 in my_set
+False
+
+>>> my_set
+{1, 3, 4}
+>>> my_set.add(3)
+>>> my_set
+{1, 3, 4}
+
+>>> my_other_set = {1, 2, 3}
+>>> my_set.union(my_other_set)
+{1, 2, 3, 4}
+>>> my_set.intersection(my_other_set)
+{1, 3}
+>>> my_set.difference(my_other_set)
+{4}
+```
+
+{{% /expand%}}
+
+
+### Tuples
+
+Tuples are a lightweight way to hold information that describes something, like a person - their name, age, and hometown. You can think about it kind of like a row in a spreadsheet. Tuples are represented inside parentheses, however parentheses are not required to create a tuple, just a sequence of objects followed by commas.
+
+```python
+>>> my_tuple = 1,
+>>> my_tuple
+# Let's add to our tuple
+>>> my_tuple[1] = 2
+```
+
+Oops! Remember that tuples are immutable, so you can't change them once they've been created. Tuples are great for moving data around in a lightweight way, because you can unpack them easily into multiple variables.
+
+```python
+>>> person = ('Jim', 29, 'Austin, TX')
+>>> name, age, hometown = person
+>>> name
+>>> age
+>>> hometown
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> my_tuple = 1,
+>>> my_tuple
+(1,)
+>>> my_tuple[1] = 2
+Traceback (most recent call last):
+ File "", line 1, in
+TypeError: 'tuple' object does not support item assignment
+```
+
+```python
+>>> person = ('Jim', 29, 'Austin, TX')
+>>> name, age, hometown = person
+>>> name
+'Jim'
+>>> age
+29
+>>> hometown
+'Austin, TX'
+```
+
+{{% /expand%}}
+
+
+### Dictionaries
+
+Dictionaries are great for storing data that you can index with keys. The keys must be unique, and the dictionaries *are* stored in the order you inserted items, however this is only guaranteed as of Python 3.7.
+
+```python
+>>> my_dict = {"key": "value"}
+# Remember, dictionaries don't have numerical indexes like lists, so if you try to use an index number...
+# Unless 0 happens to be a key.
+>>> my_dict[0]
+# You'll get a KeyError!
+
+# Let's put some more things into our dictionary
+>>> my_dict["hello"] = "world"
+>>> my_dict["foo"] = "bar"
+>>> my_dict
+
+# What was the value for "hello" again?
+>>> my_dict["hello"]
+# You can also use get() to get a key
+>>> my_dict.get("hello")
+# What if the key you want doesn't exist?
+>>> my_dict["baz"]
+# If you're not sure if a key exists, you can ask:
+>>> "baz" in my_dict
+# Or you can use a default value. If "baz" doesn't exist, return "boo":
+>>> my_dict.get("baz", "boo")
+
+# Let's try separating the dictionary into lists of keys and values:
+>>> my_dict.keys()
+>>> my_dict.values()
+
+# What if we want to iterate over a dictionary's items? We can use the items() function to get a list of tuples:
+>>> my_dict.items()
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> my_dict = {"key": "value"}
+>>> my_dict[0]
+Traceback (most recent call last):
+ File "", line 1, in
+KeyError: 0
+
+>>> my_dict["hello"] = "world"
+>>> my_dict["foo"] = "bar"
+>>> my_dict
+{'foo': 'bar', 'hello': 'world', 'key': 'value'}
+
+>>> my_dict["hello"]
+'world'
+>>> my_dict.get("hello")
+'world'
+>>> my_dict["baz"]
+Traceback (most recent call last):
+ File "", line 1, in
+KeyError: 'baz'
+>>> "baz" in my_dict
+False
+>>> my_dict.get("baz", "default response")
+'default response'
+
+>>> my_dict.keys()
+['foo', 'hello', 'key']
+>>> my_dict.values()
+['bar', 'world', 'value']
+
+>>> my_dict.items()
+[('foo', 'bar'), ('hello', 'world'), ('key', 'value')]
+```
+
+{{% /expand%}}
+
+### Mutability
+
+Remember, in Python, some data types are **immutable** -- that means that once they're created, their contents can't be changed. Tuples are immutable - once you make one, you can't alter it, you can only make a new one. Conversely, lists, dictionaries, and sets are mutable - you can change them without making new ones.
+
+Let's see this in practice:
+
+```python
+# Lists are mutable
+>>> my_list = [1, 2, 3]
+>>> my_list[0] = 'a'
+>>> my_list
+
+# Dictionaries are also mutable
+>>> my_dict = {"hello": "world"}
+>>> my_dict["foo"] = "bar"
+>>> my_dict
+
+# Sets are mutable, but don't support indexing or item assignment, so you have to use add() and remove()
+>>> my_set = {1, 2, 3}
+>>> my_set[0] = 'a' # This will throw a TypeError
+>>> my_set.add('a')
+>>> my_set
+
+# Tuples are immutable
+>>> my_tuple = (1, 2, 3)
+>>> my_tuple[0] = 'a' # This will throw a TypeError
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> my_list = [1, 2, 3]
+>>> my_list[0] = 'a'
+>>> my_list
+['a', 2, 3]
+
+>>> my_dict = {"hello": "world"}
+>>> my_dict["foo"] = "bar"
+>>> my_dict
+{'hello': 'world', 'foo': 'bar'}
+
+>>> my_set = {1, 2, 3}
+>>> my_set[0] = 'a'
+Traceback (most recent call last):
+ File "", line 1, in
+TypeError: 'set' object does not support item assignment
+>>> my_set.add('a')
+>>> my_set
+{1, 2, 3, 'a'}
+
+>>> my_tuple = (1, 2, 3)
+>>> my_tuple[0] = 'a'
+Traceback (most recent call last):
+ File "", line 1, in
+TypeError: 'tuple' object does not support item assignment
+```
+
+{{% /expand%}}
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/080-advanced-datatypes/_index.md b/website/content/02-introduction-to-python/080-advanced-datatypes/_index.md
new file mode 100644
index 0000000..b0eb5c0
--- /dev/null
+++ b/website/content/02-introduction-to-python/080-advanced-datatypes/_index.md
@@ -0,0 +1,14 @@
++++
+title = "Advanced Container Types"
+date = 2019-01-25T15:04:34-06:00
+weight = 80
+chapter = true
+draft = false
+pre = "4. "
++++
+
+### Chapter 4
+
+# Advanced Data Types - Containers and sequences
+
+Now that we've got the basics of strings and numbers down, let's talk about the advanced data types - `list`, `tuple`, `dict` and `set`. These are container objects that let us organize other types of objects into one data structure.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/090-boolean-logic/10-truthiness.md b/website/content/02-introduction-to-python/090-boolean-logic/10-truthiness.md
new file mode 100644
index 0000000..f7ecf7f
--- /dev/null
+++ b/website/content/02-introduction-to-python/090-boolean-logic/10-truthiness.md
@@ -0,0 +1,108 @@
+---
+title: "Truthiness"
+date: 2019-02-10T18:17:39-08:00
+draft: false
+weight: 10
+---
+
+Evaluating expression to be `True` or `False` will help us control the flow of our program.
+
+### cheat sheet
+
+| type | truthiness | |
+|--------------------------------------------- |-------------------------------------------------------------------------------- |--- |
+| `int` | `0` is `False`, all other numbers are `True` (including negative) | |
+| containers - `list`, `tuple`, `set`, `dict` | empty container evaluates to `False`, container with items evaluates to `True`) | |
+| `None` | `False` | |
+
+We talked about `boolean` types, `True` and `False` earlier. `True` and `False` are keywords in Python, so make sure you don't name your variables the same thing.
+
+```python
+>>> True
+True
+>>> False
+False
+```
+
+Sometimes the truth is obvious. For example `3 < 5` is always `True`. Other times, in Python, the truth value might surprise you. Let's review. First, let's start with an expression we know is always `True`.
+
+```python
+>>> 3 < 5
+True
+```
+
+{{% notice tip %}}
+Tip: If you want to test your assumptions about an expression that returns `True` or `False`, you can pass it into the constructor for `bool`eans: `bool(expression)`.
+{{% /notice %}}
+
+### Numbers
+
+In Python, the integer `0` is always `False`, while every other number, *including negative numbers*, are `True`. In fact, under the hood, `bool`eans inherit from `int`egers.
+
+```python
+>>> bool(0)
+False
+>>> bool(1)
+True
+>>> bool(-1)
+True
+```
+
+### Sequences
+
+Empty sequences in Python always evaluate to `False`, **including empty `str`ings.**
+
+```python
+>>> bool("") # String
+False
+>>> bool([]) # Empty List
+False
+>>> bool(set()) # Empty Set
+False
+>>> bool({}) # Empty Dictionary
+False
+>>> bool(()) # Empty Tuple
+False
+```
+
+Sequences with at least one value will evaluate to `True`.
+
+```python
+>>> bool("Hello") # String
+True
+>>> bool([1]) # List
+True
+>>> bool({1}) # Set
+True
+>>> bool({1: 1}) # Dictionary
+True
+>>> bool((1,)) # Tuple
+True
+```
+
+### `None`
+
+The `None` type in Python represents nothing. No returned value. It shouldn't come as a surprise that the truthiness of `None` is `False`.
+
+```python
+>>> bool(None)
+False
+```
+
+`None` is commonly used as a placeholder to mean *"I haven't set this value yet."* Since empty `str`ings and sequence evaluate to `False`, we need to be very careful when we're checking if a sequence has been *declared* or not, or if it's *empty*. We'll review this concept again when talking about `if` statements later in the day.
+
+```python
+>>> my_name = None
+>>> bool(my_name)
+False
+>>> my_name = ""
+>>> bool(my_name)
+False
+
+>>> my_list = None
+>>> bool(my_list)
+False
+>>> my_list = []
+>>> bool(my_list)
+False
+```
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/090-boolean-logic/20-comparisons.md b/website/content/02-introduction-to-python/090-boolean-logic/20-comparisons.md
new file mode 100644
index 0000000..83ff9a0
--- /dev/null
+++ b/website/content/02-introduction-to-python/090-boolean-logic/20-comparisons.md
@@ -0,0 +1,128 @@
+---
+title: "Comparisons"
+date: 2019-02-10T18:18:14-08:00
+draft: false
+weight: 20
+---
+
+How can we compare different values with each other?
+
+### Order Comparisons Cheat Sheet
+
+|Operator|Means|
+|---|---|
+|`<`|less-than|
+|`<=`|less-than-or-equal-to|
+|`>`|greater-than|
+|`>=`|greater-than-or-equal-to|
+
+In Python, comparing numbers is pretty straight forward.
+
+```python
+>>> 1 < 10 # 1 is less than 10? True
+True
+>>> 20 <= 20 # 20 is less than or equal to 20? True
+True
+>>> 10 > 1 # 10 is greater than 1? True
+True
+>>> -1 > 1 # -1 is greater than 1? False
+False
+>>> 30 >= 30 # 30 is greater than or equal to 30? True
+True
+```
+
+Things get interesting when you try to compare strings. Strings are compared lexicographically. That means by the ASCII value of the character. You don't need to know much about ASCII, besides that capital letters come before lower case ones.
+
+Each character in the two strings is checked one by one, until a character is found that is of a different value. That determines the order. Under the hood, this allows Python to sort strings by comparing them to each other.
+
+```python
+>>> "T" < "t" # Upper case letters are "lower" valued.
+True
+>>> "a" < "b"
+True
+>>> "bat" < "cat"
+True
+```
+
+### Equality Cheat Sheet
+
+|Operator|Means|
+|---|---|
+|`==`|equals|
+|`!=`|not-equals|
+
+The equality operators `val1 == val2` *(`val1` equals `val2`)* and `val1 != val2` *(`val1` doesn't equal `val2`)* compare the contents of two different values and return a `bool`ean.
+
+Equality works like you'd expect it to for simple data types.
+
+```python
+>>> a = 1
+>>> b = 1
+>>> a == b
+True
+>>> a != b
+False
+
+>>> a = "Nina"
+>>> b = "Nina"
+>>> a == b
+True
+>>> a != b
+False
+```
+
+Equality for container types is interesting. Even though `a` and `b` are two different `list`s, their contents are still the same. So compared two lists containing the same values with `==` will return `True`.
+
+```python
+>>> a = [1, 2, 3]
+>>> b = [1, 2, 3]
+>>> a == b
+True
+>>> a != b
+False
+```
+
+### Identity Cheat Sheet
+
+|Operator|Means|
+|---|---|
+|`is`| *is* the same object in memory? (not equality!)|
+|`is not`| *is not* the same object in memory? (not equality!)|
+
+{{% notice note %}}
+This is something that trips up Python beginners, so make sure you remember that *equality* (`==`, `!=`) **is not** the same as *identity* (`is`, `not is`).
+{{% /notice %}}
+
+The `is` keywords tests if the two compared objects are stored in the same memory location. I won't go into too much detail into why, but remember **not** to use `is` when what you actually want to check for is equality.
+
+```python
+>>> a = [1, 2, 3]
+>>> b = [1, 2, 3]
+
+>>> a == b # Testing for equality. a and b contain the same values
+True
+>>> a is b # Testing for identity. a and b are NOT the same object.
+False
+```
+
+{{% notice tip %}}
+When you're first starting out, the only place you'll want to use the `is` keyword is to explicitly compare a value to the built-in types of `None`, `True`, or `False`.
+{{% /notice %}}
+
+```python
+>>> a = True
+>>> a is True
+True
+
+>>> b = False
+>>> b is False
+True
+>>> b is not True # Opposite of is b True. aka is b False?
+True
+
+>>> c = None
+>>> c is None
+True
+>>> c is not None
+False
+```
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/090-boolean-logic/30-and-or-not.md b/website/content/02-introduction-to-python/090-boolean-logic/30-and-or-not.md
new file mode 100644
index 0000000..e534d23
--- /dev/null
+++ b/website/content/02-introduction-to-python/090-boolean-logic/30-and-or-not.md
@@ -0,0 +1,191 @@
+---
+title: "and, or, not"
+date: 2019-02-10T18:17:45-08:00
+draft: false
+weight: 30
+---
+
+`and`, `or`, and `not` are the three basic types of boolean operators that are present in math, programming, and database logic.
+
+In other programming languages, you might have seen the concept of `and` represented with `&&`, `or`, represented with `||`, and `not` represented by `!`. The Python language is instead focused on readability. So we'll use the english `and` instead of trying to remember fancy symbols. Python still uses the `&`, `|` and `!` expressions, but they're used for bitwise operations.
+
+You can use them to compare one (or more expressions) and determine if they evaluate to `True` or `False`.
+
+Thankfully, you don't have to be a computer scientist to understand them if you use this handy table.
+
+### `and`, `or`, `not` Cheat Sheet
+
+|Operation|Result|
+|---|---|
+|`a or b`|if a is False, then b, else a|
+|`a and b`|if a is False, then a, else b|
+|`not a`|if a is false, then `True`, else `False`|
+
+
+### `and`
+
+
+
+{{% notice note %}}
+For `a and b`, if a is false, a is returned. Otherwise b is returned.
+*If `a and b` are both `bool`ean values, the expression evaluates to`True` if both a and b are `True`.*
+{{% /notice %}}
+
+```python
+>>> a = True # a is True
+>>> b = True
+>>> a and b # True is returned. (value of b)
+True
+
+>>> a = False # a is False
+>>> b = True
+>>> a and b # False is returned. (value of a)
+False
+
+>>> a = False # a is False
+>>> b = False
+>>> a and b # False is returned. (value of a)
+False
+```
+
+Notice what happens when do the same thing to values that have a "truthiness" to them.
+
+```python
+>>> bool(0) # Verify that zero is "falsey"
+False
+>>> bool(1) # Verify that one is "truthy"
+True
+>>> 0 and 1 # 0 is False. 0 is returned.
+0
+```
+
+### `or`
+
+
+
+{{% notice note %}}
+For `a or b`, if a is false, b is returned. If a is true, a is returned.
+*`a or b` evaluates to `True` if either (or both) of the expressions are true.*
+{{% /notice %}}
+
+```python
+>>> a = True # a is true
+>>> b = True
+>>> a or b # True is returned (value of a)
+True
+
+>>> a = False # a is false
+>>> b = True
+>>> a or b # True is returned (value of b)
+True
+
+>>> 0 or 1 # 0 is false. Return 1.
+1
+```
+
+### `not`
+
+| a | `not` a |
+|--------- |------------ |
+| true | False |
+| false | **`True`** |
+
+
+{{% notice note %}}
+`not a` reverses the `bool`ean value of `a`. If it *was* true, it will return `False`. If it was false, it will return `True`.
+{{% /notice %}}
+
+```python
+>>> a = True
+>>> not a # not returns the opposite. True -> False
+False
+
+>>> a = False
+>>> not a # not returns the opposite. False -> True
+True
+```
+
+And again, with numbers. Remember, zero is considered `False`, any other number is considered `True`.
+
+```python
+>>> bool(1)
+True
+>>> not 1
+False
+>>> bool(0)
+False
+>>> not 0
+True
+```
+
+### In Combination
+
+When combining multiple boolean operators, you can add optional parenthesis for readability.
+
+```python
+>>> a = True
+>>> b = True
+>>> c = False
+
+>>> a and (b or c)
+True
+```
+
+You can combine multiple operators to test complex assumptions. For example, to return `True` only if *both* values are `False`, we can use the `not` negation operation on the result of an `or`.
+
+```python
+>>> a = False
+>>> b = False
+
+>>> a or b # False because both are False.
+False
+
+>>> not (a or b) # True - checking if both are False.
+True
+```
+
+### With "truthiness"
+
+Remember, we learned that some values in Python are *falsey* like the number zero, and some are *truthy* like any number *expect* for zero.
+
+It's a little counter intuitive, but when we compare values other than `bool`eans, our code behaves a little differently.
+
+|Operation|Result|
+|---|---|
+|`x or y`|if x is false, then y, else x|
+|`x and y`|if x is false, then x, else y|
+
+Let's see it in action. First, lets test our assumptions again.
+
+```python
+>>> bool(0) # Truthiness of 0 is False
+False
+
+>>> bool(1) # Truthiness of 1 is True
+True
+
+>>> bool(None) # Truthiness of None type is False
+False
+
+>>> 1 or 0 # Returns 1, the True value
+1
+
+>>> 1 and 0 # Returns 0, the False value
+0
+
+>>> 0 or None # Neither are True. Returns nothing (None)
+```
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/090-boolean-logic/40-exercise.md b/website/content/02-introduction-to-python/090-boolean-logic/40-exercise.md
new file mode 100644
index 0000000..4cc1c25
--- /dev/null
+++ b/website/content/02-introduction-to-python/090-boolean-logic/40-exercise.md
@@ -0,0 +1,182 @@
+---
+title: "Practice"
+date: 2019-03-03T00:00:00-08:00
+draft: false
+weight: 40
+pre: "⭐️ "
+---
+
+## Boolean Logic
+
+### Comparisons
+
+Let's practice using our comparison operators. Remember:
+
+|Operator|Means|
+|---|---|
+|`<`|less-than|
+|`<=`|less-than-or-equal-to|
+|`>`|greater-than|
+|`>=`|greater-than-or-equal-to|
+|`==`|equals|
+|`!=`|not-equals|
+
+Remember, the first six operators test the object's *value*. `is` and `is not` test whether two objects are the same thing. This is useful for singletons, such as `None` or `False`. We won't be using them much in this intro course, but feel free to play with them.
+
+```python
+>>> 10 > 5
+>>> 5 > 10
+>>> 10 > 10
+>>> 10 >= 10
+>>> 5 < 10
+>>> 5 < 5
+>>> 5 <= 5
+>>> 5 == 5
+>>> 5 != 10
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> 10 > 5
+True
+>>> 5 > 10
+False
+>>> 10 > 10
+False
+>>> 10 >= 10
+True
+>>> 5 < 10
+True
+>>> 5 < 5
+False
+>>> 5 <= 5
+True
+>>> 5 == 5
+True
+>>> 5 != 10
+True
+```
+
+{{% /expand %}}
+
+### Truthiness
+
+Different languages have different ideas of what is "truthy" and "falsy." In Python, all objects can be tested for truth, and an object is considered True unless except under certain circumstances that we talked about earlier in the chapter. Remember that checking if an object is "equal" to another object doesn't necessarily mean the same thing. An object is considered "truthy" if it satisfies the check performed by `if` or `while` statements.
+
+Let's try a few of these out:
+
+```python
+>>> 5 == True
+>>> # The number 5 does not equal True, but...
+>>> if 5:
+... print("The number 5 is truthy!")
+...
+>>> # The number 5 is truthy for an if test!
+```
+
+True and False can also be represented by 1 and 0
+```python
+>>> 1 == True
+>>> 0 == False
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> 5 == True
+False
+>>> # The number 5 does not equal True, but...
+>>> if 5:
+... print("The number 5 is truthy!")
+...
+The number 5 is truthy!
+>>> # The number 5 is truthy for an if test!
+```
+
+```python
+>>> 1 == True
+True
+>>> 0 == False
+True
+```
+{{% /expand %}}
+
+### Boolean Operators
+
+Python also supports boolean operators, although they're a little different than the comparison operators. Remember that `or` and `and` return one of their operands, rather than `True` or `False`.
+
+|Operation|Result|
+|---|---|
+|`x or y`|if x is false, then y, else x|
+|`x and y`|if x is false, then x, else y|
+|`not x`|if x is false, then `True`, else `False`|
+
+```python
+>>> True or False
+>>> [] or [1, 2, 3]
+>>> "Hello" or None
+```
+
+```python
+>>> True and False
+>>> 5 and 0
+>>> [1] and [1, 2, 3]
+>>> "Hello" and None
+```
+
+```python
+# Of course, you can use `and` and `or` aren't limited to two operands
+>>> a = False
+>>> b = False
+>>> c = False
+>>> a or b or c
+>>> b = True
+>>> a or b or c
+
+>>> a and b and c
+>>> a = True
+>>> c = True
+>>> a and b and c
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+```python
+>>> True or False
+True
+>>> [] or [1, 2, 3]
+[1, 2, 3]
+>>> "Hello" or None
+'Hello'
+```
+
+```python
+>>> True and False
+False
+>>> 5 and 0
+0
+>>> [1] and [1, 2, 3]
+[1, 2, 3]
+>>> "Hello" and None
+>>> # No output, since the result was None
+```
+
+```python
+>>> a = False
+>>> b = False
+>>> c = False
+>>> a or b or c
+False
+>>> b = True
+>>> a or b or c
+True
+
+>>> a and b and c
+False
+>>> a = True
+>>> c = True
+>>> a and b and c
+True
+```
+
+{{% /expand %}}
diff --git a/website/content/02-introduction-to-python/090-boolean-logic/_index.md b/website/content/02-introduction-to-python/090-boolean-logic/_index.md
new file mode 100644
index 0000000..b8649f6
--- /dev/null
+++ b/website/content/02-introduction-to-python/090-boolean-logic/_index.md
@@ -0,0 +1,14 @@
++++
+title = "Boolean Logic"
+date = 2019-01-25T15:04:16-06:00
+weight = 90
+chapter = true
+draft = false
+pre = "5. "
++++
+
+### Chapter 5
+
+# Boolean Logic
+
+Boolean Logic allows you to test your assumptions.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/110-control-statements-looping/05-looping-in-python.md b/website/content/02-introduction-to-python/110-control-statements-looping/05-looping-in-python.md
new file mode 100644
index 0000000..a08c35d
--- /dev/null
+++ b/website/content/02-introduction-to-python/110-control-statements-looping/05-looping-in-python.md
@@ -0,0 +1,195 @@
+---
+title: "Looping in Python"
+date: 2019-02-10T18:21:55-08:00
+draft: false
+weight: 5
+---
+
+### `for` Loop Cheat Sheet
+
+Looping in Python doesn't look like looping in other languages.
+
+If you write JavaScript, Java, or other languages, you might have seen code that looks something like this code, that keeps track of 3 things: the starting index, the condition the loop will run until, and which action to take (in this case, incrementing the variable `i` by 1) until the condition is met.
+
+```javascript
+for (i = 0; i < 5; i++) {
+ text += "The number is " + i + " ";
+}
+```
+
+In fact, before these languages introduced something called a `for each` loop, that was also the clunky way you'd loop through items in a sequence.
+
+### Looping in Python
+
+Looping in Python is a simpler, cleaner process because the Python language prides itself on readability.
+
+Remember you used the `in` keyword to test if an item was in a sequence? When combined with the `for` keyword, `in` can be used to indicate looping over each item in the sequence. The syntax is: `for single_item in items`, followed by a colon `:`, followed by a new line, a level of indentation, and the code you'd like to consider as the *body* of the loop. That is, the code that'll run multiple times, until there are no more items in the collection.
+
+Let's see it in action.
+
+```python
+>>> colors = ["Red", "Green", "Blue", "Orange"]
+>>> for color in colors:
+... print(f"The color is: {color}")
+The color is: Red
+The color is: Green
+The color is: Blue
+The color is: Orange
+```
+
+#### Looping over a range of numbers
+
+Let's say we wanted to duplicate the code in the example JavaScript above, that prints out the numbers from 0 to 4.
+
+In order to do this, we'll need to use a built-in function called `range()`. The range function in python produces a sequence of integers from an optional and inclusive start, to a defined and exclusive finish.
+
+
+In Python2, this function created a list of each number in that sequence. As you can imagine, it was horribly inefficient for large ranges. In Python3, the `range()` function returns a new optimized data type. It's great for optimization, but it's harder for debugging.
+
+{{% notice note %}}
+If you want to explicitly see what a call to `range()` produces for debugging purposes, you can pass the result into the `list()` method to see all the values at once. For example: `list(range(5))`. Remember that this is inefficient, so use it for testing, not in production code.
+{{% /notice %}}
+
+If we wanted to loop over all the values from 0 to 4, we'd use the range function like this:
+
+```python
+>>> for num in range(5):
+... print(f"The number is: {num}")
+...
+The number is: 0
+The number is: 1
+The number is: 2
+The number is: 3
+The number is: 4
+```
+
+You'll notice that this call didn't *include* the number 5.
+
+What if we wanted the range from 1 to 4, instead of 0 to 4? `range()` can be called with `start` and `stop` parameters, and the range will *start* from `start`.
+
+```python
+>>> for num in range(1, 5):
+... print(f"The number is: {num}")
+...
+The number is: 1
+The number is: 2
+The number is: 3
+The number is: 4
+```
+
+You can also pass an a third optional `step` parameter in. Let's say I quickly wanted to print out all the even numbers from 2 to 10. I would call `range(2, 11, 2)`. Remember, 2 is where we're starting, 11 is one higher than where we're ending (10), and 2 is the step, or the amount to jump between numbers.
+
+```python
+>>> for num in range(2, 11, 2):
+... print(f"The number is: {num}")
+...
+The number is: 2
+The number is: 4
+The number is: 6
+The number is: 8
+The number is: 10
+```
+
+What do inclusive and exclusive mean in this context? *Exclusive* means that the end result *will not* include that number. If you'd like the numbers from 0 to 4, you would call `range(5)`. Consider 5 to the *stopping point*. *Inclusive* means the range will include the number. The `start` parameter is inclusive, meaning if you'd like the range of numbers from 1 to 4, you'd call `range(1, 5)`.
+
+{{% notice tip %}}
+If you can't remember how to use range, don't forget to call `help(range)` from the command line.
+{{% /notice %}}
+
+#### Looping over items with the index using `enumerate`.
+
+In Python, we avoid writing code like the JavaScript `for` loop at the top, but sometimes it's unavoidable, and we need a way to access the index of the items we're looping through. To do that we use a special function called `enumerate()`. The function takes a sequence, like a `list`, and it *returns* a `list` of tuples, containing the index of the item in the sequence, and the sequence itself.
+
+Don't worry about the list of tuples for now, but remember our tuple unpacking from earlier?
+
+```python
+>>> point = (2, 5, 11)
+>>> x, y, z = point
+>>> x
+2
+>>> y
+5
+>>> z
+11
+```
+
+Because `enumerate()` returns a structure that looks like a list of `tuple`s under the hood, we can take advantage of tuple unpacking in the `for` loop.
+
+```python
+>>> for index, item in enumerate(colors):
+... print(f"Item: {item} is at index: {index}.")
+...
+Item: Red is at index: 0.
+Item: Green is at index: 1.
+Item: Blue is at index: 2.
+Item: Orange is at index: 3.
+```
+
+{{% notice tip %}}
+Remember, indicies in Python start at zero.
+{{% /notice %}}
+
+#### Looping over a dictionary
+
+Now that we know we can use tuple unpacking in a for loop, let's go over how to loop over a dictionary.
+
+Let's say we have a dictionary of colors to their hex color code used for HTML in websites.
+
+```python
+>>> hex_colors = {
+... "Red": "#FF",
+... "Green": "#008",
+... "Blue": "#0000FF",
+... }
+```
+
+{{% notice warning %}}
+Remember, a dictionary is composed of key, value pairs. When we loop over a dictionary with the `for item in my_dict` syntax, we'll end up looping over **just** the keys.
+{{% /notice %}}
+
+In this example, notice how we're looping over the wrong thing:
+
+```python
+>>> for color in hex_colors:
+... print(f"The value of color is actually: {color}")
+...
+The value of color is actually: Red
+The value of color is actually: Green
+The value of color is actually: Blue
+```
+
+{{% notice tip %}}
+If we want to loop over the key, value pairs in a dictionary, we'll want to call `my_dict.items()`.
+{{% /notice %}}
+
+We can use tuple unpacking along with the `my_dict.items()` list to loop over both the keys and the values at the same time.
+
+```python
+>>> for color, hex_value in hex_colors.items():
+... print(f"For color {color}, the hex value is: {hex_value}")
+...
+For color Red, the hex value is: #FF0000
+For color Green, the hex value is: #008000
+For color Blue, the hex value is: #0000FF
+```
+
+##### Common Errors
+
+What if you try to loop over key, value pairs, and forget to use `my_dict.items()`?
+
+```python
+>>> for color, hex_value in hex_colors:
+... print(f"For color {color}, the hex value is: {hex_value}")
+...
+Traceback (most recent call last):
+ File "", line 1, in
+ValueError: too many values to unpack (expected 2)
+```
+
+{{% notice info %}}
+You'll see `ValueError: too many values to unpack (expected 2)` if you *forget* to call `my_dict.items()`, and try to loop over what you'd expect to be key, value pairs.
+{{% /notice %}}
+
+#### Additional Resources
+
+If you really want to be a pro at looping in a Pythonic way, I recommend watching Raymond Hettinger's talk - [Transforming Code into Beautiful, Idiomatic Python](https://www.youtube.com/watch?time_continue=1855&v=OSGv2VnC0go) after the course.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/110-control-statements-looping/10-if-else-elif.md b/website/content/02-introduction-to-python/110-control-statements-looping/10-if-else-elif.md
new file mode 100644
index 0000000..ff9d09c
--- /dev/null
+++ b/website/content/02-introduction-to-python/110-control-statements-looping/10-if-else-elif.md
@@ -0,0 +1,230 @@
+---
+title: "if, else, elif"
+date: 2019-02-10T18:20:07-08:00
+draft: false
+weight: 10
+---
+
+### The `if` Statement and Conditionals
+
+`if` in Python means: **only run the rest of this code once, *if* the condition evaluates to `True`.** Don't run the rest of the code at all if it's not.
+
+Anatomy of an `if` statement: Start with the `if` keyword, followed by a boolean value, an expression that evaluates to `True`, or a value with "Truthiness". Add a colon `:`, a new line, and write the code that will run if the statement is `True` under a level of indentation.
+
+{{% notice note %}}
+Remember, just like with functions, we know that code is associated with an `if` statement *by it's level of indentation*. All the lines indented under the `if` statement will run if it evaluates to `True`.
+{{% /notice %}}
+
+```python
+>>> if 3 < 5:
+... print("Hello, World!")
+...
+Hello, World!
+```
+
+{{% notice tip %}}
+Remember, your `if` statements only run if the expression in them evaluates to `True` and just like with functions, you'll need to enter an extra space in the REPL to run it.
+{{% /notice %}}
+
+#### Using `not` With `if` Statements
+
+If you only want your code to run if the expression is `False`, use the `not` keyword.
+
+```python
+>>> b = False
+>>> if not b:
+... print("Negation in action!")
+...
+Negation in action!
+```
+
+### `if` Statements and Truthiness
+
+`if` statements also work with items that have a "truthiness" to them.
+
+For example:
+
+ * The number 0 is False-y, any other number (including negatives) is Truth-y
+ * An empty `list`, `set`, `tuple` or `dict` is False-y
+ * Any of those structures with items in it is Truth-y
+
+```python
+>>> message = "Hi there."
+
+>>> a = 0
+>>> if a: # 0 is False-y
+... print(message)
+...
+
+>>> b = -1
+>>> if b: # -1 is Truth-y
+... print(message)
+...
+Hi there.
+
+>>> c = []
+>>> if c: # Empty list is False-y
+... print(message)
+...
+
+>>> d = [1, 2, 3]
+>>> if d: # List with items is Truth-y
+... print(message)
+...
+Hi there.
+```
+
+### `if` Statements and Functions
+
+You can easily declare `if` statements in your functions, you just need to mindful of the level of indentation. Notice how the code belonging to the `if` statement is indented at *two levels*.
+
+```python
+>>> def modify_name(name):
+... if len(name) < 5:
+... return name.upper()
+... else:
+... return name.lower()
+...
+>>> name = "Nina"
+>>> modify_name(name)
+'NINA'
+```
+
+#### Nested `if` Statements
+
+Using the same technique, you can also *nest* your `if` statements.
+
+```python
+>>> def num_info(num):
+... if num > 0:
+... print("Greater than zero")
+... if num > 10:
+... print("Also greater than 10.")
+...
+>>> num_info(1)
+Greater than zero
+>>> num_info(15)
+Greater than zero
+Also greater than 10.
+```
+
+
+#### How *Not* To Use `if` Statements
+
+Remember, comparisons in Python evaluate to `True` or `False`. With conditional statements, we check for that value *implicitly*. In Python, we **do not** want to compare to `True` or `False` with `==`.
+
+{{% notice warning %}}
+Warning - pay attention, because the code below shows what you **shouldn't** do.
+{{% /notice %}}
+
+```python
+# Warning: Don't do this!
+>>> if (3 < 5) == True: # Warning: Don't do this!
+... print("Hello")
+...
+Hello
+
+# Warning: Don't do this!
+>>> if (3 < 5) is True: # Warning: Don't do this!
+... print("Hello")
+...
+Hello
+```
+
+{{% notice tip %}}
+Do this instead:
+{{% /notice %}}
+
+```python
+>>> if 3 < 5:
+... print("Hello")
+...
+Hello
+```
+
+If we want to explicitly check if the value is explicitly set to `True` or `False`, we can use the `is` keyword.
+
+```python
+>>> a = True # a is set to True
+>>> b = [1, 2, 3] # b is a list with items, is "truthy"
+>>>
+>>> if a and b: # this is True, a is True, b is "truthy"
+... print("Hello")
+...
+Hello
+>>> if a is True: # we can explicitly check if a is True
+... print("Hello")
+...
+Hello
+>>> if b is True: # b does not contain the actual value of True.
+... print("Hello")
+...
+>>>
+```
+
+### `else`
+
+The `else` statement is what you want to run **if and only if** your `if` statement wasn't triggered.
+
+An `else` statement is part of an `if` statement. If your `if` statement ran, your `else` statement will never run.
+
+
+```python
+>>> a = True
+>>> if a:
+... print("Hello")
+... else:
+... print("Goodbye")
+...
+Hello
+```
+
+And vice-versa.
+
+```python
+>>> a = False
+>>> if a:
+... print("Hello")
+... else:
+... print("Goodbye")
+...
+Goodbye
+```
+
+In the REPL it must be written on the line after your last line of indented code. In Python code in a file, there can't be any other code between the `if` and the `else`.
+
+{{% notice info %}}
+You'll see `SyntaxError: invalid syntax` if you try to write an `else` statement on its own, or put extra code between the `if` and the `else` in a Python file.
+{{% /notice %}}
+
+```python
+>>> if a:
+... print("Hello")
+...
+Hello
+>>> else:
+ File "", line 1
+ else:
+ ^
+SyntaxError: invalid syntax
+```
+
+### `elif` Means Else, If.
+
+`elif` means *else if*. It means, **if** this `if` statement isn't considered `True`, try this **instead.**
+
+You can have as many `elif` statements in your code as you want. They get evaluated in the order that they're declared **until** Python finds one that's `True`. That runs the code defined in that `elif`, and skips the rest.
+
+```python
+>>> a = 5
+>>> if a > 10:
+... print("Greater than 10")
+... elif a < 10:
+... print("Less than 10")
+... elif a < 20:
+... print("Less than 20")
+... else:
+... print("Dunno")
+...
+Less than 10
+```
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/110-control-statements-looping/30-while-loops.md b/website/content/02-introduction-to-python/110-control-statements-looping/30-while-loops.md
new file mode 100644
index 0000000..656563c
--- /dev/null
+++ b/website/content/02-introduction-to-python/110-control-statements-looping/30-while-loops.md
@@ -0,0 +1,49 @@
+---
+title: "while loops"
+date: 2019-02-10T18:22:01-08:00
+draft: false
+weight: 30
+---
+
+`while` loops are a special type of loop in Python. Instead of running just once when a condition is met, like an `if` statement, they run **forever** until a condition is *no longer* met.
+
+`while` loops usually need to be accompanied by an always changing sentinel value.
+
+```python
+>>> counter = 0
+>>> max = 4
+>>>
+>>> while counter < max:
+... print(f"The count is: {counter}")
+... counter = counter + 1
+...
+The count is: 0
+The count is: 1
+The count is: 2
+The count is: 3
+```
+
+{{% notice warning %}}
+Our loop will run forever if we forget to *update* the sentinel value. **Press Ctrl-C to exit the infinite loop.**
+{{% /notice %}}
+
+
+```python
+# Warning: don't copy and paste this example.
+
+>>> counter = 0
+>>> max = 4
+
+>>> while counter < max:
+... print(f"The count is: {counter}")
+...
+# What happens if we don't update counter?
+The count is: 0
+The count is: 0
+The count is: 0
+The count is: 0
+# An infinite loop repeated until we hit Ctrl-C
+The count ^CTraceback (most recent call last):
+ File "", line 2, in
+KeyboardInterrupt
+```
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/110-control-statements-looping/40-break-continue.md b/website/content/02-introduction-to-python/110-control-statements-looping/40-break-continue.md
new file mode 100644
index 0000000..3d99e5b
--- /dev/null
+++ b/website/content/02-introduction-to-python/110-control-statements-looping/40-break-continue.md
@@ -0,0 +1,153 @@
+---
+title: "break, continue, and return"
+date: 2019-03-03T16:05:01-08:00
+draft: false
+weight: 40
+---
+
+
+
+`break` and `continue` allow you to control the flow of your loops. They're a concept that beginners to Python tend to misunderstand, so pay careful attention.
+
+### Using `break`
+
+The `break` statement will completely break out of the *current loop*, meaning it won't run any more of the statements contained inside of it.
+
+
+```python
+>>> names = ["Rose", "Max", "Nina", "Phillip"]
+>>> for name in names:
+... print(f"Hello, {name}")
+... if name == "Nina":
+... break
+...
+Hello, Rose
+Hello, Max
+Hello, Nina
+```
+
+{{% notice tip %}}
+`break` completely **breaks out** of the loop.
+{{% /notice %}}
+
+### Using `continue`
+
+`continue` works a little differently. Instead, it goes back to the start of the loop, skipping over any other statements contained within the loop.
+
+```python
+>>> for name in names:
+... if name != "Nina":
+... continue
+... print(f"Hello, {name}")
+...
+Hello, Nina
+```
+
+{{% notice tip %}}
+`continue` continues to the **start of the loop**
+
+{{% /notice %}}
+
+
+### `break` and `continue` visualized
+
+What happens when we run the code from this Python file?
+
+```python
+# Python file names.py
+names = ["Jimmy", "Rose", "Max", "Nina", "Phillip"]
+
+for name in names:
+ if len(name) != 4:
+ continue
+
+ print(f"Hello, {name}")
+
+ if name == "Nina":
+ break
+
+print("Done!")
+```
+
+
+
+#### Results
+
+{{%expand "See if you can guess the results before expanding this section." %}}
+```bash
+(env) $ python names.py
+
+Hello, Rose
+Hello, Nina
+Done!
+```
+{{% /expand%}}
+
+### Using `break` and `continue` in nested loops.
+
+Remember, `break` and `continue` only work for the **current loop**. *Even though I've been programming Python for years, this is something that still trips me up!*
+
+```python
+>>> names = ["Rose", "Max", "Nina"]
+>>> target_letter = 'x'
+>>> for name in names:
+... print(f"{name} in outer loop")
+... for char in name:
+... if char == target_letter:
+... print(f"Found {name} with letter: {target_letter}")
+... print("breaking out of inner loop")
+... break
+...
+Rose in outer loop
+Max in outer loop
+Found Max with letter: x
+breaking out of inner loop
+Nina in outer loop
+>>>
+```
+
+{{% notice tip %}}
+`break` in the inner loop only breaks out of the inner loop! The outer loop continues to run.
+{{% /notice %}}
+
+
+### Loop Control in `while` loops
+
+You can also use `break` and `continue` in `while` loops. One common scenario is running a loop forever, until a certain condition is met.
+
+```python
+>>> count = 0
+>>> while True:
+... count += 1
+... if count == 5:
+... print("Count reached")
+... break
+...
+Count reached
+```
+
+{{% notice note %}}
+Be careful that your condition will eventually be met, or else your program will get stuck in an infinite loop. For production use, it's better to use asynchronous programming.
+{{% /notice %}}
+
+### Loops and the `return` statement
+
+Just like in functions, consider the `return` statement the hard kill-switch of the loop.
+
+```python
+>>> def name_length(names):
+... for name in names:
+... print(name)
+... if name == "Nina":
+... return "Found the special name"
+...
+>>> names = ["Max", "Nina", "Rose"]
+>>> name_length(names)
+Max
+Nina
+'Found the special name'
+```
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/110-control-statements-looping/50-exercise.md b/website/content/02-introduction-to-python/110-control-statements-looping/50-exercise.md
new file mode 100644
index 0000000..f12dc48
--- /dev/null
+++ b/website/content/02-introduction-to-python/110-control-statements-looping/50-exercise.md
@@ -0,0 +1,438 @@
+---
+title: "Practice"
+date: 2019-03-08T00:00:00-08:00
+draft: false
+weight: 50
+pre: "⭐️ "
+---
+
+## Control statements and looping
+
+### `if`, `else`, and `elif`
+
+Let's practice our branching statements. Remember that `elif` (short for `else if`) is an optional branch that will let you add another `if` test, and `else` is an optional branch that will catch anything not previously caught by `if` or `elif`.
+
+```python
+>>> def test_number(number):
+... if number < 100:
+... print("This is a pretty small number")
+... elif number == 100:
+... print("This number is alright")
+... else:
+... print("This number is huge!")
+...
+>>> test_number(5)
+>>> test_number(99)
+>>> test_number(100)
+>>> test_number(8675309)
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+```python
+>>> def test_number(number):
+... if number < 100:
+... print("This is a pretty small number")
+... elif number == 100:
+... print("This number is alright")
+... else:
+... print("This number is huge!")
+...
+>>> test_number(5)
+This is a pretty small number
+>>> test_number(99)
+This is a pretty small number
+>>> test_number(100)
+This number is alright
+>>> test_number(8675309)
+This number is huge!
+```
+{{%/expand%}}
+
+You can also have multiple conditions in an if statement. This function prints "Fizzbuzz!" if the number is divisible by both 3 and 5 (the `%` or modulo operator returns the remainder from the division of two numbers):
+
+```python
+>>> def fizzbuzz(number):
+... if number % 3 == 0 and number % 5 == 0:
+... print("Fizzbuzz!")
+...
+>>> fizzbuzz(3)
+>>> fizzbuzz(5)
+>>> fizzbuzz(15)
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+```python
+>>> def fizzbuzz(number):
+... if number % 3 == 0 and number % 5 == 0:
+... print("Fizzbuzz!")
+...
+>>> fizzbuzz(3)
+>>> fizzbuzz(5)
+>>> fizzbuzz(15)
+Fizzbuzz!
+```
+{{%/expand%}}
+
+Let's also practice using `if` to test for an empty list. Remember that an empty list is "Falsey", or resolves to `False`. Write a function to print a list of elements, or an error message if the list is empty. Print a special message if a list item is `None`:
+
+```python
+>>> def my_func(my_list):
+... if my_list:
+... for item in my_list:
+... if item is None:
+... print("Got None!")
+... else:
+... print(item)
+... else:
+... print("Got an empty list!")
+...
+>>> my_func([1, 2, 3])
+1
+2
+3
+>>> my_func([2, None, "hello", 42])
+2
+Got None!
+hello
+42
+>>> my_func([])
+Got an empty list!
+```
+
+
+## The `for` loop, `range()` and `enumerate()`
+
+Let's try making a list and looping over it:
+
+```python
+>>> my_list = [0, 1, 2]
+>>> for num in my_list:
+... print(f"Next value: {num}")
+...
+```
+
+
+If we're just interested in looping over a list of numbers, we can use the `range()` function instead. Remember that the first argument is inclusive and the second is exclusive:
+
+```python
+>>> for num in range(0, 3):
+... print(f"Next value: {num}")
+...
+```
+
+Another useful function is `enumerate()`, which iterates over an iterable (like a list) and also gives you an automatic counter. `enumerate()` returns a tuple in the form of (`counter`, `item`).
+
+```python
+>>> my_list = ["foo", "bar", "baz"]
+>>> for index, item in enumerate(my_list):
+... print(f"Item {index}: {item}")
+...
+```
+
+We can also loop over a dictionary's keys and/or values. If you try to iterate over the dictionary object itself, what do you get?
+
+```python
+>>> my_dict = {"foo": "bar", "hello": "world"}
+>>> for key in my_dict:
+... print(f"Key: {key}")
+...
+# This is equivalent to...
+>>> for key in my_dict.keys():
+... print(f"Key: {key}")
+...
+```
+
+The `keys()` method returns the dictionary's keys as a list, which you can then iterate over as you would any other list. This also works for `values()`
+
+```python
+>>> for value in my_dict.values():
+... print(f"Value: {value}")
+...
+```
+
+The most useful function, however, is `items()`, which returns the dictionary's items as tuples in the form of (key, value):
+
+```python
+>>> for key, value in my_dict.items():
+... print(f"Item {key} = {value}")
+...
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+```python
+>>> my_list = [1, 2, 3]
+>>> for num in my_list:
+... print(f"Next value: {num}")
+...
+Next value: 0
+Next value: 1
+Next value: 2
+```
+
+```python
+>>> for num in range(0, 3):
+... print(f"Next value: {num}")
+...
+Next value: 0
+Next value: 1
+Next value: 2
+```
+
+```python
+>>> my_list = ["foo", "bar", "baz"]
+>>> for index, item in enumerate(my_list):
+... print(f"Item {index}: {item}")
+...
+Item 0: foo
+Item 1: bar
+Item 2: baz
+```
+
+```python
+>>> my_dict = {"foo": "bar", "hello": "world"}
+>>> for key in my_dict:
+... print(f"Key: {key}")
+...
+Key: foo
+Key: hello
+
+>>> for key in my_dict.keys():
+... print(f"Key: {key}")
+...
+Key: foo
+Key: hello
+
+>>> for value in my_dict.values():
+... print(f"Value: {value}")
+...
+Value: bar
+Value: world
+
+>>> for key, value in my_dict.items():
+... print(f"Item {key} = {value}")
+...
+Item foo = bar
+Item hello = world
+```
+
+{{%/expand%}}
+
+## `break`, `continue`, and `return`
+
+`break` and `continue` are important functions for controlling the program flow inside loops. `break` ends the loop immediately and continues executing from outside the loop's scope, and `continue` skips the remainder of the loop and continues executing from the next round of the loop. Let's practice:
+
+```python
+>>> for num in range(0, 100):
+... print(f"Testing number {num}")
+... if num == 3:
+... print("Found number 3!")
+... break
+... print("Not yet...")
+...
+```
+
+Notice that "Not yet..." doesn't get printed for number 3, because we `break` out of the loop first. Let's try a `continue`:
+
+```python
+>>> for num in range(0, 100):
+... print(f"Testing number {num}")
+... if num < 3:
+... continue
+... elif num == 5:
+... print("Found number 5!")
+... break
+... print("Not yet...")
+...
+```
+
+Notice that "Not yet..." doesn't get printed at all until the number is 3, because the `continue` short-circuits the loop back to the beginning. Then we `break` when we hit 5.
+
+You can also use the `return` keyword to break out of a loop within a function, while optionally returning a value.
+
+```python
+>>> def is_number_in_list(number_to_check, list_to_search):
+... for num in list_to_search:
+... print(f"Checking {num}...")
+... if num == number_to_check:
+... return True
+... return False
+>>> my_list = [1, 2, 3, 4, 5]
+>>> is_number_in_list(27, my_list)
+>>> is_number_in_list(2, my_list)
+```
+
+Notice that our function `is_number_in_list` checks all the numbers in `my_list` on the first run, but on the next run, stops immediately when it hits 3 and returns `True`.
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> for num in range(0, 100):
+... print(f"Testing number {num}")
+... if num == 3:
+... print("Found number 3!")
+... break
+... print("Not yet...")
+...
+Testing number 0
+Not yet...
+Testing number 1
+Not yet...
+Testing number 2
+Not yet...
+Testing number 3
+Found number 3!
+>>>
+```
+
+```python
+>>> for num in range(0, 100):
+... print(f"Testing number {num}")
+... if num < 3:
+... continue
+... elif num == 5:
+... print("Found number 5!")
+... break
+... print("Not yet...")
+...
+Testing number 0
+Testing number 1
+Testing number 2
+Testing number 3
+Not yet...
+Testing number 4
+Not yet...
+Testing number 5
+Found number 5!
+```
+
+```python
+>>> def is_number_in_list(number_to_check, list_to_search):
+... for num in list_to_search:
+... print(f"Checking {num}...")
+... if num == number_to_check:
+... return True
+... return False
+...
+>>> is_number_in_list(27, my_list)
+Checking 1...
+Checking 2...
+Checking 3...
+Checking 4...
+Checking 5...
+False
+>>> is_number_in_list(2, my_list)
+Checking 1...
+Checking 2...
+True
+```
+{{%/expand%}}
+
+## `while` loop
+
+Instead of looping over a sequence, `while` loops continue looping while a certain condition is met (or not met). The condition is checked at the beginning every iteration.
+
+```python
+>>> counter = 0
+>>> while counter < 3:
+... print(f"Counter = {counter}")
+... counter += 1
+```
+
+Notice that the loop ends once `counter` 3, and the remainder of the loop is bypassed. You can also loop forever by using `while True` or `while False`, but you should make sure you have solid `break` conditions, or your program will just loop forever (unless that's what you want).
+
+```python
+>>> counter = 0
+>>> while True:
+... print(f"Counter = {counter}")
+... if counter == 3:
+... break
+... counter += 1
+```
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+
+```python
+>>> counter = 0
+>>> while counter < 3:
+... print(f"Counter = {counter}")
+... counter += 1
+...
+Counter = 0
+Counter = 1
+Counter = 2
+```
+
+```python
+>>> counter = 0
+>>> while True:
+... print(f"Counter = {counter}")
+... if counter == 3:
+... break
+... counter += 1
+...
+Counter = 0
+Counter = 1
+Counter = 2
+Counter = 3
+```
+
+{{%/expand%}}
+
+## Nested Loops
+
+Nesting loops is often necessary and sometimes tricky. The `break` keyword will only get you out of whichever loop you're `break`ing. The only way to exit all loops is with multiple `break` statements (at each level), or the `return` keyword (inside a function). For example:
+
+```python
+names = ["Rose", "Max", "Nina"]
+target_letter = 'x'
+found = False
+
+for name in names:
+ for char in name:
+ if char == target_letter:
+ found = True
+ break
+
+ if found:
+ print(f"Found {name} with letter: {target_letter}")
+ break
+```
+
+Or:
+
+```python
+>>> for x in range(0, 5):
+... for y in range(0, 5):
+... print(f"x = {x}, y = {y}")
+... if y == 2:
+... break
+...
+```
+
+Notice how the inner `y` loop never gets above 2, whereas the outer `x` loop continues until the end of its range.
+
+{{%expand "Here's what you should have seen in your REPL:" %}}
+```python
+>>> for x in range(0, 5):
+... for y in range(0, 5):
+... print(f"x = {x}, y = {y}")
+... if y == 2:
+... break
+...
+x = 0, y = 0
+x = 0, y = 1
+x = 0, y = 2
+x = 1, y = 0
+x = 1, y = 1
+x = 1, y = 2
+x = 2, y = 0
+x = 2, y = 1
+x = 2, y = 2
+x = 3, y = 0
+x = 3, y = 1
+x = 3, y = 2
+x = 4, y = 0
+x = 4, y = 1
+x = 4, y = 2
+```
+{{%/expand%}}
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/110-control-statements-looping/_index.md b/website/content/02-introduction-to-python/110-control-statements-looping/_index.md
new file mode 100644
index 0000000..46e76ad
--- /dev/null
+++ b/website/content/02-introduction-to-python/110-control-statements-looping/_index.md
@@ -0,0 +1,14 @@
++++
+title = "Loops and Control Statements"
+date = 2019-01-25T15:16:00-06:00
+weight = 110
+chapter = true
+draft = false
+pre = "6. "
++++
+
+### Chapter 6
+
+# Loops and Control Statements
+
+Loops and control statements allow us to control the logical flow of our program.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/110-control-statements-looping/code/names.py b/website/content/02-introduction-to-python/110-control-statements-looping/code/names.py
new file mode 100644
index 0000000..b6a792b
--- /dev/null
+++ b/website/content/02-introduction-to-python/110-control-statements-looping/code/names.py
@@ -0,0 +1,12 @@
+names = ["Jimmy", "Rose", "Max", "Nina", "Phillip"]
+
+for name in names:
+ if len(name) != 4:
+ continue
+
+ print(f"Hello, {name}")
+
+ if name == "Nina":
+ break
+
+print("Done!")
diff --git a/website/content/02-introduction-to-python/110-control-statements-looping/images/break-continue.png b/website/content/02-introduction-to-python/110-control-statements-looping/images/break-continue.png
new file mode 100644
index 0000000..631b9cf
Binary files /dev/null and b/website/content/02-introduction-to-python/110-control-statements-looping/images/break-continue.png differ
diff --git a/website/content/02-introduction-to-python/175-running-code/10-running-python-programs.md b/website/content/02-introduction-to-python/175-running-code/10-running-python-programs.md
new file mode 100644
index 0000000..1316b49
--- /dev/null
+++ b/website/content/02-introduction-to-python/175-running-code/10-running-python-programs.md
@@ -0,0 +1,116 @@
+---
+title: "How To Run Them"
+date: 2019-02-10T18:17:06-08:00
+draft: false
+weight: 10
+---
+
+### Creating Python Files with the `*.py` extension
+
+You know a file is a Python program when it ends with a `.py` extension.
+
+#### Naming Tips
+
+{{% notice tip %}}
+Just like with formatting, Python's [PEP8 guidelines](https://www.python.org/dev/peps/pep-0008/#package-and-module-names) give us a few helpful tips about how to name our Python program files.
+{{% /notice %}}
+
+ℹ️ In Python:
+
+1. Filenames should be ***all lowercase**
+1. Words should be separated with **underscores `_`**
+1. Filenames should be **short**
+
+✅ Some good example filenames:
+
+- `apis.py`
+- `exceptions.py`
+- `personal_blog.py`
+
+⛔️ Some bad example filenames:
+
+- `MYFILE.PY`
+- `CamelCaseFile.py`
+- `really_long_over_descriptive_project_file_name.py`
+
+
+#### What are `*.pyc` files?
+
+{{% notice note %}}
+For optimization and other reasons, Python code can be compiled to intermediary `.pyc` files. The good news is you don't have to worry about them. The bad news is, very occasionally stale versions of these compiled files can cause problems. To safely delete them from the current project directory, run `find . -name "*.pyc" -delete` (on linux or macOS).
+{{% /notice %}}
+
+#### `git` tip: use a `.gitignore` for Python
+
+If you use `git` for source control, you'll want to make sure that these compiled `*.pyc` files are ignored, and not added to your repository.
+
+The best way to do this is to [add the standard `.gitignore` file for Python](https://github.com/github/gitignore/blob/master/Python.gitignore) to your project.
+
+
+### Running Python Files From VS Code
+
+Running Python files from VS Code is really quick and easy.
+
+#### Creating New Python Files
+
+To create a new file in VS Code, hit `Ctrl+N` on Windows and Linux, and `⌘N` (command + N) on Mac OS.
+
+This will open a new file. Next, save the file with a `.py` extension.
+
+{{% notice info %}}
+Create a new simple Python program in a file called `hello.py` in our `pyworkshop` direc tory with the following contents:
+{{% /notice %}}
+
+```python
+# in file: hello.py
+greetings = ["Hello", "Bonjour", "Hola"]
+
+for greeting in greetings:
+ print(f"{greeting}, World!")
+```
+
+#### Opening The VS Code Terminal Window
+
+Next, you'll need to open your terminal if you don't have it open already. The quick keyboard shortcut to do that is Ctrl - `
+
+{{% notice note %}}
+If you already had your Python REPL open, you'll need to select a terminal with a shell in it (generally, the one labeled with `1:`).
+{{% /notice %}}
+
+
+
+
+#### Running The File
+
+Once you've opened your `hello.py` file and selected your new terminal window, open the VS Code command palette.
+
+{{% notice note %}}
+Open the command palette with `Ctrl+Shift+P` on Windows and Linux, and `⌘⇧P` (command + shift + P) on Mac OS.
+{{% /notice %}}
+
+Select Python: Run Python File in Terminal
+
+
+
+You should see:
+
+```bash
+Hello, World!
+Bonjour, World!
+Hola, World!
+```
+
+How easy was that? 🎉
+
+### Running Python Files From a Non-VS Code Terminal
+
+If you want to run a Python file without using the command palette, just open your terminal of choice, `cd` into the directory with your code, and type in the command `python` followed by a space, and the name of your Python program.
+
+```bash
+(env) $ python hello.py
+Hello, World!
+Bonjour, World!
+Hola, World!
+```
+
+This also works in the VS Code terminal.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/175-running-code/20-print-tips.md b/website/content/02-introduction-to-python/175-running-code/20-print-tips.md
new file mode 100644
index 0000000..a00da21
--- /dev/null
+++ b/website/content/02-introduction-to-python/175-running-code/20-print-tips.md
@@ -0,0 +1,169 @@
+---
+title: "Printing Tips"
+date: 2019-02-10T18:13:59-08:00
+draft: false
+weight: 20
+---
+
+One of the nice things about the REPL is we can quickly and easily see the contents of our variables, just by typing their name and pressing enter. Unfortunately, running code from Python files doesn't do quite the same thing.
+
+In a file named `name.py`:
+
+```python
+# file name.py
+name = "Nina"
+name
+```
+
+Output:
+
+```bash
+(env) $ python name.py
+```
+
+*Notice, there was no output.*
+
+Now, in a file named `print_name.py`:
+```python
+# file print_name.py
+name = "Nina"
+print(name)
+```
+
+Output:
+```bash
+(env) $ python name.py
+Nina
+```
+
+Hooray! Now we see some output. 🎉
+
+{{% notice tip %}}
+If you want to see any output from your Python programs, you'll need to use `print()`.
+{{% /notice %}}
+
+### Debugging Your Code With `print()`
+
+As your Python programs become more complicated, you'll want to do some basic debugging to figure out what's going on under the hood. For beginners, using `print()` is a great way to accomplish that goal.
+
+{{% notice note %}}
+If you write Python code on a team or plan on sharing it, it's a good idea to *remove* your debugging `print()`s before you share your code with others.
+{{% /notice %}}
+
+In a Python file named `mystery.py`:
+
+```python
+def mystery():
+ num = 10 * 3
+
+ if num == 10:
+ print("Num was equal to 10")
+ num = num * 10
+ if num == 20:
+ print("Num was equal to 20")
+ num = num * 20
+ if num == 30:
+ print("Num was equal to 30")
+ num = num * 30
+
+ print(f"Value of returned num is: {num}")
+ return num
+
+mystery()
+```
+
+We'll see the output:
+
+```python
+Num was equal to 30
+Value of returned num is: 900
+```
+
+{{% notice tip %}}
+Tip: As you continue your Python journey, try using a debugger, like the built-in `pdb` instead of the `print()` function to really dive into what your code is doing under the hood.
+{{% /notice %}}
+
+
+### Output Formatting Tips
+
+If your Python program will have terminal output, you can use these tips to make it a little nicer.
+
+#### Use new lines and tabs
+
+
+Use control characters in your string to change how your output is represented.
+
+* `\n` for new line
+* `\t` for tab
+
+In `formatting_example.py`:
+
+```python
+# Use \n to add a new line, before, in the middle of, or after a string.
+print("\nExtra New Line Before")
+print("One Print\nTwo New Lines!")
+print("Extra New Line After\n")
+
+# Use \t to add a tab.
+print("\t Here's some tabbed output.")
+
+# Or, combine both!
+print("\nOne Print\n\tOne Tab")
+```
+
+Output running: `python3 formatting_example.py`.
+
+```text
+Extra New Line Before
+One Print
+Two New Lines!
+Extra New Line After
+
+ Here's some tabbed output.
+
+One Print
+ One Tab
+```
+
+#### Pretty Printing with `pprint`
+
+When printing large data structures like long lists or big dictionaries, they come out on one line. It's a bit hard to read.
+
+If you want a little bit of extra formatting, like having each element of a long list on a new line, you can use the included `pprint` module (stands for pretty print) in your files or in the REPL.
+
+```python
+>>> long_list = list(range(23))
+
+>>> print(long_list)
+[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
+
+>>> from pprint import pprint
+>>> pprint(long_list)
+[0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ 22]
+```
+
+{{% notice tip %}}
+This will become more useful as your Python programs become more complex.
+{{% /notice %}}
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/175-running-code/30-the-main-method.md b/website/content/02-introduction-to-python/175-running-code/30-the-main-method.md
new file mode 100644
index 0000000..c441a8d
--- /dev/null
+++ b/website/content/02-introduction-to-python/175-running-code/30-the-main-method.md
@@ -0,0 +1,144 @@
+---
+title: "The main Method"
+date: 2019-02-10T18:16:47-08:00
+draft: false
+weight: 30
+---
+
+Once you start writing more comprehensive Python programs, you'll want to include a `main` method in your code.
+
+The purpose of checking for the `main` method is to make sure that the code in your `main` method is only run when it's executed as a stand-alone program. Because of how Python's import system works, if someone else imports your Python program, any code in it is executed on import.
+
+We'll talk more about imports and modules on day two, but let's look at a quick example.
+
+Let's say we had a Python file named `name_lib.py`
+
+```python
+def name_length(name):
+ return len(name)
+
+def upper_case_name(name):
+ return name.upper()
+
+def lower_case_name(name):
+ return name.lower()
+
+name = "Nina"
+length = name_length(name)
+upper_case = upper_case_name(name)
+
+print(f"The length is {length} and the uppercase version is: {upper_case}")
+```
+
+If we ran this code, we'd see exactly what we expect.
+
+```bash
+(env) $ python name_lib.py
+name_lib.py
+The length is 4 and the uppercase version is: NINA
+```
+
+### Writing Reusable Code
+
+We went through all this trouble of writing a useful name library. What if someone else wanted to use our library in their own code by `import`ing it?
+
+Let's say someone else wrote their own program, in `other_program.py`
+
+```python
+import name_lib
+
+my_name = "Fred"
+
+my_length = name_lib.name_length(my_name)
+my_lower_case = name_lib.lower_case_name(my_name)
+
+print(f"In my code, my length is {my_length} and my lower case name is: {my_lower_case}")
+```
+
+If we ran this, we'd see the following result:
+
+```bash
+(env) $ python other_program.py
+
+The length is 4 and the uppercase version is: NINA
+In my code, my length is 4 and my lower case name is: fred
+```
+
+If I'm Fred, I just wanted to use this cool library. But all of a sudden, I'm seeing information about Nina!
+
+To prevent this from happening, we want to write a conditional that will only run *our* code if we're the ones running the program directly.
+
+To do that, we'll need to check if `__name__ == __main__`.
+
+Let's unwrap that.
+
+`__name__` is a special variable that's set by Python that tells it where it was called from. We can tell it's a special variable because it starts and ends with `__`. That's a hint that you don't want to *change* the value of this variable, or it could adversely affect the execution of your Python program.
+
+{{% notice tip %}}
+In Python, `__` is also called double underscore, or *dunder*.
+{{% /notice %}}
+
+Let's comment out our original `print`, and add the following line to the end of `name_lib.py`:
+
+```python
+# Note: add to the bottom of name_lib.py
+
+# print(f"The length is {length} and the uppercase version is: {upper_case}")
+print(f"The value of __name__ is: {__name__}")
+```
+
+Now, let's run `name_lib.py` again. We should see:
+
+```bash
+(env) $ python name_lib.py
+The value of __name__ is: __main__
+```
+
+We're getting somewhere. When we run this file directly, we'll see that `__name__` has the *value* of `__main__`.
+
+What if we run our other program again?
+
+```bash
+(env) $ python other_program.py
+The value of __name__ is: name_lib
+```
+
+When we "run" our library by importing it, we'll see that it's `__name__` is set to the name of the file that it's in, minus the `.py` extension. In this case, `__name__` is set to `name_lib`.
+
+### Putting Code in a `main` Conditional
+
+To avoid running our code when it's imported by other modules, we put it in a conditional statement, and explicitly check `if __name__ == "__main__"`.
+
+Let's update `name_lib.py`, and put our own code inside of the conditional check.
+
+```python
+def name_length(name):
+ return len(name)
+
+def upper_case_name(name):
+ return name.upper()
+
+def lower_case_name(name):
+ return name.lower()
+
+if __name__ == "__main__":
+ name = "Nina"
+ length = name_length(name)
+ upper_case = upper_case_name(name)
+
+ print(f"The length is {length} and the uppercase version is: {upper_case}")
+```
+
+Now, if we run `other_program.py`, we'll see:
+
+```bash
+(env) $ python other_program.py
+In my code, my length is 4 and my lower case name is: fred
+```
+
+Much better!
+
+{{% notice note %}}
+Using a `main` method is a common pattern that you’ll see in Python programs, and it comes in handy for being able to write programs that work both on their own and when imported into other programs.
+{{% /notice %}}
+
diff --git a/website/content/02-introduction-to-python/175-running-code/50-exceptions.md b/website/content/02-introduction-to-python/175-running-code/50-exceptions.md
new file mode 100644
index 0000000..d290ab6
--- /dev/null
+++ b/website/content/02-introduction-to-python/175-running-code/50-exceptions.md
@@ -0,0 +1,123 @@
++++
+title = "Exceptions and Tracebacks"
+date = 2019-01-25T15:03:40-06:00
+weight = 50
+draft = false
++++
+
+We talked about how to read the traceback you see if an error occurs in a Python program earlier in the day, but let's talk about what we can do about it.
+
+### Uncaught Exceptions Exit Our Program
+
+Let's make a file called `exceptions.py`:
+
+```python
+# this will throw an exception!
+int("a")
+
+print("End of the program.")
+```
+
+And run it:
+```bash
+(env) $ python exceptions.py
+
+Traceback (most recent call last):
+ File "/Users/nina/projects/2019-fem-python/python/content/02-introduction-to-python/175-running-code/code/exceptions.py", line 2, in
+ int("a")
+ValueError: invalid literal for int() with base 10: 'a'
+```
+
+We'll see that "Reached end of the program" was never printed out.
+
+{{% notice warning %}}
+If we're running our Python code from a file, an uncaught exception will quit the program.
+{{% /notice %}}
+
+### Using `try` and `except` to catch Exceptions
+
+In order to prevent our program from exiting, we'll need to *catch* the Exception with a `try` `except` block. The anatomy of a `try` `except` block:
+
+```text
+try:
+
+except ExceptionClass:
+
+```
+
+In order to write a `try` `except` block, we'll need to know the class name of the Exception we'd like to catch. Luckily, the name is printed right there, in the traceback!
+
+Let's update `exceptions.py`
+
+```python
+try:
+ int("a")
+except ValueError:
+ print("Oops, couldn't convert that value into an int!")
+
+print("Reached end of the program.")
+```
+
+And the output:
+
+```bash
+(env) $ python exceptions.py
+Oops, couldn't convert that value into an int!
+Reached end of the program.
+```
+
+{{% notice tip %}}
+You want to catch Exceptions that are as specific as possible.
+{{% /notice %}}
+
+### Using `as` to Access The Exception
+
+You can optionally assign a label to the exception, and the exception will be assigned to the variable you specified, so you can look at it's message, or examine it in other ways with `except as `.
+
+Using this syntax, `variable_name` can be anything. In this case, I picked `error`, but you'll commonly see `e` used for this purpose in Python programs.
+
+Let's update `exceptions.py` one more time:
+
+```python
+try:
+ int("a")
+except ValueError as error:
+ print(f"Something went wrong. Message: {error}")
+
+print("Reached end of the program.")
+```
+
+If we run this code, we'll see:
+
+```bash
+(env) $ python exceptions.py
+Something went wrong. Message: invalid literal for int() with base 10: 'a'
+Reached end of the program.
+```
+
+{{% notice note %}}
+This example just scratches the surface. We'll cover Exceptions in much more detail in Day 2.
+{{% /notice %}}
+
+### Anatomy of a Traceback
+
+When running our code from Python files, we'll need to look at your tracebacks a little more carefully.
+
+{{% notice tip %}}
+Remember, to understand tracebacks, read them from bottom to top.
+{{% /notice %}}
+
+When we ran our first example, we saw:
+
+```text
+Traceback (most recent call last):
+ File "/Users/nina/projects/2019-fem-python/python/content/02-introduction-to-python/175-running-code/code/exceptions.py", line 2, in
+ int("a")
+ValueError: invalid literal for int() with base 10: 'a'
+```
+
+If we start reading from the bottom up, we'll notice a lot of useful information.
+
+1. First, we'll see the exception that was thrown, along with its class `ValueError`.
+2. Next, we'll see the code that caused the Exception.
+3. One line up from that, we'll see the path and the file the exception originated in, as well as the **line number** to look on.
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/175-running-code/60-working-with-files.md b/website/content/02-introduction-to-python/175-running-code/60-working-with-files.md
new file mode 100644
index 0000000..f9a15d0
--- /dev/null
+++ b/website/content/02-introduction-to-python/175-running-code/60-working-with-files.md
@@ -0,0 +1,61 @@
++++
+title = "Working With Files"
+date = 2019-01-25T15:06:17-06:00
+weight = 60
+draft = false
++++
+
+Python provides a built-in function for opening files, cleverly titled `open()`. The `open()` method will return an object that you can `read()` to get the data. By default, `open()` will open a file in read-only mode, however you can change this by passing a mode parameter. The list of optional modes is here:
+
+|Character|Meaning|
+|---|---|
+|'r'|open for reading (default)|
+|'w'|open for writing, truncating the file first|
+|'x'|open for exclusive creation, failing if the file already exists|
+|'a'|open for writing, appending to the end of the file if it exists|
+|'b'|binary mode|
+|'t'|text mode (default)|
+|'+'|open a disk file for updating (reading and writing)|
+
+Opening a file would look something like this:
+
+```python
+# Open a file for reading
+>>> my_file = open("my_file.txt")
+
+# Open a file for reading or writing
+# This will replace any existing file
+>>> my_file = open("my_file.txt", "w")
+
+# Open a file for reading or writing
+# This will append to the end of any existing file
+>>> my_file = open("my_file.txt", "a")
+```
+
+Of course, you always want to call `close()` on your open file object once you're done with it, so that your program doesn't leave open file handles dangling. But what happens if your program exits or crashes before you can close a file? Use a context manager!
+
+
+### Context Managers
+
+Briefly, a Context Manager is like a wrapper around a block of code that depends on some resource. It's a safer way of handling resources than, say, using `open()` and then remembering to `close()` later (and hoping your program doesn't crash in between). It's similar to using `try... finally`, but cleaner to look at. Context managers can contain code that auto-magically provisions a resource before your code runs, and cleans up afterward. For example, the `open()` function also works as a context manager, so opening a file looks like this:
+
+```python
+>>> with open("my_file.text") as my_file:
+... contents = my_file.read()
+```
+
+You can perform all the functions that require access to the open file within the `with` scope, and once you exit the `with` scope, the context manager will automatically close your file for you, even if your code hits an exception inside the context manager.
+
+
+### Working with Files
+
+Let's try working with a simple json file. Download this [`cities.json` file](/code/cities.json), which contains a listing of the top five cities in the US by population. We'll import the `json` module to help us parse the data, and we'll use a context manager to open the file.
+
+```python
+>>> import json
+>>> with open("cities.json") as cities_file:
+... cities_data = json.load(cities_file)
+... print(cities_data)
+...
+[{'name': 'New York', 'pop': 8550405}, {'name': 'Los Angeles', 'pop': 3971883}, {'name': 'Chicago', 'pop': 2720546}, {'name': 'Houston', 'pop': 2296224}, {'name': 'Philadelphia', 'pop': 1567442}]
+```
\ No newline at end of file
diff --git a/website/content/02-introduction-to-python/175-running-code/65-libraries.md b/website/content/02-introduction-to-python/175-running-code/65-libraries.md
new file mode 100644
index 0000000..88791af
--- /dev/null
+++ b/website/content/02-introduction-to-python/175-running-code/65-libraries.md
@@ -0,0 +1,37 @@
++++
+title = "Working with Libraries"
+date = 2019-01-25T15:06:06-06:00
+weight = 65
+draft = false
++++
+
+Working with external libraries in Python makes use of the `import` keyword. While this can go anywhere in your file, it's almost always best to import libraries at the top of each file where they're used. For example, in the last section, we were able to call upon the built-in `json` library by calling `import json` at the top of our code.
+
+Importing modules with the `import` keyword is usually the best method, because it preserves the module's namespace. However, you can also use the `from import