diff --git a/PythonProgrammingBeyondTheBasics.ipynb b/PythonProgrammingBeyondTheBasics.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..69847a7a115ab69e394603faa798cbb157d81df0
--- /dev/null
+++ b/PythonProgrammingBeyondTheBasics.ipynb
@@ -0,0 +1,3108 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "dc37fc5f-9426-423a-b34f-7556122fca5a",
+   "metadata": {},
+   "source": [
+    "# Python programming beyond the basics\n",
+    "\n",
+    "In \"Intro Programming in Python\" the minimal basics of programming were introduced. This course goes beyond those basics and will introduce more abstractions that can be useful in describing your computations in the most clear way to the reader of your code. The programming language and the abstractions it provides are not designed for computers, but for humans. The only thing the computer wants are numbers that represent instructions that it knows how to perform. Its the reader of the code that finds the numbers hard to reason about. It needs the abstractions to be able to deal with the complexity of the problems solved with computations."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "713ecba6-8769-466c-99ec-dd5a71f74ffa",
+   "metadata": {},
+   "source": [
+    "## The basics (refresher)\n",
+    "\n",
+    "https://git.astron.nl/klazema/intro-programming-in-python/\n",
+    "\n",
+    "The basics include: Variables, Numbers, Strings, Functions, Math, compound data types, and flow control."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a1e7f682-95aa-4fe6-a505-fff35615497f",
+   "metadata": {},
+   "source": [
+    "### Variables\n",
+    "\n",
+    "Variables are labels that point to a value."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "06f63bc6-41c2-4013-944f-ef05eb3449f6",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "age = 41\n",
+    "age"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8ae9b102-c6dd-4fcc-904e-56013633b8b4",
+   "metadata": {},
+   "source": [
+    "### Numbers\n",
+    "\n",
+    "Numbers are one of the basic data types. Python makes a distinction between different types of numbers. Namely: integers, floats, complex, Decimal, and Fraction."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "35bf9bf7-8e61-46d5-95e2-84d18df98e3c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from decimal import Decimal\n",
+    "from fractions import Fraction\n",
+    "\n",
+    "1              # integer\n",
+    "1.0            # float\n",
+    "1+0J           # complex\n",
+    "Decimal(1.0)   # Decimal\n",
+    "Fraction(1, 1) # Fraction"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "bcdd291e-a01c-42bf-ac0f-c49129be9a30",
+   "metadata": {},
+   "source": [
+    "### Strings\n",
+    "\n",
+    "Strings are an immutable sequence of Unicode characters."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "2846246d-a2ab-4a99-88c6-5ed4016df299",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "\"a\"\n",
+    "\"astron\"\n",
+    "'a'\n",
+    "'Astron'\n",
+    "'''Astron\n",
+    "Jive\n",
+    "Nova'''\n",
+    "\" \".join((\"Astron\", \"Jive\", \"Nova\")) \n",
+    "\"Astron\"[0:3]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3e3dceca-1b80-41b2-8b7a-7a60916d7a3e",
+   "metadata": {},
+   "source": [
+    "### Functions\n",
+    "\n",
+    "Functions is one of the basic forms of abstractions together with variables and compound data types.\n",
+    "\n",
+    "It allows the programmer to combine basic functions into more complex computations. And with it to abstract away the details, hidden behind an intuitive name."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "ba4162d3-53b5-4a35-ad45-552074436d54",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Hello Y'all\n",
+      "*** Hello Y'all ***\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\"Hello Y'all\")\n",
+    "\n",
+    "def print_with_stars(words):\n",
+    "    print(f\"*** {words} ***\")\n",
+    "\n",
+    "print_with_stars(\"Hello Y'all\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "63400031-fc56-4719-9061-8a371ee5fa9c",
+   "metadata": {},
+   "source": [
+    "#### Scope\n",
+    "\n",
+    "Scope has to deal with locality of variables and functions. Each block defines its on environment. When python needs to search for the value of a variable it will look int the current block and will look upwards when it could not find the variable. The same goes for functions."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d84c5942-16e0-4363-9b13-6eae26893699",
+   "metadata": {},
+   "source": [
+    "### Math\n",
+    "\n",
+    "The basic math operations allows us to perform many types of calculations needed in solutions to a lot of problems. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "8ff0d148-d489-4232-b66c-32a0466c2b2f",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "10"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "1 * 2\n",
+    "3 + 4\n",
+    "7 - 8\n",
+    "4 / 5\n",
+    "9 // 4\n",
+    "10 % 3\n",
+    "2 ** 2\n",
+    "6 - (9 * 3)\n",
+    "abs(-10)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a31fe015-4395-4a0e-8564-8df7a829c85a",
+   "metadata": {},
+   "source": [
+    "### Compound data types\n",
+    "\n",
+    "Lists, sets, tuples, and dictionaries are the basic compound data types. They can hold multiple values in their structure."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3f5985b6-d7e2-4e2f-a8aa-7a954bfaeb50",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "[1, 2, 3] # list\n",
+    "(1, 2, 3) # tuple\n",
+    "{1, 2, 3} # set\n",
+    "{\"one\": 1, \n",
+    " \"two\": 2, \n",
+    " \"three\": 3} # dictionary"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1d0b3d3d-9245-43ce-96dd-8336d8e5e062",
+   "metadata": {},
+   "source": [
+    "### Flow control\n",
+    "\n",
+    "With flow control we can make different decisions based on values with it we can also loop over sequences."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "13d2b8d4-b71d-470d-bdda-02f3d52c8ae2",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "1 is Smaller than or equal to 3\n",
+      "* H ** e ** l ** l ** o **   ** W ** o ** r ** l ** d *\n",
+      "Whats for dinner tonight?\n",
+      "Whats for dinner tonight?\n",
+      "Whats for dinner tonight?\n"
+     ]
+    }
+   ],
+   "source": [
+    "a = 1\n",
+    "b = 3\n",
+    "\n",
+    "if a > b:\n",
+    "    print(f\"{a} is Bigger than {b}\")\n",
+    "else:\n",
+    "    print(f\"{a} is Smaller than or equal to {b}\")\n",
+    "\n",
+    "c = \"Hello World\"\n",
+    "    \n",
+    "for item in c:\n",
+    "    print(f\"* {item} *\", end=\"\")\n",
+    "    \n",
+    "print(\"\")\n",
+    "    \n",
+    "while a <= b:\n",
+    "    print(\"Whats for dinner tonight?\")\n",
+    "    a += 1"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "26b72532-6df8-43c2-9f36-3d41e816b34e",
+   "metadata": {},
+   "source": [
+    "That was a refresher of the basics. Lets dive a bit deeper into Python to see what it offers beyond the basics."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "c282791f-b902-4a6e-bab4-5ac96ef39397",
+   "metadata": {},
+   "source": [
+    "## List comprehensions\n",
+    "https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions\n",
+    "\n",
+    "### Mapping\n",
+    "\n",
+    "A pattern that is often seen if looping of a list and do something with the items. Some of the patterns can be done with the list comprehension abstraction / syntax.\n",
+    "\n",
+    "It looks like control flow inside a list."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "5e3f4ead-53e1-47f9-b03c-6a39656e5e6e",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[1, 4, 9, 16, 25, 36, 49, 64, 81]"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Here we take the items from a list and perform a calculation on them\n",
+    "items = [1, 2, 3, 4, 5, 6, 7, 8, 9]\n",
+    "\n",
+    "result = []\n",
+    "for item in items:\n",
+    "    result.append(item * item)\n",
+    "\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "id": "e9b76323-0b12-45ef-bdf2-779c8ebed580",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[1, 4, 9, 16, 25, 36, 49, 64, 81]"
+      ]
+     },
+     "execution_count": 18,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# This can be done with a list comprehension in one line\n",
+    "numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]\n",
+    "\n",
+    "result = [number * number for number in numbers]\n",
+    "\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1af32ad3-5f91-48e9-a2ad-0abcefaa9052",
+   "metadata": {},
+   "source": [
+    "The syntax starts with an opening bracket followed by an expression. After the expression you have a for clause that can be followed with zero or more for or if clauses. The whole statement is closed with a closing bracket.\n",
+    "\n",
+    "The for loop show on what needs to be iterated."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b1c75db1-2a53-4132-aef0-32578f7e2aa4",
+   "metadata": {},
+   "source": [
+    "An alternative for applying operations on items of a list is by using the map function."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "id": "31762c5e-c22f-4c8c-8123-f8724167d9fe",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[1, 4, 9, 16, 25, 36, 49, 64, 81]"
+      ]
+     },
+     "execution_count": 13,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# map comes from the functional programming style and is less seen in python code than the list comprehension\n",
+    "items = [1, 2, 3, 4, 5, 6, 7, 8, 9]\n",
+    "\n",
+    "def square(number):\n",
+    "    return number * number\n",
+    "\n",
+    "result = map(square, items)\n",
+    "\n",
+    "list(result)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3a58e52f-f424-46ee-953f-4bc5027ed903",
+   "metadata": {},
+   "source": [
+    "Most of the time you will make use of the item(s) in the for loop, but its not a requirement. But since that use case is less seen you should be careful with it and maybe you should use another construction to make your intention more clear."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "id": "accac7e4-55c3-4e70-8c89-884d0a34f296",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[True, True, True, True, True, True, True, True, True]"
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# this results in a list with all Trues with the length of the list.\n",
+    "items = [1, 2, 3, 4, 5, 6, 7, 8, 9]\n",
+    "\n",
+    "result = [True for _ in items]\n",
+    "\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0b23f5f5-6d64-4d72-b517-50c4b92ee43d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# this gives the same result but might describe your intention a bit better\n",
+    "items = [1, 2, 3, 4, 5, 6, 7, 8, 9]\n",
+    "\n",
+    "result = [True] * len(items)\n",
+    "\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "87c9db7d-6ed1-4292-8911-454406fec8cd",
+   "metadata": {},
+   "source": [
+    "#### Exercise\n",
+    "\n",
+    "Create a new list where you calculate the cube ($b^3$) of all numbers in a sequence."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b8c5c878-cce7-46a5-8a00-23d8c44c9d7a",
+   "metadata": {},
+   "source": [
+    "### Filtering\n",
+    "\n",
+    "Another use of list comprehensions can be for filtering."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "id": "d649bc4e-022f-4655-8e0a-eaaa007961c4",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[2, 4, 6, 8]"
+      ]
+     },
+     "execution_count": 19,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Here we filter out items from the list\n",
+    "items = [1, 2, 3, 4, 5, 6, 7, 8, 9]\n",
+    "\n",
+    "def is_even(number):\n",
+    "    return number % 2 == 0\n",
+    "\n",
+    "result = []\n",
+    "for item in items:\n",
+    "    if is_even(item):\n",
+    "        result.append(item)\n",
+    "\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 24,
+   "id": "72f510a5-8f37-414c-9586-a3dd01ab5fd1",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[2, 4, 6, 8]\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "[2, 4, 6, 8]"
+      ]
+     },
+     "execution_count": 24,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# This filtering can be done with a list comprehension in one line\n",
+    "items = [1, 2, 3, 4, 5, 6, 7, 8, 9]\n",
+    "\n",
+    "def is_even(number):\n",
+    "    return number % 2 == 0\n",
+    "\n",
+    "result = [x for x in items if is_even(x)]\n",
+    "\n",
+    "result_v2 = [x for x in items if x % 2 == 0] # More difficult to read\n",
+    "\n",
+    "print(result_v2)\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8144db6a-f619-4e83-8340-f9690460315d",
+   "metadata": {},
+   "source": [
+    "For filtering python also provides the filter function that does almost the same thing. It also comes for the function programming paradigm. It is also less popular than the list comprehension."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
+   "id": "1826bc98-a90d-4d6a-92b7-1751e8f2c18a",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[2, 4, 6, 8]"
+      ]
+     },
+     "execution_count": 21,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# This filtering can also done with the filter function\n",
+    "items = [1, 2, 3, 4, 5, 6, 7, 8, 9]\n",
+    "\n",
+    "def is_even(number):\n",
+    "    return number % 2 == 0\n",
+    "\n",
+    "result = filter(is_even, items)\n",
+    "\n",
+    "list(result)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "cba9d183-70c3-4a4a-aacc-dd0705e94113",
+   "metadata": {},
+   "source": [
+    "#### Exercise\n",
+    "\n",
+    "Filter a sequence such that all multiples of 3 are kept. Remember a range is also a sequence, this might save some typing ;-) ."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "f4c4e33e-69ed-4cd3-9e56-f6d305f64f18",
+   "metadata": {},
+   "source": [
+    "You can also combine filtering with mapping and also nest multiple sequence loops."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 25,
+   "id": "85b826ea-8f84-4f67-9c8b-0e70378db6f8",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[(4, 1),\n",
+       " (4, 9),\n",
+       " (4, 25),\n",
+       " (4, 49),\n",
+       " (4, 81),\n",
+       " (16, 1),\n",
+       " (16, 9),\n",
+       " (16, 25),\n",
+       " (16, 49),\n",
+       " (16, 81),\n",
+       " (36, 1),\n",
+       " (36, 9),\n",
+       " (36, 25),\n",
+       " (36, 49),\n",
+       " (36, 81),\n",
+       " (64, 1),\n",
+       " (64, 9),\n",
+       " (64, 25),\n",
+       " (64, 49),\n",
+       " (64, 81)]"
+      ]
+     },
+     "execution_count": 25,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "items = [1, 2, 3, 4, 5, 6, 7, 8, 9]\n",
+    "\n",
+    "def square(number):\n",
+    "    return number * number\n",
+    "\n",
+    "def is_even(number):\n",
+    "    return number % 2 == 0\n",
+    "\n",
+    "def is_odd(number):\n",
+    "    return not is_even(number)\n",
+    "\n",
+    "result = [(square(x), square(y)) for x in items if is_even(x) \n",
+    "                                 for y in items if is_odd(y)]\n",
+    "\n",
+    "result"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "dc480af9-fd0e-4076-912c-5ac18838e0a7",
+   "metadata": {},
+   "source": [
+    "List comprehensions can also be done with sets and dictionaries, but not with tuples. They are then called set comprehensions and dictionary comprehensions."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "43dc59eb-7324-41e0-bd78-02916ca3c41e",
+   "metadata": {},
+   "source": [
+    "## Generators\n",
+    "\n",
+    "https://docs.python.org/3/tutorial/classes.html#generators\n",
+    "\n",
+    "Generators create iterators with the use of a yield statement. So why are they useful? They can save some code. They can save memory. Lets have a look at how a generator is created."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 34,
+   "id": "43591070-db4a-45e2-ae16-594cce7b1de6",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "33\n",
+      "43\n",
+      "53\n",
+      "63\n",
+      "1\n",
+      "2\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "range(2, 11)"
+      ]
+     },
+     "execution_count": 34,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "def my_range(start, end, step=1):\n",
+    "    n = start\n",
+    "    while n < end:\n",
+    "        yield n\n",
+    "        n += step\n",
+    "\n",
+    "for item in my_range(33, 66, 10):\n",
+    "    print(item)\n",
+    "\n",
+    "# Don't use this in your code if a for loop or while loop will do the trick\n",
+    "iterator = my_range(1, 3)\n",
+    "\n",
+    "print(next(iterator))\n",
+    "print(next(iterator))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "25a2813c-d7a5-4684-a278-11530996c68a",
+   "metadata": {},
+   "source": [
+    "Why is it saving memory space? Well, because it only returns one value until the next method is called on the iterator. We could create a range generator that only had a start and a step parameter. It would go on forever. If we did this with a list we would need infinite memory."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "01f8d691-9859-490b-932d-019d6895c146",
+   "metadata": {},
+   "source": [
+    "Why is it saving code? Well, because the alternative is to create a class based generator. It requires a new class with the `__iter__` and `__next__` magic methods implemented. This results in around three times more lines of code. And to use it we need to instantiate an object."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "150c02e4-76bc-464d-9d0c-d9b767c8e3d0",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "\n",
+    "Create a generator that generates an infinite sequence of odd numbers starting from 1 to 100."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d30a52ce-c724-4680-af77-94a964ad9cb3",
+   "metadata": {},
+   "source": [
+    "## Decorators\n",
+    "\n",
+    "Decorators are a design pattern where the function that gets decorated gets wrapped by another function. The syntax uses the @ sign followed by the name of a decorator wrapper function."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ecb8609a-f230-4b00-aa01-f5787b986b79",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import time\n",
+    "\n",
+    "def timeit(func):\n",
+    "    def wrapper(*args, **kwargs):\n",
+    "        start = time.time()\n",
+    "        \n",
+    "        result = func(*args, **kwargs)\n",
+    "        \n",
+    "        stop = time.time()\n",
+    "        \n",
+    "        print(f\"Total time used by {func.__name__}: {stop - start}\")\n",
+    "        \n",
+    "        return result\n",
+    "        \n",
+    "    return wrapper\n",
+    "\n",
+    "@timeit\n",
+    "def calculate(end):\n",
+    "    return sum(range(1, end))\n",
+    "\n",
+    "def calc(end):\n",
+    "    return sum(range(1, end))\n",
+    "\n",
+    "end = 10**8\n",
+    "\n",
+    "print(f\"The sum of 1 to {end}: {calculate(end)}\")\n",
+    "print(calculate)\n",
+    "print(calc)\n",
+    "\n",
+    "# This is what decorator does for you\n",
+    "calc = timeit(calc)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 45,
+   "id": "05e3cd46-57a6-4b68-a8c1-62189a7ae120",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "10 times 2 is 20\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "20"
+      ]
+     },
+     "execution_count": 45,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "def type_check(allowed_type):\n",
+    "    def check(func):\n",
+    "        def wrapper(arg):\n",
+    "            if(isinstance(arg, allowed_type)):\n",
+    "                return func(arg)\n",
+    "            else:\n",
+    "                raise ValueError(\"Wrong type\")\n",
+    "                \n",
+    "        return wrapper\n",
+    "    \n",
+    "    return check\n",
+    "\n",
+    "@type_check(int)\n",
+    "def times_two(number):\n",
+    "    return number * 2\n",
+    "\n",
+    "number = 10\n",
+    "print(f\"{number} times 2 is {times_two(number)}\")\n",
+    "\n",
+    "name = \"Astron\"\n",
+    "\n",
+    "# print(f\"{name} times 2 is {times_two(name)}\")\n",
+    "\n",
+    "# What the decorator does for you\n",
+    "times_two = type_check(int)(times_two)\n",
+    "\n",
+    "times_two(10)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "f9da4464-bf66-45a2-a05e-1b0b9b4bc73a",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "\n",
+    "Create a decorator that keeps track of and prints all arguments used on the decorated function."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3371feff-b116-4ca9-bd66-7ce5ae40eafb",
+   "metadata": {},
+   "source": [
+    "So the timeit function gets a function as its parameter. It takes that function and uses it in a wrapper function. That wrapper function gets returned. It replaces the value the original function name with the wrapped function."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ac2b0e8a-27be-4d46-ba47-063580ee5238",
+   "metadata": {},
+   "source": [
+    "Decorators are really powerful, but remember that your goal is to make your intentions and solution as clear as possible. So if you start to mess with the original behavior you make things less clear. Extending the behavior generally does not cause any confusion."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5cf18495-af6f-4b58-a9d4-0383909bea4c",
+   "metadata": {},
+   "source": [
+    "## Modules\n",
+    "\n",
+    "https://docs.python.org/3/reference/simple_stmts.html#the-import-statement\n",
+    "\n",
+    "A module holds code and has its own namespace. Modules are used to organize code. To access code from another module a module most use the import system. The import statement is one way of importing code and is the most used."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d2a36df1-56c5-4f18-a752-e5bd58a0af61",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# NOTE restart kernel to see the correct dir()\n",
+    "import os\n",
+    "\n",
+    "print(dir())\n",
+    "print(os.getcwd())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "daff8a07-f966-44f3-baa1-97a9b52ea08b",
+   "metadata": {},
+   "source": [
+    "If for some reason you have a name clash with the import you can rename the name of the module in your namespace by appending \"as\" and a different name."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "24b3b951-6351-4930-b180-c7e99d75bc7b",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "The name os is taken\n",
+      "/home/klazema/src/python-programming-beyond-the-basics\n"
+     ]
+    }
+   ],
+   "source": [
+    "# NOTE restart kernel to see the correct dir()\n",
+    "os = \"taken\"\n",
+    "\n",
+    "# import os\n",
+    "import os as os_module\n",
+    "\n",
+    "print(f\"The name os is {os}\")\n",
+    "print(os_module.getcwd())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "405879fc-6c68-4143-aacd-21cc22c89a22",
+   "metadata": {},
+   "source": [
+    "Please use the renaming of the module sparingly especially with popular packages. People will know / recognize the module and are confused when it has been renamed. Its usually better to rename the other variable."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "39c53b8e-ff9f-4e36-ac66-0d33653e825e",
+   "metadata": {},
+   "source": [
+    "Another way to import a more limited set of a module is to import with the from \"module_name\" import \"module item\". Multiple items can be imported this way by separating the items with a comma. This way of importing makes the dependency on the module more clear. The \"as\" option is also available here. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e27f1655-7427-489b-b737-7f2e3b46c6c0",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# NOTE restart kernel to see the correct dir()\n",
+    "from os import getcwd\n",
+    "\n",
+    "print(dir())\n",
+    "print(getcwd())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a2cc8993-4090-4faf-9990-671dd2bcb6b5",
+   "metadata": {},
+   "source": [
+    "The from - import method has one downside and that is that the imported items become available in the modules namespace directly. But since this is done explicitly this is usually not a problem. There is another option that can cause problems with namespace clashes. And that is the star import option. This way all items get imported and since modules can change it might import new / unknown items that can clash with your own module but more likely with other modules imported this way."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "782fd2a3-e0a5-42dc-9b38-99c11ec562f7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# NOTE restart kernel to see the correct dir()\n",
+    "from os import *\n",
+    "\n",
+    "print(dir())\n",
+    "print(getcwd())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7ec44399-d4ac-4850-ad3e-2962ee03b5f3",
+   "metadata": {},
+   "source": [
+    "There is one agreement in the Python world and that is that we don't play with the names starting with an underscore. The convention is that those are not part of the public API of the module. You can access it, modify it and abuse it, but the author of the module will not guarantee the stability of those names and associated behavior. They might have different behavior or disappear altogether. The public, non underscore names, should be more stable. When you are building a module make sure that non public names start with an underscore. This is usually not needed for a script type of program. For libraries and larger program you should think about what should be public and what should be for internal usage only."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8e79720a-94f0-4ddc-8862-fb38cf346409",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "\n",
+    "Do some imports and see how the global namespace changes. Try to import some non existing modules and non existing names in modules."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d363c88e-1ea3-4422-b824-52278e98c6eb",
+   "metadata": {},
+   "source": [
+    "## Exceptions\n",
+    "\n",
+    "https://docs.python.org/3/reference/executionmodel.html#exceptions\n",
+    "https://docs.python.org/3/library/exceptions.html\n",
+    "\n",
+    "Exceptions is used to indicate errors in the program. Maybe some code is used incorrectly, or a user provided incorrect input. And if the issue can't be handled by the code an exception should be raised.\n",
+    "\n",
+    "In order to generate an exception the raise statement needs to be used."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "893f4669-5f6b-428d-980c-124dc1e011d1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def error():\n",
+    "    raise Exception(\"Oh ooh!\")\n",
+    "    \n",
+    "error()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "feaa50f6-67c5-481b-a1e9-84e39b921fa2",
+   "metadata": {},
+   "source": [
+    "If you think you can fix the issue or you at least can deal with the exception you can handle the exception with an try ... except statement."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "id": "3ae1d2d0-f0a9-4d7d-9690-0571f3993722",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def error():\n",
+    "    raise ValueError(\"Oh ooh!\")\n",
+    "\n",
+    "try:\n",
+    "    error()\n",
+    "except ValueError:\n",
+    "    pass"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "e6c911f8-c330-46a0-943e-02393ae6ed0a",
+   "metadata": {},
+   "source": [
+    "Whenever possible try handle only the most specific exception. All exceptions tend to live in a hierarchy with Exception as the highest. So by using Exception you would handle all issues and usually this is not wise. There might be an issue that you can't handle and/or should not ignore. Also don't be afraid to create your own specific exception class to make it even more specific."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "cb3a4e19-a21c-41ba-a7a6-1c216ca5e0a8",
+   "metadata": {},
+   "source": [
+    "Sometimes you want log the issue but still keep the exception moving up the calling trace. This can be done by re-raising the exception."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "299d54c1-57b6-49f9-97d3-b8510f21d561",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def log_exception(exception):\n",
+    "    print(f\"Logging an exception: {exception}\")\n",
+    "\n",
+    "def error():\n",
+    "    raise ValueError(\"Oh ooh!\")\n",
+    "    \n",
+    "try:\n",
+    "    error()\n",
+    "except ValueError as e:\n",
+    "    log_exception(e)\n",
+    "    raise"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "e6a02a6f-c802-4cf5-afed-e35bf11a640d",
+   "metadata": {},
+   "source": [
+    "Sometimes you need to take action even if you get an exception. Thats where the statement `finally` comes into play. It follows the try and except statements if there are any. No matter is there was an exception or not the code in the finally block will be run. If the exception is not caught it will still propagate higher up."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "id": "6ad9f1d1-81a9-4071-8e8a-be6c9f624b96",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "What just happend?\n"
+     ]
+    },
+    {
+     "ename": "ValueError",
+     "evalue": "Boom",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[0;31mValueError\u001b[0m                                Traceback (most recent call last)",
+      "\u001b[0;32m<ipython-input-16-f132b7014204>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      3\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      4\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m     \u001b[0merror\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      6\u001b[0m \u001b[0;32mfinally\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      7\u001b[0m     \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"What just happend?\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m<ipython-input-16-f132b7014204>\u001b[0m in \u001b[0;36merror\u001b[0;34m()\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0merror\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m     \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Boom\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      3\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      4\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      5\u001b[0m     \u001b[0merror\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;31mValueError\u001b[0m: Boom"
+     ]
+    }
+   ],
+   "source": [
+    "def error():\n",
+    "    raise ValueError(\"Boom\")\n",
+    "    \n",
+    "try:\n",
+    "    error()\n",
+    "finally:\n",
+    "    print(\"What just happend?\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "id": "e0ade870-1f7d-4aca-8938-c4058f7d679b",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdin",
+     "output_type": "stream",
+     "text": [
+      "Give me a number:  1\n"
+     ]
+    }
+   ],
+   "source": [
+    "int(\"23\")\n",
+    "result = input(\"Give me a number: \")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5979908b-fecd-4456-abe7-ab88e3d0e986",
+   "metadata": {},
+   "source": [
+    "If nothing handles the exception the program will be terminated."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ab6a5517-d360-4ba6-8057-0fd27b71ae44",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "\n",
+    "Create a small program that ask for input from the user (remember the input() function?). Convert the input into an integer. And divide 100 with that number. Handle any exception. Both the conversion and the division can cause exceptions on the wrong input. Experiment with the general Exception and the specific ones generated on wrong inputs. Also try to make good use of the finally like for example a goodbye message?"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ce23b15b-779e-4137-8724-1900e47b831b",
+   "metadata": {},
+   "source": [
+    "## Documentation\n",
+    " \n",
+    "Documentation is very important for documenting your public API. This can be done in a few ways. One way is to write documentation strings."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "id": "2a9c9eb4-c079-4bd3-8504-e3b3c6e30e88",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Help on function function in module __main__:\n",
+      "\n",
+      "function()\n",
+      "    This function does absolutely nothing.\n",
+      "    \n",
+      "    Luckely its well documentated.\n",
+      "\n",
+      " This function does absolutely nothing.\n",
+      "    \n",
+      "    Luckely its well documentated.\n",
+      "    \n"
+     ]
+    }
+   ],
+   "source": [
+    "def function():\n",
+    "    \"\"\" This function does absolutely nothing.\n",
+    "    \n",
+    "    Luckely its well documentated.\n",
+    "    \"\"\"\n",
+    "    pass\n",
+    "\n",
+    "help(function)\n",
+    "print(function.__doc__)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7fa6058d-a21b-41ce-9b92-08dce594e232",
+   "metadata": {},
+   "source": [
+    "Another way is to annotate your functions. These annotation are not enforced, but they do help document your intentions and some tools, like an IDE and static analyzers, can help detect mistakes. You can annotate both the types of the parameters and the return value."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "id": "42494b1f-4d29-407f-aa26-97567f3ded8c",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Help on function ranger in module __main__:\n",
+      "\n",
+      "ranger(start: numbers.Number, end: numbers.Number, step: numbers.Number = 1) -> Iterator[numbers.Number]\n",
+      "    ranger generates numbers from start till end with step sizes\n",
+      "    \n",
+      "    The function returns an Interator that will return Numbers starting from start and \n",
+      "    the following number will be increased by the step size as long as the number is\n",
+      "    smaller then the number given in the end parameter.\n",
+      "    \n",
+      "    Parameters\n",
+      "    ----------\n",
+      "    start : numbers.Number\n",
+      "        This is the starting number\n",
+      "    end : numbers.Number\n",
+      "        This is the number that all numbers in the range should be smaller with\n",
+      "    step : numbers.Number\n",
+      "        This is the step size dictates the distance between each number in the range\n",
+      "        \n",
+      "    Returns\n",
+      "    -------\n",
+      "    Iterator[Number]\n",
+      "        The Iterator that can loop over the range\n",
+      "\n",
+      "1.1 2.1 3.1 4.1 5.1 6.1 7.1 8.1 9.1 "
+     ]
+    }
+   ],
+   "source": [
+    "from numbers import Number\n",
+    "from typing import Iterator\n",
+    "\n",
+    "def ranger(start: Number, end: Number, step: Number = 1) -> Iterator[Number]:\n",
+    "    \"\"\" \n",
+    "    ranger generates numbers from start till end with step sizes\n",
+    "    \n",
+    "    The function returns an Interator that will return Numbers starting from start and \n",
+    "    the following number will be increased by the step size as long as the number is\n",
+    "    smaller then the number given in the end parameter.\n",
+    "    \n",
+    "    Parameters\n",
+    "    ----------\n",
+    "    start : numbers.Number\n",
+    "        This is the starting number\n",
+    "    end : numbers.Number\n",
+    "        This is the number that all numbers in the range should be smaller with\n",
+    "    step : numbers.Number\n",
+    "        This is the step size dictates the distance between each number in the range\n",
+    "        \n",
+    "    Returns\n",
+    "    -------\n",
+    "    Iterator[Number]\n",
+    "        The Iterator that can loop over the range\n",
+    "    \"\"\"\n",
+    "    n = start\n",
+    "    while n < end:\n",
+    "        yield n\n",
+    "        n += step\n",
+    "        \n",
+    "help(ranger)\n",
+    "\n",
+    "for item in ranger(1.1, 10):\n",
+    "    print(item, end=\" \")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7d31a68e-26a0-4f8e-99f2-7241e7733b96",
+   "metadata": {},
+   "source": [
+    "There is another way of documenting the code and that is with Doctests.\n",
+    "https://docs.python.org/3/library/doctest.html"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "id": "dd0a85c3-2ed2-4d65-beaf-a59ef09481bd",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "TestResults(failed=0, attempted=2)"
+      ]
+     },
+     "execution_count": 23,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "import numbers\n",
+    "\n",
+    "def add(a: numbers.Number, b: numbers.Number) -> numbers.Number:\n",
+    "    \"\"\" Returns the addition of a b\n",
+    "    >>> add(1, 1)\n",
+    "    2\n",
+    "    >>> add(1.0, 3.5)\n",
+    "    4.5\n",
+    "    \"\"\"\n",
+    "    return a + b\n",
+    "\n",
+    "import doctest\n",
+    "doctest.testmod()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9a7b54ef-92ed-4bd2-b7bd-c8cb1930d640",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "\n",
+    "Create a simple function with a few parameters and a return value. Document the function with docstrings, doctests and annotations."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "0c1659f9-4866-4e89-ad94-be3e2d9c5dbc",
+   "metadata": {},
+   "source": [
+    "## Files\n",
+    "\n",
+    "Dealing with files is something that many programs will have to do. For example, many configurations are loaded from files. Or data needs be loaded from files. Our programs can also generate data that needs to be stored in files. Its an just nice way of storing data for a long time. Python has a simple interface to deal with files.\n",
+    "\n",
+    "With the statement open you can open a file."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 24,
+   "id": "9fe548a1-79d4-439b-9355-6eb23819d640",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Astron\n",
+      "Jive\n",
+      "Nova\n"
+     ]
+    }
+   ],
+   "source": [
+    "stuff_fd = open(\"stuff.txt\")\n",
+    "\n",
+    "for line in stuff_fd:\n",
+    "    print(line, end=\"\")\n",
+    "    \n",
+    "stuff_fd.close()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "591455cf-daf7-42df-9ace-4bc5e4618702",
+   "metadata": {},
+   "source": [
+    "Its also very important to close files. This is done with the method close() on the file object. If we forget this step the file will be closed once the program closes. If we need to open up a large amount of files then we might run into the maximum allowed open files set by the operating system. It also takes up some small amount of memory. So at all times close the file when it no longer needs to be opened."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b187f094-3b82-484a-a0fd-82230e58e6ef",
+   "metadata": {},
+   "source": [
+    "When reading a file in text mode we can use different methods to read in the text."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a14fe96c-3610-4a53-94d4-f48ce31d9d23",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "stuff_fd = open(\"stuff.txt\")\n",
+    "\n",
+    "all_of_it = stuff_fd.read()\n",
+    "\n",
+    "print(all_of_it)\n",
+    "\n",
+    "stuff_fd.seek(0) # reset the reading back to the beginning of the file\n",
+    "\n",
+    "while char := stuff_fd.read(1):\n",
+    "    print(char, end=\"\")\n",
+    "    \n",
+    "stuff_fd.seek(0)\n",
+    "print(\"\")\n",
+    "\n",
+    "while line := stuff_fd.readline():\n",
+    "    print(line, end=\"\")\n",
+    "    \n",
+    "stuff_fd.close()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "0963551b-5439-4cee-a9e6-17920683f9bb",
+   "metadata": {},
+   "source": [
+    "Reading is the default mode a file is opened in. But we can also request other options. Like writing in text mode. We ca do this by specifying it with some flags with the second parameter called mode."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 27,
+   "id": "b1cf2034-2a64-40c8-b7be-a4640e7f0f09",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_fd = open(\"output.txt\", \"w\") # Use \"x\" if you don't want to overwrite a file. \"a\" if you want to append.\n",
+    "\n",
+    "for number in range(1, 100, 2):\n",
+    "    output_fd.write(str(number) + \"\\n\")\n",
+    "    \n",
+    "output_fd.close()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d49edcc4-866b-4e88-bfd1-9fb6d49a195c",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "\n",
+    "Read in a text file and write the contents out into another file but manipulate the text first. For example upper case it with .upper(). Or replace all . with an !. Be creative if you want to."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d2ef13a0-bd90-4f30-848b-93253d1942d6",
+   "metadata": {},
+   "source": [
+    "## Context manager\n",
+    "\n",
+    "https://docs.python.org/3/reference/compound_stmts.html#with\n",
+    "\n",
+    "Context managers have been added to Python because there is a common pattern, like we have seen with files, where you need do something in the beginning and in the end. And there in the middle you need to do other stuff that can cause exceptions as well. And in the case of an exception you do need to remember to the final thing as well. Doing this with context managers takes quite some code and you need to remember to do this as a programmer.\n",
+    "\n",
+    "The syntax for a context manager is to use the with ... (as) statements."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "37de7bfd-9bec-451b-8019-feda95aa4dc3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "with open(\"stuff.txt\") as file:\n",
+    "    for line in file:\n",
+    "        print(line, end=\"\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "6b2c9441-3617-4234-bf61-273e81be4143",
+   "metadata": {},
+   "source": [
+    "What the context manager does is call the `__enter__()` method on the object. The return value of the `__enter__()` assigned to the target (as part). Then the code in the block is performed. After that the `__exit__()` method on the object is called, even when an exception has occurred. Note: if the `__enter__()` fails the `__exit__()` is skipped."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "fe9bb68d-f4fd-472a-90ad-4de48c0ff845",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "\n",
+    "Rewrite you previous program with a context manager. You can have multiple context managers by separating them with a comma. `with open(\"a\") as a, open(\"b\") as b:`"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ba84965a-ec96-435e-8e48-1fc9c3c34a3d",
+   "metadata": {},
+   "source": [
+    "## Logging\n",
+    "\n",
+    "https://docs.python.org/3/library/logging.html\n",
+    "\n",
+    "Even when you write bug less code you do have to deal with issues where you don't have full control over the input. And when some of the input is unexpected it would be nice if that can be tracked. This is one reason why logging can be very important for your programs. Also if you have multiple stages its nice to see at what stage the program is and this can be done with logging. Also when designing you program it might be a good debugging tool to log certain smaller steps within the bigger stages.\n",
+    "\n",
+    "Python provides a couple of options to log. By default it logs to standard output and standard error."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 30,
+   "id": "b12c8fa7-ad71-4535-9174-768ed1de1d7e",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "4950"
+      ]
+     },
+     "execution_count": 30,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "import logging\n",
+    "import time\n",
+    "\n",
+    "formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')\n",
+    "\n",
+    "filehandler = logging.FileHandler(\"myprogram.log\")\n",
+    "filehandler.setLevel(logging.INFO)\n",
+    "filehandler.setFormatter(formatter)\n",
+    "\n",
+    "consolehandler = logging.StreamHandler()\n",
+    "consolehandler.setLevel(logging.INFO)\n",
+    "consolehandler.setFormatter(formatter)\n",
+    "\n",
+    "logger = logging.getLogger(\"myprogram\")\n",
+    "logger.addHandler(filehandler)\n",
+    "#logger.addHandler(consolehandler)\n",
+    "logger.setLevel(logging.DEBUG)\n",
+    "\n",
+    "def do_something():\n",
+    "    logger.info(\"Starting to do something\")\n",
+    "    time.sleep(1)\n",
+    "    \n",
+    "    logger.warning(\"I must have fallen asleep\")\n",
+    "    \n",
+    "    result = sum(range(1, 100))\n",
+    "    \n",
+    "    logger.debug(\"The calculation result is %s\", result)\n",
+    "    \n",
+    "    logger.info(\"Done with doing something\")\n",
+    "    \n",
+    "    return result\n",
+    "\n",
+    "do_something()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "6bab1f3d-3ad2-4887-8dd9-546447ddc9fb",
+   "metadata": {},
+   "source": [
+    "When creating string most people have fallen in love with the f-strings. But with logging we should not be using these. The logger can filter on what levels it needs to output. And string formatting is a relatively expensive operation. And the debug level tends to be more used in code then say an info or a warning level. If we don't need to print debug levels, like in a production environment we want to have our program as fast as possible.\n",
+    "\n",
+    "The logging system will perform the string formatting only when the level used is active. So we have to provide the string and the needed arguments to the logger like in the debug call in the example above."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "95878aee-b1fe-4350-96e7-cd997914c697",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "\n",
+    "Add some file logging to one of your previous exercise programs."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ae21fb80-df48-47c6-bbe8-cc867e6ac4d7",
+   "metadata": {},
+   "source": [
+    "## Object Oriented Programming\n",
+    "\n",
+    "### Classes\n",
+    "\n",
+    "So how to describe Object Oriented Programming? Many programming languages \"support\" OOP but they differ on how they do it. But there are some common features. The minimal thing we can say that its a programming paradigm that uses objects. So what are objects? Its a unit that can have both data and code in it. The code usually manipulates or uses the data inside the object. Not all data and code needs to be accessible on the outside of the object. The functions that are public are often called methods and the public data fields, but there are other names in use.\n",
+    "\n",
+    "The creation of objects in Python are based on classes. They can be seen as the blueprints or templates of the objects. It describes which data is available on the object and what code can be executed. When we create an object we need to instantiate it from a class. We have been instantiating objects many times already before. Lets start with the simplest class definition and go over the other features one by one.\n",
+    "\n",
+    "A class is defined with the statement class. The class, like everything else in Python, is also an object. When we call the class we get an instance of the class. When we create a new class we also have introduced a new type. It has its own interface and its own name."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "da78758f-1221-40e9-bdfb-0ee3e5338c96",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "<__main__.MyClass object at 0x7f69543c3e50>\n",
+      "<class '__main__.MyClass'>\n"
+     ]
+    }
+   ],
+   "source": [
+    "# This is an empty class. It has no data and no code.\n",
+    "class MyClass:\n",
+    "    pass\n",
+    "\n",
+    "myclass = MyClass()\n",
+    "\n",
+    "print(myclass)\n",
+    "print(type(myclass))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "c330e573-c0a0-496e-8844-b52cd30f7dc1",
+   "metadata": {},
+   "source": [
+    "A class is an object and that means we can add attributes to it. So we can introduce variables to the class. These are class variables and they are shared between instantiated objects."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "1020fbd1-b683-4ad0-a176-d86603cf44f7",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "1\n",
+      "1\n",
+      "2\n",
+      "2\n"
+     ]
+    }
+   ],
+   "source": [
+    "class A:\n",
+    "    class_variable = 1\n",
+    "    \n",
+    "obj_one = A()\n",
+    "obj_two = A()\n",
+    "\n",
+    "print(obj_one.class_variable)\n",
+    "print(obj_two.class_variable)\n",
+    "\n",
+    "A.class_variable = 2\n",
+    "\n",
+    "print(obj_one.class_variable)\n",
+    "print(obj_two.class_variable)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "41c896c2-5b79-4418-b7b5-4cab11ce955d",
+   "metadata": {},
+   "source": [
+    "We can add methods in a similar way as how we define functions. We pick a name and parameters and use the statement def. If we do this inside the class we get a method. But we always will need one parameter that represents the object. It needs to be the first one and its named self. If you want to make use of the object data inside of the data you need to access it through the self variable."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "34132d90-6919-41bf-9811-b4080564d321",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "42\n"
+     ]
+    }
+   ],
+   "source": [
+    "class ClassWithMethod:\n",
+    "\n",
+    "    def answer(self):\n",
+    "        return 42\n",
+    "\n",
+    "fc = ClassWithMethod()\n",
+    "\n",
+    "print(fc.answer())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1cd51b35-3d49-4446-90cb-3d179b8d48c5",
+   "metadata": {},
+   "source": [
+    "So the empty class looks empty from our code point of view. If we ask for a dir() on our object we see so much more than we created in code. This has to do with what is called inheritance. Through inheritance you can create an hierarchy of objects take over data and functions from the parent object. The `object` class is the lowest class available in Python. How can we declare we want to inherit from another class? After the name of the class we add parentheses with the parent class name between them. Note: if we leave our parent empty Python will fill it with `object`."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "06d1b0af-9d13-4036-b0d9-a696251099a4",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']\n"
+     ]
+    }
+   ],
+   "source": [
+    "class A:\n",
+    "    pass\n",
+    "\n",
+    "obj_one = A()\n",
+    "\n",
+    "print(dir(obj_one))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "43e4a488-30f5-45c6-936e-bb99f7641ed2",
+   "metadata": {},
+   "source": [
+    "One of the methods we inherit from object is `__init__`. It is called when we instantiate an object from a class. The intention of the method is to initialize our object with certain values. We can implement the `__init__`. When we create a new implementation of a method with the same name we call this overloading. In the way how naming is resolved we will basically block the implementation of the parent classes. We can still reach those classes but we will have to call those ourselves in our blocking method. Each instance method has at least one parameter and it is used for the instantiated object. And it is named self. Additional parameters can be placed after self."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "id": "c13b5f11-a818-4c82-8b10-49d0421f1694",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Hello\n",
+      "[]\n",
+      "None\n"
+     ]
+    }
+   ],
+   "source": [
+    "# This class has a few fields defined\n",
+    "class ClassWithFields:\n",
+    "    def __init__(self):\n",
+    "        self.field_one = \"Hello\"\n",
+    "        self.field_two = []\n",
+    "        self.field_three = None\n",
+    "\n",
+    "cwf = ClassWithFields()\n",
+    "\n",
+    "print(cwf.field_one)\n",
+    "print(cwf.field_two)\n",
+    "print(cwf.field_three)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "eb2d54b3-62cd-4ac5-8ce3-272947a479b6",
+   "metadata": {},
+   "source": [
+    "Now we can create a class with both instance variables / fields and methods. We can make use of the values of specific instance in our methods."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 24,
+   "id": "da78d9cb-7c0b-4238-812b-2e324fef73f1",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Hello Astron\n",
+      "Hello Jive\n",
+      "Hello Astron\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Lets make the inheritance of object a bit more explicit. But remember, its not needed.\n",
+    "class Greeter(object):\n",
+    "    def __init__(self, name):\n",
+    "        self.name = name\n",
+    "        \n",
+    "    def greet(self):\n",
+    "        print(f\"Hello {self.name}\")\n",
+    "        \n",
+    "astron_greeter = Greeter(\"Astron\")\n",
+    "jive_greeter = Greeter(\"Jive\")\n",
+    "astron_greeter.greet()\n",
+    "jive_greeter.greet()\n",
+    "astron_greeter.greet()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5d1fb19a-8e92-43da-8d5f-b52932b8f425",
+   "metadata": {},
+   "source": [
+    "The thing to know is that each instance has its own set of data. They are not shared among objects. Unless we create class variables. These are not initialized on an instance but on the class. And with mutable values like a list we can get unexpected behavior, but it also might just be what you need."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
+   "id": "5df9ad5b-e5c6-4dd1-9734-9e73a5b12d08",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "['Dwingeloo']\n",
+      "['Dwingeloo']\n",
+      "['Dwingeloo']\n",
+      "['shared']\n",
+      "['shared']\n",
+      "['shared']\n",
+      "['not shared']\n",
+      "['shared']\n",
+      "['shared']\n"
+     ]
+    }
+   ],
+   "source": [
+    "class Name:\n",
+    "    items = []\n",
+    "\n",
+    "astron = Name()\n",
+    "jive = Name()\n",
+    "nova = Name()\n",
+    "\n",
+    "# the items field is shared.\n",
+    "astron.items.append(\"Dwingeloo\")\n",
+    "\n",
+    "print(astron.items)\n",
+    "print(jive.items)\n",
+    "print(nova.items)\n",
+    "\n",
+    "# To show we really are dealing with the class field\n",
+    "Name.items = [\"shared\"] \n",
+    "\n",
+    "print(astron.items)\n",
+    "print(jive.items)\n",
+    "print(nova.items)\n",
+    "\n",
+    "# Once we set our instance field it takes precedence over the class field with the same name\n",
+    "astron.items = [\"not shared\"]\n",
+    "\n",
+    "print(astron.items)\n",
+    "print(jive.items)\n",
+    "print(nova.items)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "bca450c2-c8b7-4cdd-9871-162447edc523",
+   "metadata": {},
+   "source": [
+    "#### Exercise\n",
+    "\n",
+    "Create your own class that takes a name as a parameter. Use that parameter to create a field on the object. Then add a method that prints a greeting using the field that stored the name."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "4cbe5cf5-7771-40f3-ad22-ee1352ce0245",
+   "metadata": {},
+   "source": [
+    "### Operator overloading\n",
+    "\n",
+    "Python has special syntax for certain operations. Like for example `+` for addition and `[]` for indexing or slicing. As a programmer you tell Python what these operators mean for you class. This is called operator overloading. The methods that are used all start and end with double underscores. Like for example `__init__` that we have seen already, but also `__eq__` used by the syntax `==`."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 26,
+   "id": "371724ec-c6d8-4bf1-9b84-570e37d351a5",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "True\n",
+      "False\n",
+      "False\n"
+     ]
+    }
+   ],
+   "source": [
+    "# All dogs look the same to me\n",
+    "class Dog(object):\n",
+    "    def __init__(self, kind):\n",
+    "        self.kind = kind\n",
+    "        \n",
+    "    def __eq__(self, other):\n",
+    "        if type(other) == Dog:\n",
+    "            return True\n",
+    "        else:\n",
+    "            return False\n",
+    "        \n",
+    "    def __ne__(self, other):\n",
+    "        return not self.__eq__(other)\n",
+    "\n",
+    "class Cat():\n",
+    "    pass\n",
+    "\n",
+    "a = Dog(\"Golden Retriever\")\n",
+    "b = Dog(\"Husky\")\n",
+    "c = Cat()\n",
+    "\n",
+    "print(a == b)\n",
+    "print(a == c)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "0e4b0ce8-2472-4304-a660-f5058afff4db",
+   "metadata": {},
+   "source": [
+    "#### Exercise\n",
+    "\n",
+    "Write a class Person that takes a name and an age parameter. Overload the `__eq__`, `__lt__`, `__le__`, `__gt__`, `__ge__` operator methods. And implement then such that you can compare them based on the age of the person. You can also make it a bit harder by combining the age with some other attribute. (Rank, Function, Latitude, how many jokes the person knows) (Note: the `__ne__` by default makes use of `__eq__`). (eq, = | lt, < | le, <= | gt, > | ge, >= | ne, !=)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "cb0d55ff-839a-4f72-a5f7-1d54313a57b6",
+   "metadata": {},
+   "source": [
+    "### Inheritance (Is-A)\n",
+    "\n",
+    "With inheritance you can specialize or extend behavior of another class quite easily. The class gets all the behavior of the class that it inheritance from. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 27,
+   "id": "c483c837-7fc5-43ce-8c2d-b3906b38ed7c",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Hi I'm a human\n"
+     ]
+    }
+   ],
+   "source": [
+    "class Human(object):\n",
+    "    def speak(self):\n",
+    "        return \"Hi I'm a human\"\n",
+    "    \n",
+    "class JohnDo(Human):\n",
+    "    pass\n",
+    "\n",
+    "someone = JohnDo()\n",
+    "\n",
+    "print(someone.speak())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "4923319b-400b-4f77-ad81-3777a98e3acb",
+   "metadata": {},
+   "source": [
+    "We can also use the implementation of the parent or super class and make changes to it. Or we could decide to override it completely. When we want to call the parent behavior we can use the super() function to get access to the parent class. If we don't call the function of the super we will override its behavior for methods with the same name. This has all to do with the search order Python uses to locate objects by name."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 42,
+   "id": "c4f0b216-0a50-4f20-b1cd-f4e475b10f29",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Hello Jive\n",
+      "##########\n",
+      "Hello Jive\n",
+      "Or I could call you Jive The Greatest\n",
+      "1\n"
+     ]
+    }
+   ],
+   "source": [
+    "class Base(object):\n",
+    "    def __init__(self, name):\n",
+    "        self.name = name\n",
+    "\n",
+    "# Here we inherit the initialize funtion of the Base\n",
+    "class Greeter(Base):\n",
+    "    def greet(self):\n",
+    "        print(f\"Hello {self.name}\")\n",
+    "        \n",
+    "jive_greeter = Greeter(\"Jive\")\n",
+    "\n",
+    "jive_greeter.greet()\n",
+    "\n",
+    "print(\"#\" * 10)\n",
+    "\n",
+    "class FancyGreeter(Greeter):\n",
+    "    # I call the __init__ of the parent class as well because otherwise its not properly initialized.\n",
+    "    def __init__(self, name):\n",
+    "        super().__init__(name)\n",
+    "        self.fancyname = name + \" The Greatest\"\n",
+    "        \n",
+    "    # Here we extend the greet function by calling the parent method as well.\n",
+    "    def greet(self):\n",
+    "        super().greet()\n",
+    "        print(f\"Or I could call you {self.fancyname}\")\n",
+    "\n",
+    "fancy_jive_greeter = FancyGreeter(\"Jive\")\n",
+    "fancy_jive_greeter.greet()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d224b4b8-6b54-4a9e-9c42-f1d3cd85af96",
+   "metadata": {},
+   "source": [
+    "#### Exercise\n",
+    "\n",
+    "Create a class with some methods on them. Have them do something visible like a print. Then make another class that inherits from the first class. See that the class inherits the methods. Override some methods see what happens when you call the super and what happens when you don't."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8b9f9ee0-9e05-4169-8f8f-1b2084a2a8d9",
+   "metadata": {},
+   "source": [
+    "### Polymorphism\n",
+    "\n",
+    "If our code depends on a specific parent class we can use any object of a class that derives of the parent class. This is called polymorphism. One common example is that of a shape class that has a method called area. One thing you need to be aware of is that algorithms that make use of polymorphism do expect the same behavior of all the children. So don't break that trust by doing something unexpected like returning other types or messing with fields in an unexpected way. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 45,
+   "id": "57ac7f45-e3b5-479d-be9e-7eefd685bf5a",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "The area of this shape (A circle with a radius of 2) is 12.566370614359172\n",
+      "The area of this shape (A square with side lengths of 4) is 16\n"
+     ]
+    }
+   ],
+   "source": [
+    "import math\n",
+    "\n",
+    "class Shape(object):\n",
+    "    def area(self):\n",
+    "        raise NotImplementedError\n",
+    "\n",
+    "class Circle(Shape):\n",
+    "    def __init__(self, radius):\n",
+    "        self.radius = radius\n",
+    "        \n",
+    "    def area(self):\n",
+    "        return math.pi * (self.radius ** 2)\n",
+    "    \n",
+    "    def __str__(self):\n",
+    "        return f\"A circle with a radius of {self.radius}\"\n",
+    "\n",
+    "class Square(Shape):\n",
+    "    def __init__(self, width):\n",
+    "        self.width = width\n",
+    "        \n",
+    "    def area(self):\n",
+    "        return self.width ** 2\n",
+    "    \n",
+    "    def __str__(self):\n",
+    "        return f\"A square with side lengths of {self.width}\"\n",
+    "\n",
+    "a = Circle(2)\n",
+    "b = Square(4)\n",
+    "\n",
+    "shapes = [a, b]\n",
+    "\n",
+    "b.width = 3\n",
+    "\n",
+    "for shape in shapes:\n",
+    "    print(f\"The area of this shape ({shape}) is {shape.area()}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ffa8d655-0720-407c-9f9c-92db68bdc265",
+   "metadata": {},
+   "source": [
+    "#### Exercise\n",
+    "\n",
+    "Create a base class. Say a Star class. Implement some methods and/or fields. Create some specialized version of the base class. Like a DwarfStar, etc. And then create a list with different types of objects. Then loop over the items and using methods or fields from the base class."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "497c84fe-f4d6-40c6-9456-be0f61535c95",
+   "metadata": {},
+   "source": [
+    "### Multiple inheritance\n",
+    "\n",
+    "Python allows us to inherent from more than one class at once. This can lead to some nice abstraction options but also to potential issues you need to be aware of."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 46,
+   "id": "1da21c1a-d8b2-425f-a0b6-5d659004a93e",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Whoaf\n",
+      "Snip Snip\n"
+     ]
+    }
+   ],
+   "source": [
+    "# With multiple inheritance I can combine robot and dog instead of picking one and \n",
+    "# adding the missing bits of the other. This would still be a good option is we don't\n",
+    "# need one of the classes on its own.\n",
+    "class Dog(object):\n",
+    "    def bark(self):\n",
+    "        print(\"Whoaf\")\n",
+    "        \n",
+    "class Robot(object):\n",
+    "    def mow(self):\n",
+    "        print(\"Snip Snip\")\n",
+    "\n",
+    "class RobotDog(Robot, Dog):\n",
+    "    pass\n",
+    "\n",
+    "tino = RobotDog()\n",
+    "\n",
+    "tino.bark()\n",
+    "tino.mow()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2e3d1237-f39a-41a1-9a6d-890b22570d6a",
+   "metadata": {},
+   "source": [
+    "So what happens if we inherit from multiple classes that have methods with the same name?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 51,
+   "id": "80abf9d6-2bc4-4251-86de-9b5be3b2f378",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "A walk\n",
+      "B walk\n",
+      "Its me, B\n",
+      "Its me, B\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Which method is used depends on the order of the class inheritence. The first one that matches wins.\n",
+    "class A(object):\n",
+    "    def walk(self):\n",
+    "        print(\"A walk\")\n",
+    "        \n",
+    "class B(object):\n",
+    "    def walk(self):\n",
+    "        print(\"B walk\")\n",
+    "        \n",
+    "    def unique(self):\n",
+    "        print(\"Its me, B\")\n",
+    "\n",
+    "class C(A, B):\n",
+    "    pass\n",
+    "\n",
+    "class D(B, A):\n",
+    "    pass\n",
+    "\n",
+    "def unique():\n",
+    "    print(\"Outside\")\n",
+    "\n",
+    "see = C()\n",
+    "dee = D()\n",
+    "\n",
+    "see.walk()\n",
+    "dee.walk()\n",
+    "see.unique()\n",
+    "dee.unique()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 62,
+   "id": "7d6728b4-bdac-4f11-85b6-9aa28256767f",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "B walk\n",
+      "A walk\n",
+      "B walk\n",
+      "A walk\n",
+      "True\n",
+      "False\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Another option but if confusing and has different behavior\n",
+    "class A(object):\n",
+    "    def walk(self):\n",
+    "        print(\"A walk\")\n",
+    "\n",
+    "class B(object):\n",
+    "    def walk(self):\n",
+    "        print(\"B walk\")\n",
+    "        \n",
+    "class C(A, B):\n",
+    "    def walk(self):\n",
+    "        # We choose B over A\n",
+    "        B.walk(self)\n",
+    "        A.walk(self)\n",
+    "\n",
+    "# We can get the same similar behavior but we don't get the A and B types.\n",
+    "class D(object):\n",
+    "    def walk(self):\n",
+    "        B.walk(self)\n",
+    "        A.walk(self)\n",
+    "        \n",
+    "see = C()\n",
+    "dee = D()\n",
+    "see.walk()\n",
+    "dee.walk()\n",
+    "print(issubclass(C, A))\n",
+    "print(issubclass(D, A))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d847f22e-8ec7-4bee-becb-16b2868dfa12",
+   "metadata": {},
+   "source": [
+    "### Mix-in\n",
+    "\n",
+    "A mix-in is basically a parent class but not with the same intention. Where with inheritance we create an Is-A relations ship we create a Can-Do relationship with a mix-in. It injects certain behavior we can reuse for multiple classes. This can prevent code duplication."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 78,
+   "id": "8db10f77-b3fb-4e1e-9dda-c6863a91ab01",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Mew\n",
+      "I know the time! Its: Time to party\n",
+      "Bark\n",
+      "30\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "['Bla bla']"
+      ]
+     },
+     "execution_count": 78,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "class Watch(object):\n",
+    "    def time(self):\n",
+    "        return \"Time to party\"\n",
+    "\n",
+    "class WatchMixin(object):\n",
+    "    def __init__(self):\n",
+    "        self._watch = Watch()\n",
+    "        \n",
+    "    def tell_time(self):\n",
+    "        print(f\"I know the time! Its: {self._watch.time()}\")\n",
+    "\n",
+    "class Animal(object):\n",
+    "    def speak(self):\n",
+    "        raise NotImplementedError\n",
+    "\n",
+    "class Dog(Animal):\n",
+    "    def speak(self):\n",
+    "        print(\"Bark\")\n",
+    "        \n",
+    "class Cat(WatchMixin, Animal):\n",
+    "    def speak(self):\n",
+    "        print(\"Mew\")\n",
+    "        \n",
+    "cat = Cat()\n",
+    "dog = Dog()\n",
+    "\n",
+    "cat.speak()\n",
+    "cat.tell_time()\n",
+    "dog.speak()\n",
+    "\n",
+    "class Notebook(object):\n",
+    "    def __init__(self):\n",
+    "        self.notes = []\n",
+    "        \n",
+    "class NotebookMixin(object):\n",
+    "    def __init__(self):\n",
+    "        self._notebook = Notebook()\n",
+    "        \n",
+    "    def take_note(self, note):\n",
+    "        self._notebook.notes.append(note)\n",
+    "        \n",
+    "    def notes(self):\n",
+    "        return self._notebook.notes\n",
+    "        \n",
+    "class Person(object):\n",
+    "    def __init__(self, name, age):\n",
+    "        self.name = name\n",
+    "        self.age = age\n",
+    "        \n",
+    "class Journalist(NotebookMixin, Person):\n",
+    "    def __init__(self, name, age):\n",
+    "        NotebookMixin.__init__(self)\n",
+    "        Person.__init__(self, name, age)\n",
+    "    \n",
+    "journalist = Journalist(\"Henk\", 30)\n",
+    "\n",
+    "journalist.notes()\n",
+    "print(journalist.age)\n",
+    "journalist.take_note(\"Bla bla\")\n",
+    "journalist.notes()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "0c5b5a4b-a9fd-4c27-b08c-8c61d3de63c5",
+   "metadata": {},
+   "source": [
+    "#### Exercise\n",
+    "\n",
+    "Create a Person class with a name and age. Give it an additional behavior/method by means of a mix-in. Say something like a NotetakingMixin. It injects a notebook on the instance and adds and method take_note(self, note) that writes the note to the notebook."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d7171b49-a317-4d27-a3ce-c33ef66e4420",
+   "metadata": {},
+   "source": [
+    "### Composition (Has-A)\n",
+    "\n",
+    "Not every relationship should be handled with inheritance. If there is a clear Is-A relationship inheritance is still a good choice. For all other situations you should opt for composition. We then talk about Has-A relationship. But it is sometimes hard to see the difference. But if you don't have a full match or you don't want to expose everything from another class you should not use inheritance.\n",
+    "\n",
+    "Composition is nothing more then using an instance of a class inside a class instead of using inheritance."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 79,
+   "id": "c63b24f0-5405-481b-b231-b6d1a730a202",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# We can create a Person with vital organs.\n",
+    "class Heart:\n",
+    "    def rate(self):\n",
+    "        return 70\n",
+    "\n",
+    "class Lungs:\n",
+    "    pass\n",
+    "\n",
+    "class Brains:\n",
+    "    pass\n",
+    "\n",
+    "class Person:\n",
+    "    def __init__(self):\n",
+    "        self._heart = Heart()\n",
+    "        self._lungs = Lungs()\n",
+    "        self._brains = Brains()\n",
+    "        \n",
+    "    def heart_rate(self):\n",
+    "        return self._heart.rate()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "072b29c7-c74e-40fe-9f59-7195d0a61ec5",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Note this program is not fully implemented. It will fail if you try to instantiate objects from it.\n",
+    "# An example that might not be clear\n",
+    "# Is an Employee a Person or does it have/need a Person?\n",
+    "class Person:\n",
+    "    def __init__(self, name, age, address, hobbies, bank_accounts):\n",
+    "        self.name = name\n",
+    "        self.age = age\n",
+    "        self.address = address\n",
+    "        self.hobbies = hobbies\n",
+    "        self.bank_accounts = bank_accounts\n",
+    "        \n",
+    "    def walk(self):\n",
+    "        pass\n",
+    "    \n",
+    "    def sleep(self):\n",
+    "        pass\n",
+    "\n",
+    "class Employee(Person):\n",
+    "    def salery(self):\n",
+    "        return 10000 * self.age\n",
+    "    \n",
+    "# In this case we have the Employee expose all the fields and methodes.\n",
+    "# For Employee it might not be needed to expose hobbies or the sleep\n",
+    "# method. In this case it is likely better to use composition.\n",
+    "class EmployeeComp:\n",
+    "    def __init__(self, employee_number):\n",
+    "        self._employee_number = employee_number\n",
+    "        self._person = employee_db.fetch_person(employee_number)\n",
+    "        \n",
+    "    def salery(self):\n",
+    "        return 10000 * self._person.age\n",
+    "    \n",
+    "# It usually depends on the implementation. If the Person only has the behavior\n",
+    "# and fields that an Employee also needs you could go for Inheritance. Or keep\n",
+    "# it all in one object. The choice is always. Its there a close match and I want\n",
+    "# to extends the data or behavior you can go with Inheritance.\n",
+    "#\n",
+    "# We can achieve the same with composition as with inheritance but it requires\n",
+    "# more code with the \"downside\" that python is not aware of subclassing. But we\n",
+    "# do have more control over what we expose\n",
+    "class EmployeeInheritanceComp:\n",
+    "    def __init__(self, employee_number):\n",
+    "        self._employee_number = employee_number\n",
+    "        self._person = employee_db.fetch_person(employee_number)\n",
+    "        \n",
+    "    def salery(self):\n",
+    "        return 10000 * self._person.age\n",
+    "    \n",
+    "    @property\n",
+    "    def age(self):\n",
+    "        return self._person.age\n",
+    "    \n",
+    "    @age.setter\n",
+    "    def age(self, age):\n",
+    "        self._person.age = age\n",
+    "        \n",
+    "    # ETC..."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5513b133-bbb5-4f51-ac6e-cf03b90aad8f",
+   "metadata": {},
+   "source": [
+    "#### Exercise\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "f2996150-a64b-4fae-bffe-bfd7dd93e7a6",
+   "metadata": {},
+   "source": [
+    "### @property\n",
+    "\n",
+    "https://docs.python.org/3/library/functions.html#property\n",
+    "\n",
+    "Properties behave much like fields, but give more control of what is allowed. They also allow you to do calculation or validations."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 83,
+   "id": "dc9ca023-3a1e-4ede-bdbc-823440468075",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Bill\n",
+      "We are not allowed to set a property by default.\n"
+     ]
+    }
+   ],
+   "source": [
+    "class Dog(object):\n",
+    "    def __init__(self, name):\n",
+    "        self._name = name # and underscore means private but its not enforced.\n",
+    "        \n",
+    "    @property\n",
+    "    def name(self):\n",
+    "        return self._name\n",
+    "    \n",
+    "dog = Dog(\"Bill\")\n",
+    "\n",
+    "# syntax looks the same as of a field\n",
+    "print(dog.name)\n",
+    "\n",
+    "try:\n",
+    "    dog.name = \"Milo\"\n",
+    "except:\n",
+    "    print(\"We are not allowed to set a property by default.\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 84,
+   "id": "59283caa-1107-412c-ba3d-1b4341da3975",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "1\n",
+      "2\n",
+      "3\n",
+      "4\n",
+      "5\n"
+     ]
+    }
+   ],
+   "source": [
+    "# We can use some calculation instead of a static value like with a field\n",
+    "class Counter(object):\n",
+    "    def __init__(self):\n",
+    "        self._count = 0\n",
+    "    \n",
+    "    @property\n",
+    "    def count(self):\n",
+    "        self._count += 1\n",
+    "        \n",
+    "        return self._count\n",
+    "    \n",
+    "c = Counter()\n",
+    "\n",
+    "for _ in range(5):\n",
+    "    print(c.count)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 88,
+   "id": "01c38e5c-979a-446f-ab05-c0a6990f90ef",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "Moko\n",
+      "This dogs name needs to start with a M but Plonk was given\n"
+     ]
+    }
+   ],
+   "source": [
+    "# We can also add a setter and do some validation\n",
+    "class Dog(object):\n",
+    "    def __init__(self):\n",
+    "        self._name = \"\"\n",
+    "    \n",
+    "    @property\n",
+    "    def name(self):\n",
+    "        return self._name\n",
+    "    \n",
+    "    @name.setter\n",
+    "    def name(self, name):\n",
+    "        if name.lower().startswith(\"m\"):\n",
+    "            self._name = name\n",
+    "        else:\n",
+    "            raise ValueError(f\"This dogs name needs to start with a M but {name} was given\")\n",
+    "    \n",
+    "dog = Dog()\n",
+    "\n",
+    "print(dog.name)\n",
+    "\n",
+    "dog.name = \"Moko\"\n",
+    "\n",
+    "print(dog.name)\n",
+    "\n",
+    "try:\n",
+    "    dog.name = \"Plonk\"\n",
+    "except Exception as e:\n",
+    "    print(e)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "fce6c540-b595-481c-ab7c-fda006b1e596",
+   "metadata": {},
+   "source": [
+    "### @classmethod\n",
+    "\n",
+    "https://docs.python.org/3/library/functions.html#classmethod\n",
+    "\n",
+    "Class methods work much the same way as methods on an object but it works only with class variables and other class methods."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 91,
+   "id": "de71650a-bc68-4f22-918d-144606480905",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "I am a vehical powered by Sun\n",
+      "I am a vehical powered by Sun\n",
+      "I am a vehical powered by Sun\n"
+     ]
+    }
+   ],
+   "source": [
+    "class Vehical(object):\n",
+    "    power_source = \"Sun\"\n",
+    "    \n",
+    "    @classmethod\n",
+    "    def name(cls):\n",
+    "        print(f\"I am a vehical powered by {cls.power_source}\")\n",
+    "        \n",
+    "Vehical.name()\n",
+    "\n",
+    "v = Vehical()\n",
+    "v.name()\n",
+    "\n",
+    "# We now add a field to the instance. But name still uses the class variable\n",
+    "v.power_source = \"Oil\"\n",
+    "v.name()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "867d27c5-9bc5-4cb6-9fd2-e1497243ca8b",
+   "metadata": {},
+   "source": [
+    "### @staticmethod\n",
+    "\n",
+    "https://docs.python.org/3/library/functions.html#staticmethod\n",
+    "\n",
+    "Static methods are procedures that don't have access to the class or the instance. This is mainly used for utility functions that are related to idea of the class."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 96,
+   "id": "ec314cc0-1bf8-4e9f-8300-dee33818d255",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Is a person of the age 33 and adult? Answer: Yes\n",
+      "False\n"
+     ]
+    }
+   ],
+   "source": [
+    "class Person(object):\n",
+    "    @staticmethod\n",
+    "    def is_adult(age):\n",
+    "        return age >= 18\n",
+    "\n",
+    "print(\"Is a person of the age 33 and adult? Answer: \" + (\"Yes\" if Person.is_adult(age = 33) else \"No\"))\n",
+    "\n",
+    "# With this setup the methods is also available on the instance.\n",
+    "p = Person()\n",
+    "print(p.is_adult(3))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ea202278-cf97-4847-8015-48b93b8c434d",
+   "metadata": {},
+   "source": [
+    "### Class documentation\n",
+    "\n",
+    "Docstrings on classes. If the first line in the class block is a string it will be used as the documentation for the class."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 99,
+   "id": "b084011c-afdc-4210-a268-1d7845fabf3f",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Help on class A in module __main__:\n",
+      "\n",
+      "class A(builtins.object)\n",
+      " |  This is the best class in the world\n",
+      " |  \n",
+      " |  Data descriptors defined here:\n",
+      " |  \n",
+      " |  __dict__\n",
+      " |      dictionary for instance variables (if defined)\n",
+      " |  \n",
+      " |  __weakref__\n",
+      " |      list of weak references to the object (if defined)\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "class A:\n",
+    "    \"This is the best class in the world\"\n",
+    "    pass\n",
+    "\n",
+    "help(A)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1ca021eb-55c1-45d9-b8dc-df13fe3fb10b",
+   "metadata": {},
+   "source": [
+    "#### Exercise\n",
+    "\n",
+    "Add some documentation to one of you previous classes."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a8236506-2e52-4b46-8124-78f28ebf43f1",
+   "metadata": {},
+   "source": [
+    "### Naming Conventions / code style\n",
+    "\n",
+    "https://peps.python.org/pep-0008/\n",
+    "\n",
+    "So naming conventions provided by pep8.\n",
+    "\n",
+    "`Class names should normally use the CapWords convention.`\n",
+    "\n",
+    "So start Class name with a capital letter and concatenate all the words following and have each word start with a capital letter. For example: RobotDog. GrassMaintainer.\n",
+    "\n",
+    "`Always use self for the first argument to instance methods.`\n",
+    "\n",
+    "`Always use cls for the first argument to class methods.`\n",
+    "\n",
+    "`Method Names and Instance Variables: Use the function naming rules: lowercase with words separated by underscores as necessary to improve readability.`\n",
+    "\n",
+    "`Use one leading underscore only for non-public methods and instance variables.`\n",
+    "\n",
+    "So we have see it in some examples. If we start methods and fields with an `_` (underscore) we say to other programmers you should not use me. Its not enforced. But its good practice to mark internal units with an underscore.\n",
+    "\n",
+    "You would do this for data you need internally to perform some task. In the future you might want to change the data structure and that is hard if it is used externally.\n",
+    "\n",
+    "You would also do this for methods where you want to split up code of readability into a few smaller methods. Also here you could change the internal structure of the code in the future and that is again harder if they are used externally.\n",
+    "\n",
+    "When to create a smaller function or what data type to use is the hard part of programming. There are many books to read on the subject and it also comes with practice and exposure to other code. I can't teach you this in 5 days."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "190493bc-8510-4f55-a387-0a814d874a90",
+   "metadata": {},
+   "source": [
+    "### How to decide when to create classes\n",
+    "\n",
+    "#### IO vs Non-IO\n",
+    "\n",
+    "Code that deals with logic and no side effects (IO) is the easiest to test and understand. Try to put as much logic without IO in its own class and use that class in the IO class. That class would only glue the logic to the IO. \n",
+    "\n",
+    "#### Do one thing (Cohesion)\n",
+    "\n",
+    "If a class has more then one reason to change behavior it most likely does too much. Say for example a Employee class needs to change because tax laws have changed but also because commission calculation have changed it likely needs to have the logic of taxes and/or commissions in its own class.\n",
+    "\n",
+    "Also a good indication is the cohesion of the class. So cohesion can mean multiple things but the one I want to lift out is the cohesion between methods and data. If most methods use the same data one could speak of strong cohesion. If you would have 3 methods on you class that use half of the data and 3 methods the other half it has a weaker cohesion. In this case it might be that the class is responsible for two tasks instead of one.\n",
+    "\n",
+    "#### Don't repeat yourself\n",
+    "\n",
+    "If you see some code or data be repeated over and over again you might have indicated an pattern that can be placed into its own class."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "c0fe1c43-1cb8-4b67-8345-d71b882db2e9",
+   "metadata": {},
+   "source": [
+    "### Does everything have to be done in a class?\n",
+    "\n",
+    "No. Sometimes a simple function is better. For example if it does not need its own data on an instance.\n",
+    "\n",
+    "Sometimes a primitive type is also good enough."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "dbadff33-8d89-4929-ad19-9e9f14bb9a25",
+   "metadata": {},
+   "source": [
+    "### You write code for humans\n",
+    "\n",
+    "Remember at all times that you are writing code for humans not computers. Interpreters and compilers write code for the computer. Write you computations as clear as possible. Use good names. Make your methods as simple to follow as possible. It is very tempting to show off you knowledge of a language or your decoding skills of hard to read code. And sometimes debugging is more rewarding than writing simple code.\n",
+    "\n",
+    "But writing clean and understandable code is the hardest so when you write simple code don't be mistaken that it was easy to compose (well, sometimes it is). So please use your brain to take the extra steps to look at a working implementation and see if you can make it simpler and more readable. Is the variable named `obj` really the best name or can you think of a better name?"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d9d4765c-73f2-4ee2-976f-333ab4119bce",
+   "metadata": {},
+   "source": [
+    "## Basic testing\n",
+    "\n",
+    "The most basic test is to run the program and see if it does what it is supposed to do. This is a good overall test to always do but during the development of the program you can also do other types of tests.\n",
+    "\n",
+    "There are unit tests that test one single unit. Like a single class or function. These types of tests are usually the most detailed tests and are the bulk of all tests. But they are also the fastest tests to run. They will tests all the logic of the unit. The common cases, the corner cases, and the illegal cases.\n",
+    "\n",
+    "Then there are also integration tests where multiple units are combined to form bigger unit that provide a big chunk of features. The goal of these tests is to see if everything works together. Usually only the common cases are tested. These tests also involve IO code. Like interactions with file system, a database, or network connections. Because of the IO these tests tend to take a lot more time than unit tests.\n",
+    "\n",
+    "Even bigger tests can be defined that tests the user requirements. These are usually called the user acceptance tests. The goal of these tests is to provide evidence to the end user that the program does what the user wants.\n",
+    "\n",
+    "Then sometimes non-functional tests are defined as well. These tests test performance, reaction times, etc.\n",
+    "\n",
+    "Ideally all the tests are automated so they can be performed by the computer as often as we want.\n",
+    "\n",
+    "Python provides a library for unit tests called unittest. https://docs.python.org/3/library/unittest.html"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "4a03469a-d281-491b-9fb9-0bc898fe0c88",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import unittest\n",
+    "\n",
+    "class Calculator:\n",
+    "    def __init__(self):\n",
+    "        self._answer = 0\n",
+    "        \n",
+    "    @property\n",
+    "    def answer(self):\n",
+    "        return self._answer\n",
+    "    \n",
+    "class TestCalculator(unittest.TestCase):\n",
+    "    def test_anwser_start_with_zero(self):\n",
+    "        calc = Calculator()\n",
+    "        \n",
+    "        self.assertEqual(0, calc.answer)\n",
+    "\n",
+    "# To run unittests inside a Jupyter notebook we call main differently.\n",
+    "unittest.main(argv=[''], verbosity=3, exit=False)\n",
+    "# In a file based program you would add this.\n",
+    "# if __name__ == '__main__':\n",
+    "#    unittest.main()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ac1da4db-b646-44e0-bb2b-898ae92d6300",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import unittest\n",
+    "\n",
+    "class Calculator:\n",
+    "    def __init__(self):\n",
+    "        self._answer = 0\n",
+    "        \n",
+    "    @property\n",
+    "    def answer(self):\n",
+    "        return self._answer\n",
+    "    \n",
+    "    def add(self, value):\n",
+    "        self._answer += value\n",
+    "    \n",
+    "class TestCalculator(unittest.TestCase):\n",
+    "    def setUp(self):\n",
+    "        self.calc = Calculator()\n",
+    "        \n",
+    "    def test_answer_starts_with_zero(self):\n",
+    "        self.assertEqual(0, self.calc.answer)\n",
+    "        \n",
+    "    def test_add_two_gives_answer_two(self):\n",
+    "        self.calc.add(2)\n",
+    "        \n",
+    "        self.assertEqual(2, self.calc.answer)\n",
+    "        \n",
+    "    def test_clear_sets_answer_to_zero(self):\n",
+    "        self.calc.add(33)\n",
+    "        \n",
+    "        self.calc.clear()\n",
+    "        \n",
+    "        self.assertEqual(0, self.calc.answer)\n",
+    "\n",
+    "# To run unittests inside a Jupyter notebook we call main differently.\n",
+    "unittest.main(argv=[''], verbosity=3, exit=False)\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "6efb3301-e6af-4e7f-b35d-a0e6da4a339d",
+   "metadata": {},
+   "source": [
+    "Integration tests can be also be written with the unittest library. But the setup is usually more complex. For example the DB and program need to be started and configured before a test can be performed."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "95a28b7e-1953-4cdd-8d96-55d875e05055",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "\n",
+    "Finish the Calculator class by adding more tests and filling in the implementation. This writing tests first is called Test Driven Development (TDD) or Test first development."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ccb8e9c2-d710-46aa-a64b-26246eb18dce",
+   "metadata": {},
+   "source": [
+    "## Debugging\n",
+    "\n",
+    "So even when you have many tests around your code you still might have introduced unwanted behavior that we call bugs. And the process of finding the unwanted behavior and removing it is called debugging.\n",
+    "\n",
+    "This can be a tedious process but luckily there are a few tools and techniques to help us.\n",
+    "\n",
+    "On common technique is placing print or log statements in the code. This can shows us quickly what the state of the program is. You can for example output the local variables in a function.\n",
+    "\n",
+    "Another option is to use a tool called a debugger. Many Integrated Development Environments allow you to set break points easily. Then you can run the program and it will stop the program on the break point. This break point is set on a specific line of the source code. When it is stopped you can ask for example the values of the local variables or the stack trace (a tree that shows you what route was taken to get to this break point).\n",
+    "\n",
+    "The debugger provided by python is called pdb https://docs.python.org/3/library/pdb.html It takes some practice to fully use it and most developers don't bother and go the IDE or print/log route."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5b3a807c-cfc4-4b9f-aa9a-ee93beda9b00",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pdb\n",
+    "import buggy\n",
+    "\n",
+    "bug = buggy.Buggy()\n",
+    "\n",
+    "bug.calc(4)\n",
+    "bug.calc(10)\n",
+    "#bug.calc(20)\n",
+    "\n",
+    "pdb.runcall(bug.calc, 20)\n",
+    "\n",
+    "# We can ask for help with h\n",
+    "# We can ask the stack trace with w\n",
+    "# We can step through the code with s\n",
+    "# We can list all breakpoints with b\n",
+    "# We can add breakpoints with b (b buggy.py:3)\n",
+    "# We can print expressions with p\n",
+    "# We can as type of expression with whatis\n",
+    "# We can clear breakpoints with cl\n",
+    "# We can continue with c\n",
+    "# We can quit with q"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "4e24f885-d24c-4f24-828d-15f38b3915df",
+   "metadata": {},
+   "source": [
+    "## Further reading\n",
+    "\n",
+    "* Any book / blog that talks about how to design programs.\n",
+    "* Other peoples code (Gitlab, Github) Try to understand the structure. Think about if the chosen names could be better. Think about if the units don't do too much. Etc.\n",
+    "* Any book / blog that talks about readability\n",
+    "* Any book / blog that talks about data structures\n",
+    "* Any book / blog that talks about algorithms\n",
+    "* Any book / blog that talks about testing\n",
+    "\n",
+    "### After some experience (Language agnostic)\n",
+    "\n",
+    "* Book - The pragmatic programmer https://pragprog.com/titles/tpp20/the-pragmatic-programmer-20th-anniversary-edition/\n",
+    "* Book - Refactoring https://martinfowler.com/books/refactoring.html\n",
+    "* Book - Clean Code https://www.pearson.com/store/p/clean-code-a-handbook-of-agile-software-craftsmanship/P100001776638/9780132350884\n",
+    "* Book - Extreme Programming Explained https://www.pearson.com/us/higher-education/program/Beck-Extreme-Programming-Explained-Embrace-Change-2nd-Edition/PGM155384.html\n",
+    "* Book - Working Effectively with Legacy Code https://www.pearson.com/store/p/working-effectively-with-legacy-code/P100001342740/9780131177055\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "eed1f27e-ecf9-4942-a051-1d5c97ecc3a0",
+   "metadata": {},
+   "source": [
+    "## Community\n",
+    "\n",
+    "* Slack #learn-python\n",
+    "* Slack #cop-programming\n",
+    "* Slack #software-development\n",
+    "* Slack #webdev\n",
+    "* Reddit /r/programming\n",
+    "* Reddit /r/learnprogramming\n",
+    "* Reddit /r/python\n",
+    "* Reddit /r/AskProgramming\n",
+    "* Reddit /r/learnpython\n",
+    "* Reddit /r/dailyprogrammer\n",
+    "* Ebooks https://www.humblebundle.com"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9305b511-1699-49fd-8f92-2566e4918089",
+   "metadata": {},
+   "source": [
+    "### Example Answers"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5dc5c340-599d-4359-a885-effc64cb8922",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "def tracker(func):\n",
+    "    arguments = []\n",
+    "    def wrapper(*args, **kwargs):\n",
+    "        arguments.append((func.__name__, args, kwargs))\n",
+    "        \n",
+    "        print(arguments)\n",
+    "        \n",
+    "        return func(*args, **kwargs)\n",
+    "    \n",
+    "    return wrapper\n",
+    "\n",
+    "@tracker\n",
+    "def func_1(arg_1, arg2):\n",
+    "    pass\n",
+    "\n",
+    "@tracker\n",
+    "def func_2(arg_1, arg2, arg3):\n",
+    "    pass\n",
+    "\n",
+    "func_1(1, 2)\n",
+    "func_1(3, 5)\n",
+    "func_2(1, 4, 6)\n",
+    "func_2(1, 7, 8)\n",
+    "func_2(1, 7, arg3=10)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/buggy.py b/buggy.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc53b17e709ed19e18de4a440c086508440c512c
--- /dev/null
+++ b/buggy.py
@@ -0,0 +1,4 @@
+class Buggy:
+    def calc(self, number):
+        d = 10 - number / 2
+        return number / d
\ No newline at end of file
diff --git a/myprogram.log b/myprogram.log
new file mode 100644
index 0000000000000000000000000000000000000000..5c6fef966aa9d02ae6fdb2b4b2ae23206e77163d
--- /dev/null
+++ b/myprogram.log
@@ -0,0 +1,42 @@
+
+2022-04-26 11:24:13,636 - myprogram - INFO - Starting to do something
+2022-04-26 11:24:14,638 - myprogram - WARNING - I must have fallen asleep
+2022-04-26 11:24:14,639 - myprogram - DEBUG - The calculation result is 4950
+2022-04-26 11:24:14,639 - myprogram - INFO - Done with doing something
+2022-04-26 11:25:21,843 - myprogram - INFO - Starting to do something
+2022-04-26 11:25:21,843 - myprogram - INFO - Starting to do something
+2022-04-26 11:25:22,845 - myprogram - WARNING - I must have fallen asleep
+2022-04-26 11:25:22,845 - myprogram - WARNING - I must have fallen asleep
+2022-04-26 11:25:22,846 - myprogram - DEBUG - The calculation result is 4950
+2022-04-26 11:25:22,846 - myprogram - DEBUG - The calculation result is 4950
+2022-04-26 11:25:22,846 - myprogram - INFO - Done with doing something
+2022-04-26 11:25:22,846 - myprogram - INFO - Done with doing something
+2022-04-26 11:25:30,225 - myprogram - INFO - Starting to do something
+2022-04-26 11:25:31,227 - myprogram - WARNING - I must have fallen asleep
+2022-04-26 11:25:31,228 - myprogram - DEBUG - The calculation result is 4950
+2022-04-26 11:25:31,228 - myprogram - INFO - Done with doing something
+2022-04-26 11:26:00,148 - myprogram - INFO - Starting to do something
+2022-04-26 11:26:01,149 - myprogram - WARNING - I must have fallen asleep
+2022-04-26 11:26:01,150 - myprogram - DEBUG - The calculation result is 4950
+2022-04-26 11:26:01,150 - myprogram - INFO - Done with doing something
+2022-05-16 15:03:17,577 - myprogram - INFO - Starting to do something
+2022-05-16 15:03:18,579 - myprogram - WARNING - I must have fallen asleep
+2022-05-16 15:03:18,580 - myprogram - DEBUG - The calculation result is 4950
+2022-05-16 15:03:18,580 - myprogram - INFO - Done with doing something
+2022-05-16 15:05:02,111 - myprogram - INFO - Starting to do something
+2022-05-16 15:05:02,111 - myprogram - INFO - Starting to do something
+2022-05-16 15:05:03,112 - myprogram - WARNING - I must have fallen asleep
+2022-05-16 15:05:03,112 - myprogram - WARNING - I must have fallen asleep
+2022-05-16 15:05:03,112 - myprogram - INFO - Done with doing something
+2022-05-16 15:05:03,112 - myprogram - INFO - Done with doing something
+2022-05-16 15:06:26,292 - myprogram - INFO - Starting to do something
+2022-05-16 15:06:26,292 - myprogram - INFO - Starting to do something
+2022-05-16 15:06:26,292 - myprogram - INFO - Starting to do something
+2022-05-16 15:06:27,294 - myprogram - WARNING - I must have fallen asleep
+2022-05-16 15:06:27,294 - myprogram - WARNING - I must have fallen asleep
+2022-05-16 15:06:27,294 - myprogram - WARNING - I must have fallen asleep
+2022-05-16 15:06:27,294 - myprogram - DEBUG - The calculation result is 4950
+2022-05-16 15:06:27,294 - myprogram - DEBUG - The calculation result is 4950
+2022-05-16 15:06:27,294 - myprogram - INFO - Done with doing something
+2022-05-16 15:06:27,294 - myprogram - INFO - Done with doing something
+2022-05-16 15:06:27,294 - myprogram - INFO - Done with doing something
diff --git a/output.txt b/output.txt
new file mode 100644
index 0000000000000000000000000000000000000000..cf04fc5b676a8cdb2c278f737f7b3ec7f0f8de13
--- /dev/null
+++ b/output.txt
@@ -0,0 +1,100 @@
+1
+3
+5
+7
+9
+11
+13
+15
+17
+19
+21
+23
+25
+27
+29
+31
+33
+35
+37
+39
+41
+43
+45
+47
+49
+51
+53
+55
+57
+59
+61
+63
+65
+67
+69
+71
+73
+75
+77
+79
+81
+83
+85
+87
+89
+91
+93
+95
+97
+99
+1
+3
+5
+7
+9
+11
+13
+15
+17
+19
+21
+23
+25
+27
+29
+31
+33
+35
+37
+39
+41
+43
+45
+47
+49
+51
+53
+55
+57
+59
+61
+63
+65
+67
+69
+71
+73
+75
+77
+79
+81
+83
+85
+87
+89
+91
+93
+95
+97
+99
diff --git a/stuff.txt b/stuff.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6eca1739b6baf8815d8659a76b21bdd0fc63bec7
--- /dev/null
+++ b/stuff.txt
@@ -0,0 +1,3 @@
+Astron
+Jive
+Nova