diff --git a/python/introduction/README.md b/python/introduction/README.md
new file mode 100644
index 0000000..5656aba
--- /dev/null
+++ b/python/introduction/README.md
@@ -0,0 +1,434 @@
+# Introduction to Learning Python
+
+Best place to start is the documentation [www.python.org](https:#www.python.org/)
+
+Download [Python 3](https:#www.python.org/downloads/)
+
+Docker images for Python on [Docker Hub](https:#hub.docker.com/_/python)
+
+
+# Run Python in Docker
+
+We can also run python in a docker container:
+
+```
+cd python\introduction
+
+docker build --target dev . -t python
+docker run -it -v ${PWD}:/work python sh
+
+/work # python --version
+Python 3.9.
+
+```
+
+# Our first Program
+
+* Create a folder containing our application source code
+
+```
+mkdir src
+
+```
+
+* Create basic source code
+
+In the `src` folder, create a program called `app.py`
+Paste the following content into it.
+
+```
+
+def main():
+ print("Hello, world.")
+
+main()
+```
+
+* Run your application code
+
+You can run your application
+
+```
+/work # python src/app.py
+Hello, world.
+```
+
+# The Code
+
+In the video we cover writing functions.
+It allows us to execute a block of code
+You want to give your function a single purpose
+Functions can have an input and return an output
+Well thought out functions makes it easier to write tests
+
+Instead of doing a boring `x + y` function that adds two numbers, let's do something a little
+more realistic but still basic:
+
+```
+# This function returns some data
+# The data could be coming from a database, or a file.
+# The person calling the function should not care
+# how the data is retrieved.
+# Since the function does not leak its Data provider
+
+def getCustomer():
+ return "Marcel Dempers"
+
+print(getCustomer())
+
+```
+
+# Function inputs
+
+As we saw, functions can return outputs, and also take inputs.
+Let's accept a customer ID and return a customer record with that ID:
+
+```
+def getCustomer(customerID):
+ return "CustomerID: " + customerID + ", Name: Marcel Dempers"
+
+print(getCustomer("abc"))
+```
+
+
+## Variables
+
+To hold data in programming languages, we use variables.
+Variables take up space in memory, so we want to keep it minimal.
+Let's declare variables in our function:
+
+```
+def getCustomer(customerID):
+ firstName = "Marcel"
+ lastName = "Dempers"
+ fullName = firstName + " " + lastName
+
+ return fullName
+
+ #or we can return the computation instead of adding another variable!
+ return firstName + " " + lastName
+
+ #or we don't even need to declare variables :)
+ return "Marcel Dempers"
+
+customer = getCustomer("abc")
+print(customer)
+
+```
+
+## Control Flows (if\else)
+
+You can see we're not using the `customerId` input in our function.
+Let's use it and showcase control flows to create logic!
+
+Control flows allow us to add "rules" to our code.
+"If this is the case, then do that, else do something else".
+
+So let's say we have a customer ID 1 coming in, we may only want to
+return our customer if it matches the `customerID`
+
+```
+
+def getCustomer(customerID):
+ if customerID == "abc":
+ return "Marcel Dempers"
+ elif customerID == "def":
+ return "Bob Smith"
+ else:
+ return ""
+
+customer = getCustomer("abc")
+print(customer)
+
+```
+
+Let's invoke our function :
+
+```
+marcel = getCustomer("abc")
+print(marcel)
+
+bob = getCustomer("def")
+print(bob)
+
+```
+
+## Arrays
+
+Our function can only return one customer at a time.
+What if we wanted to return more than one customer.
+In real world systems, we may return database records.
+This is where we start looking at arrays, lists, dictionaries etc.
+
+Let's add another function to get an array of customers!
+
+```
+def getCustomers():
+ customers = [ "Marcel Dempers", "Bob Smith" ]
+ return customers
+
+customers = getCustomers()
+print(customers)
+```
+
+You can get array values by index starting at `0`:
+
+```
+customers = getCustomers()
+
+marcel = customers[0]
+bob = customers[1]
+
+print(marcel + "\n" + bob)
+
+```
+
+We can set a value in our Array.
+Arrays are fixed in size, notice we cannot just access a
+record :
+
+```
+customers[2] = "John Smith"
+IndexError: list assignment index out of range
+
+```
+
+Instead we can use the `append()` function of the array to add items.
+
+```
+def getCustomers():
+ customers = [ "Marcel Dempers", "Bob Smith" ]
+ customers.append("James Baker")
+ customers.append("Jonathan D")
+ customers.append("Aleem Janmohamed")
+ customers.append("Ivo Galic")
+ customers.append("Joel Griffiths")
+ customers.append("Michael Spinks")
+ customers.append("Victor Savkov")
+ return customers
+
+```
+
+We can remove items with the `remove()` function.
+
+```
+customers.remove("Bob Smith")
+```
+
+# Dictionaries
+
+Dictionaries have an advantage whereby items can be found using a key.
+With arrays, we have to know the index number where the item is located or else we cannot find items unless we write a loop.
+With Dictionaries, we store key value pairs. So the key, may be our customer ID.
+
+Let's replace our customer array with a dictionary
+```
+ customers = {
+ "a": "James Baker",
+ "b": "Jonathan D",
+ "c": "Aleem Janmohamed",
+ "d": "Ivo Galic",
+ "e": "Joel Griffiths",
+ "f": "Michael Spinks",
+ "g": "Victor Savkov"
+ }
+
+```
+
+We can also set and access an item by key
+
+```
+#update or create item
+customers["h"] = "Marcel Dempers"
+
+#get an item
+marcel = customers["h"]
+print(marcel)
+
+```
+
+Let's update our `getCustomer()` function to get customer from our dictionary
+
+```
+def getCustomer(customerID):
+ customer = getCustomers()
+ return customer[customerID]
+
+def getCustomers():
+ customers = {
+ "a": "James Baker",
+ "b": "Jonathan D",
+ "c": "Aleem Janmohamed",
+ "d": "Ivo Galic",
+ "e": "Joel Griffiths",
+ "f": "Michael Spinks",
+ "g": "Victor Savkov",
+ "h" : "Marcel Dempers"
+ }
+
+ return customers
+
+customers = getCustomers()
+customer = getCustomer("h")
+
+print(customer)
+print(customers)
+```
+
+## Loops
+
+Loops are used to iterate over collections, lists, arrays etc.
+There are two primitive loops in Python
+Let's say we need to loop through our customers
+
+### While loop
+
+While loops are simple to write by condition:
+
+```
+i = 0
+while i < 7:
+ print(customers[i])
+ i += 1
+```
+
+We can also use this condition to loop arrays:
+
+```
+customers = [ "Marcel Dempers", "Bob Smith" ]
+customers.append("James Baker")
+customers.append("Jonathan D")
+customers.append("Aleem Janmohamed")
+customers.append("Ivo Galic")
+customers.append("Joel Griffiths")
+customers.append("Michael Spinks")
+customers.append("Victor Savkov")
+i = 0
+while i <= 8:
+ print(customers[i])
+ i += 1
+```
+
+### For loops
+
+For looping a dictionary of list, `for` loops are a little easier:
+
+```
+customers = getCustomers()
+
+for customerID in customers:
+ print(customers[customerID])
+```
+
+## Classes and Objects
+
+So far so good, however, customer data is not useful as strings.
+Customers can have a firstname, lastname, and more properties.
+
+For this purpose we'd like to group some variables into a single variable.
+This is what `classes` allows us to do.
+Let's create a `class` for our customer
+
+```
+class Customer:
+ customerID = ""
+ firstName = ""
+ lastName = ""
+ def fullName(self):
+ return self.firstName + " " + self.lastName
+
+marcel = Customer()
+marcel.firstName = "Marcel"
+marcel.lastName = "Dempers"
+marcel.customerID = "h"
+
+print(marcel.fullName())
+
+```
+
+Now how can we create an object with our values without having to set every
+value individually ?
+Constructors allow us to initialise our object with some default values.
+
+```
+class Customer:
+ def __init__(self, c="", f="", l=""):
+ self.customerID = c
+ self.firstName = f
+ self.lastName = l
+ def fullName(self):
+ return self.firstName + " " + self.lastName
+
+marcel = Customer("h", "Marcel", "Dempers")
+
+print(marcel.fullName())
+
+```
+
+Let's plug our class into our current application and put it all together:
+Firstly, we convert all our customers from strings to objects:
+
+```
+def getCustomers():
+ customers = {
+ "a": Customer("a","James", "Baker"),
+ "b": Customer("b", "Jonathan", "D"),
+ "c": Customer("c", "Aleem", "Janmohamed"),
+ "d": Customer("d", "Ivo", "Galic"),
+ "e": Customer("e", "Joel", "Griffiths"),
+ "f": Customer("f", "Michael", "Spinks"),
+ "g": Customer("g", "Victor", "Savkov"),
+ "h" : Customer("h", "Marcel", "Dempers")
+ }
+
+ return customers
+```
+
+Then we can proceed to get our customer and access their fields:
+
+```
+customers = getCustomers()
+for customerID in customers:
+ print(customers[customerID].fullName())
+```
+
+# Docker
+
+So far we have used our container as a development environment.
+Docker allows us to run python in an isolated environment.
+Docker also has a concept of stages, I.E Docker Multistage.
+
+Therefore we may have a stage for development where we simply mount source code to access a python environment. In addition to this, we may add another layer for debugging where we can install debugger and extra development tools which we
+do not want in production.
+Finally we can create a smaller stage which contains only python + our source which we can run as a runtime image in production.
+
+Example:
+
+```
+FROM python:3.9.6-alpine3.13 as dev
+
+WORKDIR /work
+
+FROM python:3.9.6-alpine3.13 as debugging
+
+# add a debugger
+
+FROM dev as runtime
+COPY ./src/ /app
+
+ENTRYPOINT [ "python", "/app/app.py" ]
+
+```
+
+## Building the Container
+
+```
+docker build . -t customer-app
+
+```
+
+## Running the Container
+
+```
+docker run customer-app
+```
\ No newline at end of file
diff --git a/python/introduction/dockerfile b/python/introduction/dockerfile
new file mode 100644
index 0000000..d7a8337
--- /dev/null
+++ b/python/introduction/dockerfile
@@ -0,0 +1,12 @@
+FROM python:3.9.6-alpine3.13 as dev
+
+WORKDIR /work
+
+FROM python:3.9.6-alpine3.13 as debugging
+
+# add a debugger
+
+FROM dev as runtime
+COPY ./src/ /app
+
+ENTRYPOINT [ "python", "/app/app.py" ]
diff --git a/python/introduction/src/app.py b/python/introduction/src/app.py
new file mode 100644
index 0000000..a26d2c9
--- /dev/null
+++ b/python/introduction/src/app.py
@@ -0,0 +1,33 @@
+def getCustomer(customerID):
+ customer = getCustomers()
+ return customer[customerID]
+
+def getCustomers():
+ customers = {
+ "a": Customer("a","James", "Baker"),
+ "b": Customer("b", "Jonathan", "D"),
+ "c": Customer("c", "Aleem", "Janmohamed"),
+ "d": Customer("d", "Ivo", "Galic"),
+ "e": Customer("e", "Joel", "Griffiths"),
+ "f": Customer("f", "Michael", "Spinks"),
+ "g": Customer("g", "Victor", "Savkov"),
+ "h" : Customer("h", "Marcel", "Dempers")
+ }
+
+ return customers
+
+class Customer:
+ def __init__(self, c="", f="", l=""):
+ self.customerID = c
+ self.firstName = f
+ self.lastName = l
+ def fullName(self):
+ return self.firstName + " " + self.lastName
+
+customers = getCustomers()
+for customerID in customers:
+ print(customers[customerID].fullName())
+
+
+
+