diff --git a/README.md b/README.md
index 003a8506907312d2adf2eaf8dced61cf8119f45a..c3ca8ace3c12aaf0c3598b2f4c18cf3d3999be3b 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,62 @@
 
 
 This repository contains Jupyter notebooks and data for the 2018 WIN PyTreat.
+It contains two sets of practicals:
+
+- The `getting_started` directory contains a series of practicals intended
+  for those of you who are new to the Python programming language, or need
+  a refresher.
+
+- The `advanced_topics` directory contains a series of practicals on various
+  aspects of the Python programming language - these are intended for those
+  of you who are familiar with the basics of Python, and want to learn more
+  about the language.
+
+
+These practicals have been written under the assumption that FSL 5.0.10 is
+installed.
+
+
+## For attendees
+
+
+To run these notebooks in the `fslpython` environment, you must first install
+jupyter:
+
+
+```
+# If your FSL installation requires administrative privileges to modify, then
+# you MUST run these commands as root - don't just prefix each individual
+# command with sudo, or you will probably install jupyter into the wrong
+# location!
+#
+# One further complication - once you have become root, $FSLDIR may not be set,
+# so either set it as we have ione below, or make sure that it is set, before
+# proceeding.
+sudo su
+export FSLDIR=/usr/local/fsl
+source $FSLDIR/fslpython/bin/activate fslpython
+conda install jupyter
+source deactivate
+ln -s $FSLDIR/fslpython/envs/fslpython/bin/jupyter $FSLDIR/bin/fsljupyter
+```
+
+
+Then, clone this repository on your local machine, and run
+`fsljupyter notebook`:
+
+
+```
+git clone git@git.fmrib.ox.ac.uk:fsl/pytreat-2018-practicals.git
+cd pytreat-2018-practicals
+fsljupyter notebook
+```
+
+
+Have fun!
+
+
+## For contributors
 
 
 The upstream repository can be found at:
@@ -39,44 +95,27 @@ To contribute to the practicals:
    repository.
 
 
-To run these notebooks in the `fslpython` environment, you must first install
-jupyter:
-
+When you install `jupyter` above, you may also wish to install
+[`notedown`](https://github.com/aaren/notedown):
 
 ```
-# If your FSL installation requires administrative privileges to modify, then
-# you MUST run these commands as root - don't just prefix each individual
-# command with sudo, or you will probably install jupyter into the wrong
-# location!
-#
-# One further complication - once you have become root, $FSLDIR may not be set,
-# so either set it as we have ione below, or make sure that it is set, before
-# proceeding.
-sudo su
-export FSLDIR=/usr/local/fsl
-source $FSLDIR/fslpython/bin/activate fslpython
+# .
+# see instructions above
+# .
 conda install jupyter
 pip install notedown
 source deactivate
-ln -s $FSLDIR/fslpython/envs/fslpython/bin/jupyter $FSLDIR/bin/fsljupyter
+ln -s $FSLDIR/fslpython/envs/fslpython/bin/jupyter  $FSLDIR/bin/fsljupyter
 ln -s $FSLDIR/fslpython/envs/fslpython/bin/notedown $FSLDIR/bin/fslnotedown
 ```
 
+`notedown` is a handy tool which allows you to convert a markdown (`.md`) file
+to a Jupyter notebook (`.ipynb`) file. So you can write your practical in your
+text editor of choice, and then convert it into a notebook, instead of writing
+the practical in the web browser interface. If you install notedown as
+suggested in the code block above, you can run it on a markdown file like so:
 
-> [`notedown`](https://github.com/aaren/notedown) is a handy tool which allows
-> you to convert a markdown (`.md`) file to a Jupyter notebook (`.ipynb`)
-> file. So you can write your practical in your text editor of choice, and
-> then convert it into a notebook, instead of writing the practical in the web
-> browser interface. If you install notedown as suggested in the code block
-> above, you can run it on a markdown file like so:
->
-> ```
-> fslnotedown my_markdown_file.md > my_notebook.ipynb
-> ```
-
-
-Now you can start the notebook server from the repository root:
 
 ```
-fsljupyter notebook
+fslnotedown my_markdown_file.md > my_notebook.ipynb
 ```
diff --git a/advanced_topics/README.md b/advanced_topics/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..86d80881f592e895e31dfc360220b1df3eea6925
--- /dev/null
+++ b/advanced_topics/README.md
@@ -0,0 +1,16 @@
+Advanced Python
+===============
+
+This directory contains a collection of practicals, each of which covers a
+particular feature of the Python Programming language. They are intended for
+people who are familiar with the basics of Python, and want to learn about
+some of the more advanced features of the language.
+
+Practicals on the following topics are available:
+
+* Function inputs and outputs
+* Modules and packages
+* Object-oriented programming
+* Operator overloading
+* Decorators
+* Context managers
diff --git a/advanced_topics/function_inputs_and_outputs.ipynb b/advanced_topics/function_inputs_and_outputs.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..d18fc09a26b3d57104362c75f46938c0ac4e4a65
--- /dev/null
+++ b/advanced_topics/function_inputs_and_outputs.ipynb
@@ -0,0 +1,370 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Function inputs and outputs\n",
+    "\n",
+    "\n",
+    "In Python, arguments to a function can be specified in two different ways - by\n",
+    "using _positional_ arguments, or by using _keyword_ arguments.\n",
+    "\n",
+    "\n",
+    "## Positional arguments\n",
+    "\n",
+    "\n",
+    "Let's say we have a function that looks like this"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def myfunc(a, b, c):\n",
+    "   print('First argument: ', a)\n",
+    "   print('Second argument:', b)\n",
+    "   print('Third argument: ', c)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "If we call this function like so:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "myfunc(1, 2, 3)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The values `1`, `2` and `3` get assigned to arguments `a`, `b`, and `c`\n",
+    "respectively, based on the position in which they are passed.\n",
+    "\n",
+    "\n",
+    "Python allows us to pass positional arguments into a function from a sequence,\n",
+    "using the star (`*`) operator. So we could store our arguments in a list or\n",
+    "tuple, and then pass the list straight in:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "args = [3, 4, 5]\n",
+    "myfunc(*args)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "You can think of the star operator as _unpacking_ the contents of the\n",
+    "sequence.\n",
+    "\n",
+    "\n",
+    "## Keyword arguments\n",
+    "\n",
+    "\n",
+    "Using keyword arguments allows us to pass arguments to a function in any order\n",
+    "we like.  We could just as easily call our `myfunc` function like so, and get\n",
+    "the same result that we did earlier when using positional arguments:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "myfunc(c=3, b=2, a=1)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Python has another operator - the double-star (`**`), which will unpack\n",
+    "keyword arguments from a `dict`. For example:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "kwargs = {'a' : 4, 'b' : 5, 'c' : 6}\n",
+    "myfunc(**kwargs)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Combining positional and keyword arguments\n",
+    "\n",
+    "\n",
+    "In fact, we can use both of these techniques at once, like so:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "args   = (100, 200)\n",
+    "kwargs = {'c' : 300}\n",
+    "\n",
+    "myfunc(*args, **kwargs)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Default argument values\n",
+    "\n",
+    "\n",
+    "Function arguments can be given default values, like so:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def myfunc(a=1, b=2, c=3):\n",
+    "    print('First argument: ', a)\n",
+    "    print('Second argument:', b)\n",
+    "    print('Third argument: ', c)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now we can call `myfunc`, only passing the arguments that we need to. The\n",
+    "arguments which are unspecified in the function call will be assigned their\n",
+    "default value:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "myfunc()\n",
+    "myfunc(10)\n",
+    "myfunc(10, b=30)\n",
+    "myfunc(c=300)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "__WARNING:__ _Never_ define a function with a mutable default value, such as a\n",
+    "`list`, `dict` or other non-primitive type. Let's see what happens when we do:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def badfunc(a=[]):\n",
+    "    a.append('end of sequence')\n",
+    "    output = ', '.join([str(elem) for elem in a])\n",
+    "    print(output)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "With this function, all is well and good if we pass in our own value for `a`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "badfunc([1, 2, 3, 4])\n",
+    "badfunc([2, 4, 6])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "But what happens when we let `badfunc` use the default value for `a`?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "badfunc()\n",
+    "badfunc()\n",
+    "badfunc()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This happens because default argument values are created when the function is\n",
+    "defined, and will persist for the duration of your program. So in this\n",
+    "example, the default value for `a`, a Python `list`, gets created when\n",
+    "`badfunc` is defined, and hangs around for the lifetime of the `badfunc`\n",
+    "function!\n",
+    "\n",
+    "\n",
+    "## Variable numbers of arguments - `args` and `kwargs`\n",
+    "\n",
+    "\n",
+    "The `*` and `**` operators can also be used in function definitions - this\n",
+    "indicates that a function may accept a variable number of arguments.\n",
+    "\n",
+    "\n",
+    "Let's redefine `myfunc` to accept any number of positional arguments - here,\n",
+    "all positional arguments will be passed into `myfunc` as a tuple called\n",
+    "`args`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def myfunc(*args):\n",
+    "    print('myfunc({})'.format(args))\n",
+    "    print('  Number of arguments: {}'.format(len(args)))\n",
+    "    for i, arg in enumerate(args):\n",
+    "        print('  Argument {:2d}: {}'.format(i, arg))\n",
+    "\n",
+    "myfunc()\n",
+    "myfunc(1)\n",
+    "myfunc(1, 2, 3)\n",
+    "myfunc(1, 'a', [3, 4])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Similarly, we can define a function to accept any number of keyword\n",
+    "arguments. In this case, the keyword arguments will be packed into a `dict`\n",
+    "called `kwargs`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def myfunc(**kwargs):\n",
+    "    print('myfunc({})'.format(kwargs))\n",
+    "    for k, v in kwargs.items():\n",
+    "        print('  Argument {} = {}'.format(k, v))\n",
+    "\n",
+    "myfunc()\n",
+    "myfunc(a=1, b=2)\n",
+    "myfunc(a='abc', foo=123)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This is a useful technique in many circumstances. For example, if you are\n",
+    "writing a function which calls another function that takes many arguments, you\n",
+    "can use ``**kwargs`` to pass-through arguments to the second function.  As an\n",
+    "example, let's say we have functions `flirt` and `fnirt`, which respectively\n",
+    "perform linear and non-linear registration:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def flirt(infile,\n",
+    "          ref,\n",
+    "          outfile=None,\n",
+    "          init=None,\n",
+    "          omat=None,\n",
+    "          dof=12):\n",
+    "    # TODO get MJ to fill this bit in\n",
+    "    pass\n",
+    "\n",
+    "def fnirt(infile,\n",
+    "          ref,\n",
+    "          outfile=None,\n",
+    "          aff=None,\n",
+    "          interp='nn',\n",
+    "          refmask=None,\n",
+    "          minmet='lg',\n",
+    "          subsamp=4):\n",
+    "    # TODO get Jesper to fill this bit in\n",
+    "    pass"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We want to write our own registration function which uses the `flirt` and\n",
+    "`fnirt` functions, while also allowing the `fnirt` parameters to be\n",
+    "customised. We can use `**kwargs` to do this:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def do_nonlinear_reg(infile, ref, outfile, **kwargs):\n",
+    "    \"\"\"Aligns infile to ref using non-linear registration. All keyword\n",
+    "    arguments are passed through to the fnirt function.\n",
+    "    \"\"\"\n",
+    "\n",
+    "    affmat = '/tmp/aff.mat'\n",
+    "\n",
+    "    # calculate a rough initial linear alignemnt\n",
+    "    flirt(infile, ref, omat=affmat)\n",
+    "\n",
+    "    fnirt(infile, ref, outfile, aff=affmat, **kwargs)"
+   ]
+  }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/advanced_topics/function_inputs_and_outputs.md b/advanced_topics/function_inputs_and_outputs.md
new file mode 100644
index 0000000000000000000000000000000000000000..234cea469dfa5ee0ac20e6f4d2c416c4c81cedd9
--- /dev/null
+++ b/advanced_topics/function_inputs_and_outputs.md
@@ -0,0 +1,237 @@
+# Function inputs and outputs
+
+
+In Python, arguments to a function can be specified in two different ways - by
+using _positional_ arguments, or by using _keyword_ arguments.
+
+
+## Positional arguments
+
+
+Let's say we have a function that looks like this
+
+
+```
+def myfunc(a, b, c):
+   print('First argument: ', a)
+   print('Second argument:', b)
+   print('Third argument: ', c)
+```
+
+
+If we call this function like so:
+
+
+```
+myfunc(1, 2, 3)
+```
+
+
+The values `1`, `2` and `3` get assigned to arguments `a`, `b`, and `c`
+respectively, based on the position in which they are passed.
+
+
+Python allows us to pass positional arguments into a function from a sequence,
+using the star (`*`) operator. So we could store our arguments in a list or
+tuple, and then pass the list straight in:
+
+```
+args = [3, 4, 5]
+myfunc(*args)
+```
+
+You can think of the star operator as _unpacking_ the contents of the
+sequence.
+
+
+## Keyword arguments
+
+
+Using keyword arguments allows us to pass arguments to a function in any order
+we like.  We could just as easily call our `myfunc` function like so, and get
+the same result that we did earlier when using positional arguments:
+
+
+```
+myfunc(c=3, b=2, a=1)
+```
+
+
+Python has another operator - the double-star (`**`), which will unpack
+keyword arguments from a `dict`. For example:
+
+```
+kwargs = {'a' : 4, 'b' : 5, 'c' : 6}
+myfunc(**kwargs)
+```
+
+
+## Combining positional and keyword arguments
+
+
+In fact, we can use both of these techniques at once, like so:
+
+```
+args   = (100, 200)
+kwargs = {'c' : 300}
+
+myfunc(*args, **kwargs)
+```
+
+
+## Default argument values
+
+
+Function arguments can be given default values, like so:
+
+
+```
+def myfunc(a=1, b=2, c=3):
+    print('First argument: ', a)
+    print('Second argument:', b)
+    print('Third argument: ', c)
+```
+
+
+Now we can call `myfunc`, only passing the arguments that we need to. The
+arguments which are unspecified in the function call will be assigned their
+default value:
+
+
+```
+myfunc()
+myfunc(10)
+myfunc(10, b=30)
+myfunc(c=300)
+```
+
+
+__WARNING:__ _Never_ define a function with a mutable default value, such as a
+`list`, `dict` or other non-primitive type. Let's see what happens when we do:
+
+
+```
+def badfunc(a=[]):
+    a.append('end of sequence')
+    output = ', '.join([str(elem) for elem in a])
+    print(output)
+```
+
+
+With this function, all is well and good if we pass in our own value for `a`:
+
+
+```
+badfunc([1, 2, 3, 4])
+badfunc([2, 4, 6])
+```
+
+
+But what happens when we let `badfunc` use the default value for `a`?
+
+
+```
+badfunc()
+badfunc()
+badfunc()
+```
+
+
+This happens because default argument values are created when the function is
+defined, and will persist for the duration of your program. So in this
+example, the default value for `a`, a Python `list`, gets created when
+`badfunc` is defined, and hangs around for the lifetime of the `badfunc`
+function!
+
+
+## Variable numbers of arguments - `args` and `kwargs`
+
+
+The `*` and `**` operators can also be used in function definitions - this
+indicates that a function may accept a variable number of arguments.
+
+
+Let's redefine `myfunc` to accept any number of positional arguments - here,
+all positional arguments will be passed into `myfunc` as a tuple called
+`args`:
+
+
+```
+def myfunc(*args):
+    print('myfunc({})'.format(args))
+    print('  Number of arguments: {}'.format(len(args)))
+    for i, arg in enumerate(args):
+        print('  Argument {:2d}: {}'.format(i, arg))
+
+myfunc()
+myfunc(1)
+myfunc(1, 2, 3)
+myfunc(1, 'a', [3, 4])
+```
+
+
+Similarly, we can define a function to accept any number of keyword
+arguments. In this case, the keyword arguments will be packed into a `dict`
+called `kwargs`:
+
+
+```
+def myfunc(**kwargs):
+    print('myfunc({})'.format(kwargs))
+    for k, v in kwargs.items():
+        print('  Argument {} = {}'.format(k, v))
+
+myfunc()
+myfunc(a=1, b=2)
+myfunc(a='abc', foo=123)
+```
+
+
+This is a useful technique in many circumstances. For example, if you are
+writing a function which calls another function that takes many arguments, you
+can use ``**kwargs`` to pass-through arguments to the second function.  As an
+example, let's say we have functions `flirt` and `fnirt`, which respectively
+perform linear and non-linear registration:
+
+
+```
+def flirt(infile,
+          ref,
+          outfile=None,
+          init=None,
+          omat=None,
+          dof=12):
+    # TODO get MJ to fill this bit in
+    pass
+
+def fnirt(infile,
+          ref,
+          outfile=None,
+          aff=None,
+          interp='nn',
+          refmask=None,
+          minmet='lg',
+          subsamp=4):
+    # TODO get Jesper to fill this bit in
+    pass
+```
+
+
+We want to write our own registration function which uses the `flirt` and
+`fnirt` functions, while also allowing the `fnirt` parameters to be
+customised. We can use `**kwargs` to do this:
+
+
+```
+def do_nonlinear_reg(infile, ref, outfile, **kwargs):
+    """Aligns infile to ref using non-linear registration. All keyword
+    arguments are passed through to the fnirt function.
+    """
+
+    affmat = '/tmp/aff.mat'
+
+    # calculate a rough initial linear alignemnt
+    flirt(infile, ref, omat=affmat)
+
+    fnirt(infile, ref, outfile, aff=affmat, **kwargs)
+```
diff --git a/advanced_topics/modules_and_packages.ipynb b/advanced_topics/modules_and_packages.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..cb1f719189b60dd94728baebb94c4bf7d1dde077
--- /dev/null
+++ b/advanced_topics/modules_and_packages.ipynb
@@ -0,0 +1,518 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Modules and packages\n",
+    "\n",
+    "\n",
+    "Python gives you a lot of flexibility in how you organise your code. If you\n",
+    "want, you can write a Python program just as you would write a Bash script.\n",
+    "You don't _have_ to use functions, classes, modules or packages if you don't\n",
+    "want to, or if the script's task does not require them.\n",
+    "\n",
+    "\n",
+    "But when your code starts to grow beyond what can reasonably be defined in a\n",
+    "single file, you will (hopefully) want to start arranging it in a more\n",
+    "understandable manner.\n",
+    "\n",
+    "\n",
+    "For this practical we have prepared a handful of example files - you can find\n",
+    "them alongside this notebook file, in a directory called\n",
+    "`modules_and_packages/`.\n",
+    "\n",
+    "\n",
+    "## Contents\n",
+    "\n",
+    "* [What is a module?](#what-is-a-module)\n",
+    "* [Importing modules](#importing-modules)\n",
+    " * [Importing specific items from a module](#importing-specific-items-from-a-module)\n",
+    " * [Importing everything from a module](#importing-everything-from-a-module)\n",
+    " * [Module aliases](#module-aliases)\n",
+    " * [What happens when I import a module?](#what-happens-when-i-import-a-module)\n",
+    " * [How can I make my own modules importable?](#how-can-i-make-my-own-modules-importable)\n",
+    "* [Modules versus scripts](#modules-versus-scripts)\n",
+    "* [What is a package?](#what-is-a-package)\n",
+    " * [`__init__.py`](#init-py)\n",
+    "* [Useful references](#useful-references)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import os\n",
+    "os.chdir('modules_and_packages')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"what-is-a-module\"></a>\n",
+    "## What is a module?\n",
+    "\n",
+    "\n",
+    "Any file ending with `.py` is considered to be a module in Python. Take a look\n",
+    "at `modules_and_packages/numfuncs.py` - either open it in your editor, or run\n",
+    "this code block:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "with open('numfuncs.py', 'rt') as f:\n",
+    "    for line in f:\n",
+    "        print(line.rstrip())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This is a perfectly valid Python module, although not a particularly useful\n",
+    "one. It contains an attribute called `PI`, and a function `add`.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"importing-modules\"></a>\n",
+    "## Importing modules\n",
+    "\n",
+    "\n",
+    "Before we can use our module, we must `import` it. Importing a module in\n",
+    "Python will make its contents available to the local scope.  We can import the\n",
+    "contents of `mymodule` like so:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numfuncs"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This imports `numfuncs` into the local scope - everything defined in the\n",
+    "`numfuncs` module can be accessed by prefixing it with `numfuncs.`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print('PI:', numfuncs.PI)\n",
+    "print(numfuncs.add(1, 50))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "There are a couple of other ways to import items from a module...\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"importing-specific-items-from-a-module\"></a>\n",
+    "### Importing specific items from a module\n",
+    "\n",
+    "\n",
+    "If you only want to use one, or a few items from a module, you can import just\n",
+    "those items - a reference to just those items will be created in the local\n",
+    "scope:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from numfuncs import add\n",
+    "print(add(1, 3))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"importing-everything-from-a-module\"></a>\n",
+    "### Importing everything from a module\n",
+    "\n",
+    "\n",
+    "It is possible to import _everything_ that is defined in a module like so:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from numfuncs import *\n",
+    "print('PI: ', PI)\n",
+    "print(add(1, 5))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "__PLEASE DON'T DO THIS!__ Because every time you do, somewhere in the world, a\n",
+    "software developer will will spontaneously stub his/her toe, and start crying.\n",
+    "Using this approach can make more complicated programs very difficult to read,\n",
+    "because it is not possible to determine the origin of the functions and\n",
+    "attributes that are being used.\n",
+    "\n",
+    "\n",
+    "And naming collisions are inevitable when importing multiple modules in this\n",
+    "way, making it very difficult for somebody else to figure out what your code\n",
+    "is doing:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from numfuncs import *\n",
+    "from strfuncs import *\n",
+    "\n",
+    "print(add(1, 5))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Instead, it is better to give modules a name when you import them.  While this\n",
+    "requires you to type more code, the benefits of doing this far outweigh the\n",
+    "hassle of typing a few extra characters - it becomes much easier to read and\n",
+    "trace through code when the functions you use are accessed through a namespace\n",
+    "for each module:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numfuncs\n",
+    "import strfuncs\n",
+    "print('number add: ', numfuncs.add(1, 2))\n",
+    "print('string add: ', strfuncs.add(1, 2))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"module-aliases\"></a>\n",
+    "### Module aliases\n",
+    "\n",
+    "\n",
+    "And Python allows you to define an _alias_ for a module when you import it,\n",
+    "so you don't necessarily need to type out the full module name each time\n",
+    "you want to access something inside:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numfuncs as nf\n",
+    "import strfuncs as sf\n",
+    "print('number add: ', nf.add(1, 2))\n",
+    "print('string add: ', sf.add(1, 2))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "You have already seen this in the earlier practicals - here are a few\n",
+    "aliases which have become a de-facto standard for commonly used Python\n",
+    "modules:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import os.path           as op\n",
+    "import numpy             as np\n",
+    "import nibabel           as nib\n",
+    "import matplotlib        as mpl\n",
+    "import matplotlib.pyplot as plt"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"what-happens-when-i-import-a-module\"></a>\n",
+    "### What happens when I import a module?\n",
+    "\n",
+    "\n",
+    "When you `import` a module, the contents of the module file are literally\n",
+    "executed by the Python runtime, exactly the same as if you had typed its\n",
+    "contents into `ipython`. Any attributes, functions, or classes which are\n",
+    "defined in the module will be bundled up into an object that represents the\n",
+    "module, and through which you can access the module's contents.\n",
+    "\n",
+    "\n",
+    "When we typed `import numfuncs` in the examples above, the following events\n",
+    "occurred:\n",
+    "\n",
+    "\n",
+    "1. Python created a `module` object to represent the module.\n",
+    "\n",
+    "2. The `numfuncs.py` file was read and executed, and all of the items defined\n",
+    "   inside `numfuncs.py` (i.e. the `PI` attribute and the `add` function) were\n",
+    "   added to the `module` object.\n",
+    "\n",
+    "3. A local variable called `numfuncs`, pointing to the `module` object,\n",
+    "   was added to the local scope.\n",
+    "\n",
+    "\n",
+    "Because module files are literally executed on import, any statements in the\n",
+    "module file which are not encapsulated inside a class or function will be\n",
+    "executed.  As an example, take a look at the file `sideeffects.py`. Let's\n",
+    "import it and see what happens:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import sideeffects"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Ok, hopefully that wasn't too much of a surprise. Something which may be less\n",
+    "intuitive, however, is that a module's contents will only be executed on the\n",
+    "_first_ time that it is imported. After the first import, Python caches the\n",
+    "module's contents (all loaded modules are accessible through\n",
+    "[`sys.modules`](https://docs.python.org/3.5/library/sys.html#sys.modules)). On\n",
+    "subsequent imports, the cached version of the module is returned:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import sideeffects"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"how-can-i-make-my-own-modules-importable\"></a>\n",
+    "### How can I make my own modules importable?\n",
+    "\n",
+    "\n",
+    "When you `import` a module, Python searches for it in the following locations,\n",
+    "in the following order:\n",
+    "\n",
+    "\n",
+    "1. Built-in modules (e.g. `os`, `sys`, etc.).\n",
+    "2. In the current directory or, if a script has been executed, in the directory\n",
+    "   containing that script.\n",
+    "3. In directories listed in the PYTHONPATH environment variable.\n",
+    "4. In installed third-party libraries (e.g. `numpy`).\n",
+    "\n",
+    "\n",
+    "If you are experimenting or developing your program, the quickest and easiest\n",
+    "way to make your module(s) importable is to add their containing directory to\n",
+    "the `PYTHONPATH`. But if you are developing a larger piece of software, you\n",
+    "should probably organise your modules into _packages_, which are [described\n",
+    "below](#what-is-a-package).\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"modules-versus-scripts\"></a>\n",
+    "## Modules versus scripts\n",
+    "\n",
+    "\n",
+    "You now know that Python treats all files ending in `.py` as importable\n",
+    "modules. But all files ending in `.py` can also be treated as scripts. In\n",
+    "fact, there no difference between a _module_ and a _script_ - any `.py` file\n",
+    "can be executed as a script, or imported as a module, or both.\n",
+    "\n",
+    "\n",
+    "Have a look at the file `modules_and_packages/module_and_script.py`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "with open('module_and_script.py', 'rt') as f:\n",
+    "    for line in f:\n",
+    "        print(line.rstrip())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This file contains two functions `mul` and `main`.  The\n",
+    "`if __name__ == '__main__':` clause at the bottom is a standard trick in Python\n",
+    "that allows you to add code to a file that is _only executed when the module is\n",
+    "called as a script_. Try it in a terminal now:\n",
+    "\n",
+    "\n",
+    "> `python modules_and_packages/module_and_script.py`\n",
+    "\n",
+    "\n",
+    "But if we `import` this module from another file, or from an interactive\n",
+    "session, the code within the `if __name__ == '__main__':` clause will not be\n",
+    "executed, and we can access its functions just like any other module that we\n",
+    "import."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import module_and_script as mas\n",
+    "\n",
+    "a = 1.5\n",
+    "b = 3\n",
+    "\n",
+    "print('mul({}, {}): {}'.format(a, b, mas.mul(a, b)))\n",
+    "print('calling main...')\n",
+    "mas.main([str(a), str(b)])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"what-is-a-package\"></a>\n",
+    "## What is a package?\n",
+    "\n",
+    "\n",
+    "You now know how to split your Python code up into separate files\n",
+    "(a.k.a. _modules_). When your code grows beyond a handful of files, you may\n",
+    "wish for more fine-grained control over the namespaces in which your modules\n",
+    "live. Python has another feature which allows you to organise your modules\n",
+    "into _packages_.\n",
+    "\n",
+    "\n",
+    "A package in Python is simply a directory which:\n",
+    "\n",
+    "\n",
+    "* Contains a special file called `__init__.py`\n",
+    "* May contain one or more module files (any other files ending in `*.py`)\n",
+    "* May contain other package directories.\n",
+    "\n",
+    "\n",
+    "For example, the [FSLeyes](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes)\n",
+    "code is organised into packages and sub-packages as follows (abridged):\n",
+    "\n",
+    "\n",
+    "> ```\n",
+    "> fsleyes/\n",
+    ">     __init__.py\n",
+    ">     main.py\n",
+    ">     frame.py\n",
+    ">     views/\n",
+    ">         __init__.py\n",
+    ">         orthopanel.py\n",
+    ">         lightboxpanel.py\n",
+    ">     controls/\n",
+    ">         __init__.py\n",
+    ">         locationpanel.py\n",
+    ">         overlaylistpanel.py\n",
+    "> ```\n",
+    "\n",
+    "\n",
+    "Within a package structure, we will typically still import modules directly,\n",
+    "via their full path within the package:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import fsleyes.main as fmain\n",
+    "fmain.fsleyes_main()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"init-py\"></a>\n",
+    "### `__init__.py`\n",
+    "\n",
+    "\n",
+    "Every Python package must have an `__init__.py` file. In many cases, this will\n",
+    "actually be an empty file, and you don't need to worry about it any more, apart\n",
+    "from knowing that it is needed. But you can use `__init__.py` to perform some\n",
+    "package-specific initialisation, and/or to customise the package's namespace.\n",
+    "\n",
+    "\n",
+    "As an example, take a look the `modules_and_packages/fsleyes/__init__.py` file\n",
+    "in our mock FSLeyes package. We have imported the `fsleyes_main` function from\n",
+    "the `fsleyes.main` module, making it available at the package level. So\n",
+    "instead of importing the `fsleyes.main` module, we could instead just import\n",
+    "the `fsleyes` package:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import fsleyes\n",
+    "fsleyes.fsleyes_main()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"useful-references\"></a>\n",
+    "## Useful references\n",
+    "\n",
+    "* [Modules and packages in Python](https://docs.python.org/3.5/tutorial/modules.html)\n",
+    "* [Using `__init__.py`](http://mikegrouchy.com/blog/2012/05/be-pythonic-__init__py.html)"
+   ]
+  }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/advanced_topics/modules_and_packages.md b/advanced_topics/modules_and_packages.md
new file mode 100644
index 0000000000000000000000000000000000000000..f459a1ca08b47967a20e53ae2854342da8aa6049
--- /dev/null
+++ b/advanced_topics/modules_and_packages.md
@@ -0,0 +1,371 @@
+# Modules and packages
+
+
+Python gives you a lot of flexibility in how you organise your code. If you
+want, you can write a Python program just as you would write a Bash script.
+You don't _have_ to use functions, classes, modules or packages if you don't
+want to, or if the script's task does not require them.
+
+
+But when your code starts to grow beyond what can reasonably be defined in a
+single file, you will (hopefully) want to start arranging it in a more
+understandable manner.
+
+
+For this practical we have prepared a handful of example files - you can find
+them alongside this notebook file, in a directory called
+`modules_and_packages/`.
+
+
+## Contents
+
+* [What is a module?](#what-is-a-module)
+* [Importing modules](#importing-modules)
+ * [Importing specific items from a module](#importing-specific-items-from-a-module)
+ * [Importing everything from a module](#importing-everything-from-a-module)
+ * [Module aliases](#module-aliases)
+ * [What happens when I import a module?](#what-happens-when-i-import-a-module)
+ * [How can I make my own modules importable?](#how-can-i-make-my-own-modules-importable)
+* [Modules versus scripts](#modules-versus-scripts)
+* [What is a package?](#what-is-a-package)
+ * [`__init__.py`](#init-py)
+* [Useful references](#useful-references)
+
+```
+import os
+os.chdir('modules_and_packages')
+```
+
+<a class="anchor" id="what-is-a-module"></a>
+## What is a module?
+
+
+Any file ending with `.py` is considered to be a module in Python. Take a look
+at `modules_and_packages/numfuncs.py` - either open it in your editor, or run
+this code block:
+
+
+```
+with open('numfuncs.py', 'rt') as f:
+    for line in f:
+        print(line.rstrip())
+```
+
+
+This is a perfectly valid Python module, although not a particularly useful
+one. It contains an attribute called `PI`, and a function `add`.
+
+
+<a class="anchor" id="importing-modules"></a>
+## Importing modules
+
+
+Before we can use our module, we must `import` it. Importing a module in
+Python will make its contents available to the local scope.  We can import the
+contents of `mymodule` like so:
+
+
+```
+import numfuncs
+```
+
+
+This imports `numfuncs` into the local scope - everything defined in the
+`numfuncs` module can be accessed by prefixing it with `numfuncs.`:
+
+
+```
+print('PI:', numfuncs.PI)
+print(numfuncs.add(1, 50))
+```
+
+
+There are a couple of other ways to import items from a module...
+
+
+<a class="anchor" id="importing-specific-items-from-a-module"></a>
+### Importing specific items from a module
+
+
+If you only want to use one, or a few items from a module, you can import just
+those items - a reference to just those items will be created in the local
+scope:
+
+
+```
+from numfuncs import add
+print(add(1, 3))
+```
+
+
+<a class="anchor" id="importing-everything-from-a-module"></a>
+### Importing everything from a module
+
+
+It is possible to import _everything_ that is defined in a module like so:
+
+
+```
+from numfuncs import *
+print('PI: ', PI)
+print(add(1, 5))
+```
+
+
+__PLEASE DON'T DO THIS!__ Because every time you do, somewhere in the world, a
+software developer will will spontaneously stub his/her toe, and start crying.
+Using this approach can make more complicated programs very difficult to read,
+because it is not possible to determine the origin of the functions and
+attributes that are being used.
+
+
+And naming collisions are inevitable when importing multiple modules in this
+way, making it very difficult for somebody else to figure out what your code
+is doing:
+
+
+```
+from numfuncs import *
+from strfuncs import *
+
+print(add(1, 5))
+```
+
+
+Instead, it is better to give modules a name when you import them.  While this
+requires you to type more code, the benefits of doing this far outweigh the
+hassle of typing a few extra characters - it becomes much easier to read and
+trace through code when the functions you use are accessed through a namespace
+for each module:
+
+
+```
+import numfuncs
+import strfuncs
+print('number add: ', numfuncs.add(1, 2))
+print('string add: ', strfuncs.add(1, 2))
+```
+
+<a class="anchor" id="module-aliases"></a>
+### Module aliases
+
+
+And Python allows you to define an _alias_ for a module when you import it,
+so you don't necessarily need to type out the full module name each time
+you want to access something inside:
+
+
+```
+import numfuncs as nf
+import strfuncs as sf
+print('number add: ', nf.add(1, 2))
+print('string add: ', sf.add(1, 2))
+```
+
+
+You have already seen this in the earlier practicals - here are a few
+aliases which have become a de-facto standard for commonly used Python
+modules:
+
+
+```
+import os.path           as op
+import numpy             as np
+import nibabel           as nib
+import matplotlib        as mpl
+import matplotlib.pyplot as plt
+```
+
+<a class="anchor" id="what-happens-when-i-import-a-module"></a>
+### What happens when I import a module?
+
+
+When you `import` a module, the contents of the module file are literally
+executed by the Python runtime, exactly the same as if you had typed its
+contents into `ipython`. Any attributes, functions, or classes which are
+defined in the module will be bundled up into an object that represents the
+module, and through which you can access the module's contents.
+
+
+When we typed `import numfuncs` in the examples above, the following events
+occurred:
+
+
+1. Python created a `module` object to represent the module.
+
+2. The `numfuncs.py` file was read and executed, and all of the items defined
+   inside `numfuncs.py` (i.e. the `PI` attribute and the `add` function) were
+   added to the `module` object.
+
+3. A local variable called `numfuncs`, pointing to the `module` object,
+   was added to the local scope.
+
+
+Because module files are literally executed on import, any statements in the
+module file which are not encapsulated inside a class or function will be
+executed.  As an example, take a look at the file `sideeffects.py`. Let's
+import it and see what happens:
+
+
+```
+import sideeffects
+```
+
+
+Ok, hopefully that wasn't too much of a surprise. Something which may be less
+intuitive, however, is that a module's contents will only be executed on the
+_first_ time that it is imported. After the first import, Python caches the
+module's contents (all loaded modules are accessible through
+[`sys.modules`](https://docs.python.org/3.5/library/sys.html#sys.modules)). On
+subsequent imports, the cached version of the module is returned:
+
+
+```
+import sideeffects
+```
+
+<a class="anchor" id="how-can-i-make-my-own-modules-importable"></a>
+### How can I make my own modules importable?
+
+
+When you `import` a module, Python searches for it in the following locations,
+in the following order:
+
+
+1. Built-in modules (e.g. `os`, `sys`, etc.).
+2. In the current directory or, if a script has been executed, in the directory
+   containing that script.
+3. In directories listed in the PYTHONPATH environment variable.
+4. In installed third-party libraries (e.g. `numpy`).
+
+
+If you are experimenting or developing your program, the quickest and easiest
+way to make your module(s) importable is to add their containing directory to
+the `PYTHONPATH`. But if you are developing a larger piece of software, you
+should probably organise your modules into _packages_, which are [described
+below](#what-is-a-package).
+
+
+<a class="anchor" id="modules-versus-scripts"></a>
+## Modules versus scripts
+
+
+You now know that Python treats all files ending in `.py` as importable
+modules. But all files ending in `.py` can also be treated as scripts. In
+fact, there no difference between a _module_ and a _script_ - any `.py` file
+can be executed as a script, or imported as a module, or both.
+
+
+Have a look at the file `modules_and_packages/module_and_script.py`:
+
+
+```
+with open('module_and_script.py', 'rt') as f:
+    for line in f:
+        print(line.rstrip())
+```
+
+
+This file contains two functions `mul` and `main`.  The
+`if __name__ == '__main__':` clause at the bottom is a standard trick in Python
+that allows you to add code to a file that is _only executed when the module is
+called as a script_. Try it in a terminal now:
+
+
+> `python modules_and_packages/module_and_script.py`
+
+
+But if we `import` this module from another file, or from an interactive
+session, the code within the `if __name__ == '__main__':` clause will not be
+executed, and we can access its functions just like any other module that we
+import.
+
+
+```
+import module_and_script as mas
+
+a = 1.5
+b = 3
+
+print('mul({}, {}): {}'.format(a, b, mas.mul(a, b)))
+print('calling main...')
+mas.main([str(a), str(b)])
+```
+
+
+<a class="anchor" id="what-is-a-package"></a>
+## What is a package?
+
+
+You now know how to split your Python code up into separate files
+(a.k.a. _modules_). When your code grows beyond a handful of files, you may
+wish for more fine-grained control over the namespaces in which your modules
+live. Python has another feature which allows you to organise your modules
+into _packages_.
+
+
+A package in Python is simply a directory which:
+
+
+* Contains a special file called `__init__.py`
+* May contain one or more module files (any other files ending in `*.py`)
+* May contain other package directories.
+
+
+For example, the [FSLeyes](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes)
+code is organised into packages and sub-packages as follows (abridged):
+
+
+> ```
+> fsleyes/
+>     __init__.py
+>     main.py
+>     frame.py
+>     views/
+>         __init__.py
+>         orthopanel.py
+>         lightboxpanel.py
+>     controls/
+>         __init__.py
+>         locationpanel.py
+>         overlaylistpanel.py
+> ```
+
+
+Within a package structure, we will typically still import modules directly,
+via their full path within the package:
+
+
+```
+import fsleyes.main as fmain
+fmain.fsleyes_main()
+```
+
+<a class="anchor" id="init-py"></a>
+### `__init__.py`
+
+
+Every Python package must have an `__init__.py` file. In many cases, this will
+actually be an empty file, and you don't need to worry about it any more, apart
+from knowing that it is needed. But you can use `__init__.py` to perform some
+package-specific initialisation, and/or to customise the package's namespace.
+
+
+As an example, take a look the `modules_and_packages/fsleyes/__init__.py` file
+in our mock FSLeyes package. We have imported the `fsleyes_main` function from
+the `fsleyes.main` module, making it available at the package level. So
+instead of importing the `fsleyes.main` module, we could instead just import
+the `fsleyes` package:
+
+
+```
+import fsleyes
+fsleyes.fsleyes_main()
+```
+
+
+<a class="anchor" id="useful-references"></a>
+## Useful references
+
+* [Modules and packages in Python](https://docs.python.org/3.5/tutorial/modules.html)
+* [Using `__init__.py`](http://mikegrouchy.com/blog/2012/05/be-pythonic-__init__py.html)
\ No newline at end of file
diff --git a/advanced_topics/modules_and_packages/fsleyes/__init__.py b/advanced_topics/modules_and_packages/fsleyes/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a1a8c37d86718ffad50b0252e40ed23b06fba17c
--- /dev/null
+++ b/advanced_topics/modules_and_packages/fsleyes/__init__.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+
+from fsleyes.main import fsleyes_main
diff --git a/advanced_topics/modules_and_packages/fsleyes/controls/__init__.py b/advanced_topics/modules_and_packages/fsleyes/controls/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/advanced_topics/modules_and_packages/fsleyes/controls/locationpanel.py b/advanced_topics/modules_and_packages/fsleyes/controls/locationpanel.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/advanced_topics/modules_and_packages/fsleyes/controls/overlaylistpanel.py b/advanced_topics/modules_and_packages/fsleyes/controls/overlaylistpanel.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/advanced_topics/modules_and_packages/fsleyes/frame.py b/advanced_topics/modules_and_packages/fsleyes/frame.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/advanced_topics/modules_and_packages/fsleyes/main.py b/advanced_topics/modules_and_packages/fsleyes/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d52a171dd15863488e0d927da6004de7a9dae6a
--- /dev/null
+++ b/advanced_topics/modules_and_packages/fsleyes/main.py
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+
+def fsleyes_main():
+    print('Woo, you\'ve started a mock version of FSLeyes!')
diff --git a/advanced_topics/modules_and_packages/fsleyes/views/__init__.py b/advanced_topics/modules_and_packages/fsleyes/views/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/advanced_topics/modules_and_packages/fsleyes/views/lightboxpanel.py b/advanced_topics/modules_and_packages/fsleyes/views/lightboxpanel.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/advanced_topics/modules_and_packages/fsleyes/views/orthopanel.py b/advanced_topics/modules_and_packages/fsleyes/views/orthopanel.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/advanced_topics/modules_and_packages/module_and_script.py b/advanced_topics/modules_and_packages/module_and_script.py
new file mode 100644
index 0000000000000000000000000000000000000000..42420b59a01dc1816bff1028b312d52b03d39cf0
--- /dev/null
+++ b/advanced_topics/modules_and_packages/module_and_script.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+
+import sys
+
+
+def mul(a, b):
+    """Multiply two numbers together. """
+    return a * b
+
+
+def main(args=None):
+    """Read in command line arguments,
+    and call the mul function.
+    """
+    if args is None:
+        args = sys.argv[1:]
+
+    if len(args) != 2:
+        print('Usage: module_and_scripy.py a b')
+        sys.exit(1)
+
+    a = float(args[0])
+    b = float(args[1])
+
+    print('{} * {}: {}'.format(a, b, mul(a, b)))
+
+
+# If this module is executed as a
+# script, call the main function
+if __name__ == '__main__':
+    main()
diff --git a/advanced_topics/modules_and_packages/numfuncs.py b/advanced_topics/modules_and_packages/numfuncs.py
new file mode 100644
index 0000000000000000000000000000000000000000..c250d8be3b5b2b328c94eab9322f2d991800ed82
--- /dev/null
+++ b/advanced_topics/modules_and_packages/numfuncs.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python
+
+# See: https://fsl.fmrib.ox.ac.uk/fslcourse/lectures/scripting/_0200.fpd/cheat3
+PI = 3.1417
+
+def add(a, b):
+    return float(a) + float(b)
diff --git a/advanced_topics/modules_and_packages/sideeffects.py b/advanced_topics/modules_and_packages/sideeffects.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ccc43430706bcf0aaf0354d50f8a87a006acac9
--- /dev/null
+++ b/advanced_topics/modules_and_packages/sideeffects.py
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+
+print('Hah, you\'ve imported the sideeffects module! '
+      'You\'ll never see this message again!')
diff --git a/advanced_topics/modules_and_packages/strfuncs.py b/advanced_topics/modules_and_packages/strfuncs.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9f7dd7f745f44b9a2c85bcfc41d81e91cad6905
--- /dev/null
+++ b/advanced_topics/modules_and_packages/strfuncs.py
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+
+def add(a, b):
+    return str(a) + str(b)
diff --git a/advanced_topics/object_oriented_programming.ipynb b/advanced_topics/object_oriented_programming.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..deca29f7e0d2aef7a45dcc7d3fa68c183ea42711
--- /dev/null
+++ b/advanced_topics/object_oriented_programming.ipynb
@@ -0,0 +1,1709 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Object-oriented programming in Python\n",
+    "\n",
+    "\n",
+    "By now you might have realised that __everything__ in Python is an\n",
+    "object. Strings are objects, numbers are objects, functions are objects,\n",
+    "modules are objects - __everything__ is an object!\n",
+    "\n",
+    "\n",
+    "But this does not mean that you have to use Python in an object-oriented\n",
+    "fashion. You can stick with functions and statements, and get quite a lot\n",
+    "done. But some problems are just easier to solve, and to reason about, when\n",
+    "you use an object-oriented approach.\n",
+    "\n",
+    "\n",
+    "* [Objects versus classes](#objects-versus-classes)\n",
+    "* [Defining a class](#defining-a-class)\n",
+    "* [Object creation - the `__init__` method](#object-creation-the-init-method)\n",
+    " * [Our method is called `__init__`, but we didn't actually call the `__init__` method!](#our-method-is-called-init)\n",
+    " * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument)\n",
+    "* [Attributes](#attributes)\n",
+    "* [Methods](#methods)\n",
+    "* [Protecting attribute access](#protecting-attribute-access)\n",
+    " * [A better way - properties](#a-better-way-properties])\n",
+    "* [Inheritance](#inheritance)\n",
+    " * [The basics](#the-basics)\n",
+    " * [Code re-use and problem decomposition](#code-re-use-and-problem-decomposition)\n",
+    " * [Polymorphism](#polymorphism)\n",
+    " * [Multiple inheritance](#multiple-inheritance)\n",
+    "* [Class attributes and methods](#class-attributes-and-methods)\n",
+    " * [Class attributes](#class-attributes)\n",
+    " * [Class methods](#class-methods)\n",
+    "* [Appendix: The `object` base-class](#appendix-the-object-base-class)\n",
+    "* [Appendix: `__init__` versus `__new__`](#appendix-init-versus-new)\n",
+    "* [Appendix: Monkey-patching](#appendix-monkey-patching)\n",
+    "* [Appendix: Method overloading](#appendix-method-overloading)\n",
+    "* [Useful references](#useful-references)\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"objects-versus-classes\"></a>\n",
+    "## Objects versus classes\n",
+    "\n",
+    "\n",
+    "If you are versed in C++, Java, C#, or some other object-oriented language,\n",
+    "then this should all hopefully sound familiar, and you can skip to the next\n",
+    "section.\n",
+    "\n",
+    "\n",
+    "If you have not done any object-oriented programming before, your first step\n",
+    "is to understand the difference between _objects_ (also known as\n",
+    "_instances_) and _classes_ (also known as _types_).\n",
+    "\n",
+    "\n",
+    "If you have some experience in C, then you can start off by thinking of a\n",
+    "class as like a `struct` definition - a `struct` is a specification for the\n",
+    "layout of a chunk of memory. For example, here is a typical struct definition:\n",
+    "\n",
+    "> ```\n",
+    "> /**\n",
+    ">  * Struct representing a stack.\n",
+    ">  */\n",
+    "> typedef struct __stack {\n",
+    ">   uint8_t capacity; /**< the maximum capacity of this stack */\n",
+    ">   uint8_t size;     /**< the current size of this stack     */\n",
+    ">   void  **top;      /**< pointer to the top of this stack   */\n",
+    "> } stack_t;\n",
+    "> ```\n",
+    "\n",
+    "\n",
+    "Now, an _object_ is not a definition, but rather a thing which resides in\n",
+    "memory. An object can have _attributes_ (pieces of information), and _methods_\n",
+    "(functions associated with the object). You can pass objects around your code,\n",
+    "manipulate their attributes, and call their methods.\n",
+    "\n",
+    "\n",
+    "Returning to our C metaphor, you can think of an object as like an\n",
+    "instantiation of a struct:\n",
+    "\n",
+    "\n",
+    "> ```\n",
+    "> stack_t stack;\n",
+    "> stack.capacity = 10;\n",
+    "> ```\n",
+    "\n",
+    "\n",
+    "One of the major differences between a `struct` in C, and a `class` in Python\n",
+    "and other object oriented languages, is that you can't (easily) add functions\n",
+    "to a `struct` - it is just a chunk of memory. Whereas in Python, you can add\n",
+    "functions to your class definition, which will then be added as methods when\n",
+    "you create an object from that class.\n",
+    "\n",
+    "\n",
+    "Of course there are many more differences between C structs and classes (most\n",
+    "notably [inheritance](todo), [polymorphism](todo), and [access\n",
+    "protection](todo)). But if you can understand the difference between a\n",
+    "_definition_ of a C struct, and an _instantiation_ of that struct, then you\n",
+    "are most of the way towards understanding the difference between a _class_,\n",
+    "and an _object_.\n",
+    "\n",
+    "\n",
+    "> But just to confuse you, remember that in Python, __everything__ is an\n",
+    "> object - even classes!\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"defining-a-class\"></a>\n",
+    "## Defining a class\n",
+    "\n",
+    "\n",
+    "Defining a class in Python is simple. Let's take on a small project, by\n",
+    "developing a class which can be used in place of the `fslmaths` shell command."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class FSLMaths(object):\n",
+    "    pass"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "In this statement, we defined a new class called `FSLMaths`, which inherits\n",
+    "from the built-in `object` base-class (see [below](inheritance) for more\n",
+    "details on inheritance).\n",
+    "\n",
+    "\n",
+    "Now that we have defined our class, we can create objects - instances of that\n",
+    "class - by calling the class itself, as if it were a function:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fm1 = FSLMaths()\n",
+    "fm2 = FSLMaths()\n",
+    "print(fm1)\n",
+    "print(fm2)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Although these objects are not of much use at this stage. Let's do some more\n",
+    "work.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"object-creation-the-init-method\"></a>\n",
+    "## Object creation - the `__init__` method\n",
+    "\n",
+    "\n",
+    "The first thing that our `fslmaths` replacement will need is an input image.\n",
+    "It makes sense to pass this in when we create an `FSLMaths` object:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class FSLMaths(object):\n",
+    "    def __init__(self, inimg):\n",
+    "        self.img = inimg"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Here we have added a _method_ called `__init__` to our class (remember that a\n",
+    "_method_ is just a function which is defined in a class, and which can be\n",
+    "called on instances of that class).  This method expects two arguments -\n",
+    "`self`, and `inimg`. So now, when we create an instance of the `FSLMaths`\n",
+    "class, we will need to provide an input image:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import nibabel as nib\n",
+    "import os.path as op\n",
+    "\n",
+    "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n",
+    "inimg = nib.load(fpath)\n",
+    "fm    = FSLMaths(inimg)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "There are a couple of things to note here...\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"our-method-is-called-init\"></a>\n",
+    "### Our method is called `__init__`, but we didn't actually call the `__init__` method!\n",
+    "\n",
+    "\n",
+    "`__init__` is a special method in Python - it is called when an instance of a\n",
+    "class is created. And recall that we can create an instance of a class by\n",
+    "calling the class in the same way that we call a function.\n",
+    "\n",
+    "\n",
+    "There are a number of \"special\" methods that you can add to a class in Python\n",
+    "to customise various aspects of how instances of the class behave.  One of the\n",
+    "first ones you may come across is the `__str__` method, which defines how an\n",
+    "object should be printed (more specifically, how an object gets converted into\n",
+    "a string). For example, we could add a `__str__` method to our `FSLMaths`\n",
+    "class like so:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class FSLMaths(object):\n",
+    "\n",
+    "    def __init__(self, inimg):\n",
+    "        self.img = inimg\n",
+    "\n",
+    "    def __str__(self):\n",
+    "        return 'FSLMaths({})'.format(self.img.get_filename())\n",
+    "\n",
+    "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n",
+    "inimg = nib.load(fpath)\n",
+    "fm    = FSLMaths(inimg)\n",
+    "\n",
+    "print(fm)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Refer to the [official\n",
+    "docs](https://docs.python.org/3.5/reference/datamodel.html#special-method-names)\n",
+    "for details on all of the special methods that can be defined in a class. And\n",
+    "take a look at the appendix for some more details on [how Python objects get\n",
+    "created](appendix-init-versus-new).\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"we-didnt-specify-the-self-argument\"></a>\n",
+    "### We didn't specify the `self` argument - what gives?!?\n",
+    "\n",
+    "\n",
+    "The `self` argument is a special argument for methods in Python. If you are\n",
+    "coming from C++, Java, C# or similar, `self` in Python is equivalent to `this`\n",
+    "in those languages.\n",
+    "\n",
+    "\n",
+    "In a method, the `self` argument is a reference to the object that the method\n",
+    "was called on. So in this line of code:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fm = FSLMaths(inimg)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "the `self` argument in `__init__` will be a reference to the `FSLMaths` object\n",
+    "that has been created (and is then assigned to the `fm` variable, after the\n",
+    "`__init__` method has finished).\n",
+    "\n",
+    "\n",
+    "But note that you __do not__ need to explicitly provide the `self` argument\n",
+    "when you call a method on an object, or when you create a new object. The\n",
+    "Python runtime will take care of passing the instance to its method, as the\n",
+    "first argument to the method.\n",
+    "\n",
+    "\n",
+    "But when you are writing a class, you __do__ need to explicitly list `self` as\n",
+    "the first argument to all of the methods of the class.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"attributes\"></a>\n",
+    "## Attributes\n",
+    "\n",
+    "\n",
+    "In Python, the term __attribute__ is used to refer to a piece of information\n",
+    "that is associated with an object. An attribute is generally a reference to\n",
+    "another object (which might be a string, a number, or a list, or some other\n",
+    "more complicated object).\n",
+    "\n",
+    "\n",
+    "Remember that we modified our `FSLMaths` class so that it is passed an input\n",
+    "image on creation:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class FSLMaths(object):\n",
+    "    def __init__(self, inimg):\n",
+    "        self.img = inimg\n",
+    "\n",
+    "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n",
+    "fm    = FSLMaths(nib.load(fpath))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Take a look at what is going on in the `__init__` method - we take the `inimg`\n",
+    "argument, and create a reference to it called `self.img`. We have added an\n",
+    "_attribute_ to the `FSLMaths` instance, called `img`, and we can access that\n",
+    "attribute like so:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print('Input for our FSLMaths instance: {}'.format(fm.img.get_filename()))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "And that concludes the section on adding attributes to Python objects.\n",
+    "\n",
+    "\n",
+    "Just kidding. But it really is that simple. This is one aspect of Python which\n",
+    "might be quite jarring to you if you are coming from a language with more\n",
+    "rigid semantics, such as C++ or Java. In those languages, you must pre-specify\n",
+    "all of the attributes and methods that are a part of a class. But Python is\n",
+    "much more flexible - you can simply add attributes to an object after it has\n",
+    "been created.  In fact, you can even do this outside of the class\n",
+    "definition<sup>1</sup>:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fm = FSLMaths(inimg)\n",
+    "fm.another_attribute = 'Haha'\n",
+    "print(fm.another_attribute)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "__But ...__ while attributes can be added to a Python object at any time, it is\n",
+    "good practice (and makes for more readable and maintainable code) to add all\n",
+    "of an object's attributes within the `__init__` method.\n",
+    "\n",
+    "\n",
+    "> <sup>1</sup>This not possible with many of the built-in types, such as\n",
+    "> `list` and `dict` objects, nor with types that are defined in Python\n",
+    "> extensions (Python modules that are written in C).\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"methods\"></a>\n",
+    "## Methods\n",
+    "\n",
+    "\n",
+    "We've been dilly-dallying on this little `FSLMaths` project for a while now,\n",
+    "but our class still can't actually do anything. Let's start adding some\n",
+    "functionality:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class FSLMaths(object):\n",
+    "\n",
+    "    def __init__(self, inimg):\n",
+    "        self.img        = inimg\n",
+    "        self.operations = []\n",
+    "\n",
+    "    def add(self, value):\n",
+    "        self.operations.append(('add', value))\n",
+    "\n",
+    "    def mul(self, value):\n",
+    "        self.operations.append(('mul', value))\n",
+    "\n",
+    "    def div(self, value):\n",
+    "        self.operations.append(('div', value))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Woah woah, [slow down egg-head!](https://www.youtube.com/watch?v=yz-TemWooa4)\n",
+    "We've modified `__init__` so that a second attribute called `operations` is\n",
+    "added to our object - this `operations` attribute is simply a list.\n",
+    "\n",
+    "\n",
+    "Then, we added a handful of methods - `add`, `mul`, and `div` - which each\n",
+    "append a tuple to that `operations` list.\n",
+    "\n",
+    "\n",
+    "> Note that, just like in the `__init__` method, the first argument that will\n",
+    "> be passed to these methods is `self` - a reference to the object that the\n",
+    "> method has been called on.\n",
+    "\n",
+    "\n",
+    "The idea behind this design is that our `FSLMaths` class will not actually do\n",
+    "anything when we call the `add`, `mul` or `div` methods. Instead, it will\n",
+    "\"stage\" each operation, and then perform them all in one go. So let's add\n",
+    "another method, `run`, which actually does the work:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy   as np\n",
+    "import nibabel as nib\n",
+    "\n",
+    "class FSLMaths(object):\n",
+    "\n",
+    "    def __init__(self, inimg):\n",
+    "        self.img        = inimg\n",
+    "        self.operations = []\n",
+    "\n",
+    "    def add(self, value):\n",
+    "        self.operations.append(('add', value))\n",
+    "\n",
+    "    def mul(self, value):\n",
+    "        self.operations.append(('mul', value))\n",
+    "\n",
+    "    def div(self, value):\n",
+    "        self.operations.append(('div', value))\n",
+    "\n",
+    "    def run(self, output=None):\n",
+    "\n",
+    "        data = np.array(self.img.get_data())\n",
+    "\n",
+    "        for oper, value in self.operations:\n",
+    "\n",
+    "            # Value could be an image.\n",
+    "            # If not, we assume that\n",
+    "            # it is a scalar/numpy array.\n",
+    "            if isinstance(value, nib.nifti1.Nifti1Image):\n",
+    "                value = value.get_data()\n",
+    "\n",
+    "\n",
+    "            if oper == 'add':\n",
+    "                data = data + value\n",
+    "            elif oper == 'mul':\n",
+    "                data = data * value\n",
+    "            elif oper == 'div':\n",
+    "                data = data / value\n",
+    "\n",
+    "        # turn final output into a nifti,\n",
+    "        # and save it to disk if an\n",
+    "        # 'output' has been specified.\n",
+    "        outimg = nib.nifti1.Nifti1Image(data, inimg.affine)\n",
+    "\n",
+    "        if output is not None:\n",
+    "            nib.save(outimg, output)\n",
+    "\n",
+    "        return outimg"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We now have a useable (but not very useful) `FSLMaths` class!"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n",
+    "fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n",
+    "inimg = nib.load(fpath)\n",
+    "mask  = nib.load(fmask)\n",
+    "fm    = FSLMaths(inimg)\n",
+    "\n",
+    "fm.mul(mask)\n",
+    "fm.add(-10)\n",
+    "\n",
+    "outimg = fm.run()\n",
+    "\n",
+    "norigvox = (inimg .get_data() > 0).sum()\n",
+    "nmaskvox = (outimg.get_data() > 0).sum()\n",
+    "\n",
+    "print('Number of voxels >0 in original image: {}'.format(norigvox))\n",
+    "print('Number of voxels >0 in masked image:   {}'.format(nmaskvox))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"protecting-attribute-access\"></a>\n",
+    "## Protecting attribute access\n",
+    "\n",
+    "\n",
+    "In our `FSLMaths` class, the input image was added as an attribute called\n",
+    "`img` to `FSLMaths` objects. We saw that it is easy to read the attributes\n",
+    "of an object - if we have a `FSLMaths` instance called `fm`, we can read its\n",
+    "input image via `fm.img`.\n",
+    "\n",
+    "\n",
+    "But it is just as easy to write the attributes of an object. What's to stop\n",
+    "some sloppy research assistant from overwriting our `img` attribute?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz'))\n",
+    "fm = FSLMaths(inimg)\n",
+    "fm.img = None\n",
+    "fm.run()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Well, the scary answer is ... there is __nothing__ stopping you from doing\n",
+    "whatever you want to a Python object. You can add, remove, and modify\n",
+    "attributes at will. You can even replace the methods of an existing object if\n",
+    "you like:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fm = FSLMaths(inimg)\n",
+    "\n",
+    "def myadd(value):\n",
+    "    print('Oh no, I\\'m not going to add {} to '\n",
+    "          'your image. Go away!'.format(value))\n",
+    "\n",
+    "fm.add = myadd\n",
+    "fm.add(123)\n",
+    "\n",
+    "fm.mul = None\n",
+    "fm.mul(123)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "But you really shouldn't get into the habit of doing devious things like\n",
+    "this. Think of the poor souls who inherit your code years after you have left\n",
+    "the lab - if you go around overwriting all of the methods and attributes of\n",
+    "your objects, they are not going to have a hope in hell of understanding what\n",
+    "your code is actually doing, and they are not going to like you very\n",
+    "much. Take a look at the appendix for a [brief discussion on this\n",
+    "topic](appendix-monkey-patching).\n",
+    "\n",
+    "\n",
+    "Python tends to assume that programmers are \"responsible adults\", and hence\n",
+    "doesn't do much in the way of restricting access to the attributes or methods\n",
+    "of an object. This is in contrast to languages like C++ and Java, where the\n",
+    "notion of a private attribute or method is strictly enforced by the language.\n",
+    "\n",
+    "\n",
+    "However, there are a couple of conventions in Python that are [universally\n",
+    "adhered\n",
+    "to](https://docs.python.org/3.5/tutorial/classes.html#private-variables):\n",
+    "\n",
+    "* Class-level attributes and methods, and module-level attributes, functions,\n",
+    "  and classes, which begin with a single underscore (`_`), should be\n",
+    "  considered __protected__ - they are intended for internal use only, and\n",
+    "  should not be considered part of the public API of a class or module.  This\n",
+    "  is not enforced by the language in any way<sup>2</sup> - remember, we are\n",
+    "  all responsible adults here!\n",
+    "\n",
+    "* Class-level attributes and methods which begin with a double-underscore\n",
+    "  (`__`) should be considered __private__. Python provides a weak form of\n",
+    "  enforcement for this rule - any attribute or method with such a name will\n",
+    "  actually be _renamed_ (in a standardised manner) at runtime, so that it is\n",
+    "  not accessible through its original name (it is still accessible via its\n",
+    "  [mangled\n",
+    "  name](https://docs.python.org/3.5/tutorial/classes.html#private-variables)\n",
+    "  though).\n",
+    "\n",
+    "\n",
+    "> <sup>2</sup> With the exception that module-level fields which begin with a\n",
+    "> single underscore will not be imported into the local scope via the\n",
+    "> `from [module] import *` techinque.\n",
+    "\n",
+    "\n",
+    "So with all of this in mind, we can adjust our `FSLMaths` class to discourage\n",
+    "our sloppy research assistant from overwriting the `img` attribute:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# remainder of definition omitted for brevity\n",
+    "class FSLMaths(object):\n",
+    "    def __init__(self, inimg):\n",
+    "        self.__img        = inimg\n",
+    "        self.__operations = []"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "But now we have lost the ability to read our `__img` attribute:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz'))\n",
+    "fm = FSLMaths(inimg)\n",
+    "print(fm.__img)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"a-better-way-properties\"></a>\n",
+    "### A better way - properties\n",
+    "\n",
+    "\n",
+    "Python has a feature called\n",
+    "[`properties`](https://docs.python.org/3.5/library/functions.html#property),\n",
+    "which is a nice way of controlling access to the attributes of an object. We\n",
+    "can use properties by defining a \"getter\" method which can be used to access\n",
+    "our attributes, and \"decorating\" them with the `@property` decorator (we will\n",
+    "cover decorators in a later practical)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class FSLMaths(object):\n",
+    "    def __init__(self, inimg):\n",
+    "        self.__img        = inimg\n",
+    "        self.__operations = []\n",
+    "\n",
+    "    @property\n",
+    "    def img(self):\n",
+    "        return self.__img"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "So we are still storing our input image as a private attribute, but now we\n",
+    "have made it available in a read-only manner via the public `img` property:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n",
+    "inimg = nib.load(fpath)\n",
+    "fm    = FSLMaths(inimg)\n",
+    "\n",
+    "print(fm.img.get_filename())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Note that, even though we have defined `img` as a method, we can access it\n",
+    "like an attribute - this is due to the magic behind the `@property` decorator.\n",
+    "\n",
+    "\n",
+    "We can also define \"setter\" methods for a property. For example, we might wish\n",
+    "to add the ability for a user of our `FSLMaths` class to change the input\n",
+    "image after creation."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class FSLMaths(object):\n",
+    "    def __init__(self, inimg):\n",
+    "        self.__img        = None\n",
+    "        self.__operations = []\n",
+    "        self.img          = inimg\n",
+    "\n",
+    "    @property\n",
+    "    def img(self):\n",
+    "        return self.__img\n",
+    "\n",
+    "    @img.setter\n",
+    "    def img(self, value):\n",
+    "        if not isinstance(value, nib.nifti1.Nifti1Image):\n",
+    "            raise ValueError('value must be a NIFTI image!')\n",
+    "        self.__img = value"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "> Note that we used the `img` setter method within `__init__` to validate the\n",
+    "> initial `inimg` that was passed in during creation.\n",
+    "\n",
+    "\n",
+    "Property setters are a nice way to add validation logic for when an attribute\n",
+    "is assigned a value. In this example, an error will be raised if the new input\n",
+    "is not a NIFTI image."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n",
+    "inimg = nib.load(fpath)\n",
+    "fm    = FSLMaths(inimg)\n",
+    "\n",
+    "print('Input:     ', fm.img.get_filename())\n",
+    "\n",
+    "# let's change the input\n",
+    "# to a different image\n",
+    "fpath2 = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain.nii.gz')\n",
+    "inimg2 = nib.load(fpath2)\n",
+    "fm.img = inimg2\n",
+    "\n",
+    "print('New input: ', fm.img.get_filename())\n",
+    "\n",
+    "print('This is going to explode')\n",
+    "fm.img = 'abcde'"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"inheritance\"></a>\n",
+    "## Inheritance\n",
+    "\n",
+    "\n",
+    "One of the major advantages of an object-oriented programming approach is\n",
+    "_inheritance_ - the ability to define hierarchical relationships between\n",
+    "classes and instances.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"the-basics\"></a>\n",
+    "### The basics\n",
+    "\n",
+    "\n",
+    "My local veterinary surgery runs some Python code which looks like the\n",
+    "following, to assist the nurses in identifying an animal when it arrives at\n",
+    "the surgery:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Animal(object):\n",
+    "    def noiseMade(self):\n",
+    "        raise NotImplementedError('This method must be '\n",
+    "                                  'implemented by sub-classes')\n",
+    "\n",
+    "class Dog(Animal):\n",
+    "    def noiseMade(self):\n",
+    "        return 'Woof'\n",
+    "\n",
+    "class TalkingDog(Dog):\n",
+    "    def noiseMade(self):\n",
+    "        return 'Hi Homer, find your soulmate!'\n",
+    "\n",
+    "class Cat(Animal):\n",
+    "    def noiseMade(self):\n",
+    "        return 'Meow'\n",
+    "\n",
+    "class Labrador(Dog):\n",
+    "    pass\n",
+    "\n",
+    "class Chihuahua(Dog):\n",
+    "    def noiseMade(self):\n",
+    "        return 'Yap yap yap'"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Hopefully this example doesn't need much in the way of explanation - this\n",
+    "collection of classes captures a hierarchical relationship which exists in the\n",
+    "real world (and also captures the inherently annoying nature of\n",
+    "chihuahuas). For example, in the real world, all dogs are animals, but not all\n",
+    "animals are dogs.  Therefore in our model, the `Dog` class has specified\n",
+    "`Animal` as its base class. We say that the `Dog` class _extends_, _derives\n",
+    "from_, or _inherits from_, the `Animal` class, and that all `Dog` instances\n",
+    "are also `Animal` instances (but not vice-versa).\n",
+    "\n",
+    "\n",
+    "What does that `noiseMade` method do?  There is a `noiseMade` method defined\n",
+    "on the `Animal` class, but it has been re-implemented, or _overridden_ in the\n",
+    "`Dog`,\n",
+    "[`TalkingDog`](https://twitter.com/simpsonsqotd/status/427941665836630016?lang=en),\n",
+    "`Cat`, and `Chihuahua` classes (but not on the `Labrador` class).  We can call\n",
+    "the `noiseMade` method on any `Animal` instance, but the specific behaviour\n",
+    "that we get is dependent on the specific type of animal."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "d  = Dog()\n",
+    "l  = Labrador()\n",
+    "c  = Cat()\n",
+    "ch = Chihuahua()\n",
+    "\n",
+    "print('Noise made by dogs:       {}'.format(d .noiseMade()))\n",
+    "print('Noise made by labradors:  {}'.format(l .noiseMade()))\n",
+    "print('Noise made by cats:       {}'.format(c .noiseMade()))\n",
+    "print('Noise made by chihuahuas: {}'.format(ch.noiseMade()))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Note that calling the `noiseMade` method on a `Labrador` instance resulted in\n",
+    "the `Dog.noiseMade` implementation being called.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"code-re-use-and-problem-decomposition\"></a>\n",
+    "### Code re-use and problem decomposition\n",
+    "\n",
+    "\n",
+    "Inheritance allows us to split a problem into smaller problems, and to re-use\n",
+    "code.  Let's demonstrate this with a more involved (and even more contrived)\n",
+    "example.  Imagine that a former colleague had written a class called\n",
+    "`Operator`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Operator(object):\n",
+    "\n",
+    "    def __init__(self):\n",
+    "        super().__init__() # this line will be explained later\n",
+    "        self.__operations = []\n",
+    "        self.__opFuncs    = {}\n",
+    "\n",
+    "    @property\n",
+    "    def operations(self):\n",
+    "        return list(self.__operations)\n",
+    "\n",
+    "    @property\n",
+    "    def functions(self):\n",
+    "        return dict(self.__opFuncs)\n",
+    "\n",
+    "    def addFunction(self, name, func):\n",
+    "        self.__opFuncs[name] = func\n",
+    "\n",
+    "    def do(self, name, *values):\n",
+    "        self.__operations.append((name, values))\n",
+    "\n",
+    "    def preprocess(self, value):\n",
+    "        return value\n",
+    "\n",
+    "    def run(self, input):\n",
+    "        data = self.preprocess(input)\n",
+    "        for oper, vals in self.__operations:\n",
+    "            func = self.__opFuncs[oper]\n",
+    "            vals = [self.preprocess(v) for v in vals]\n",
+    "            data = func(data, *vals)\n",
+    "        return data"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This `Operator` class provides an interface and logic to execute a chain of\n",
+    "operations - an operation is some function which accepts one or more inputs,\n",
+    "and produce one output.\n",
+    "\n",
+    "\n",
+    "But it stops short of defining any operations. Instead, we can create another\n",
+    "class - a sub-class - which derives from the `Operator` class. This sub-class\n",
+    "will define the operations that will ultimately be executed by the `Operator`\n",
+    "class. All that the `Operator` class does is:\n",
+    "\n",
+    "- Allow functions to be registered with the `addFunction` method - all\n",
+    "  registered functions can be used via the `do` method.\n",
+    "\n",
+    "- Stage an operation (using a registered function) via the `do` method. Note\n",
+    "  that `do` allows any number of values to be passed to it, as we used the `*`\n",
+    "  operator when specifying the `values` argument.\n",
+    "\n",
+    "- Run all staged operations via the `run` method - it passes an input through\n",
+    "  all of the operations that have been staged, and then returns the final\n",
+    "  result.\n",
+    "\n",
+    "\n",
+    "Let's define a sub-class:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class NumberOperator(Operator):\n",
+    "\n",
+    "    def __init__(self):\n",
+    "        super().__init__()\n",
+    "        self.addFunction('add',    self.add)\n",
+    "        self.addFunction('mul',    self.mul)\n",
+    "        self.addFunction('negate', self.negate)\n",
+    "\n",
+    "    def preprocess(self, value):\n",
+    "        return float(value)\n",
+    "\n",
+    "    def add(self, a, b):\n",
+    "        return a + b\n",
+    "\n",
+    "    def mul(self, a, b):\n",
+    "        return a * b\n",
+    "\n",
+    "    def negate(self, a):\n",
+    "        return -a"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The `NumberOperator` is a sub-class of `Operator`, which we can use for basic\n",
+    "numerical calculations. It provides a handful of simple numerical methods, but\n",
+    "the most interesting stuff is inside `__init__`.\n",
+    "\n",
+    "\n",
+    "> ```\n",
+    "> super().__init__()\n",
+    "> ```\n",
+    "\n",
+    "\n",
+    "This line invokes `Operator.__init__` - the initialisation method for the\n",
+    "`Operator` base-class.\n",
+    "\n",
+    "\n",
+    "In Python, we can use the [built-in `super`\n",
+    "method](https://docs.python.org/3.5/library/functions.html#super) to take care\n",
+    "of correctly calling methods that are defined in an object's base-class (or\n",
+    "classes, in the case of [multiple inheritance](multiple-inheritance)).\n",
+    "\n",
+    "\n",
+    "> The `super` function is one thing which changed between Python 2 and 3 -\n",
+    "> in Python 2, it was necessary to pass both the type and the instance\n",
+    "> to `super`. So it is common to see code that looks like this:\n",
+    ">\n",
+    "> ```\n",
+    "> def __init__(self):\n",
+    ">     super(NumberOperator, self).__init__()\n",
+    "> ```\n",
+    ">\n",
+    "> Fortunately things are a lot cleaner in Python 3.\n",
+    "\n",
+    "\n",
+    "Let's move on to the next few lines in `__init__`:\n",
+    "\n",
+    "\n",
+    "> ```\n",
+    "> self.addFunction('add',    self.add)\n",
+    "> self.addFunction('mul',    self.mul)\n",
+    "> self.addFunction('negate', self.negate)\n",
+    "> ```\n",
+    "\n",
+    "\n",
+    "Here we are registering all of the functionality that is provided by the\n",
+    "`NumberOperator` class, via the `Operator.addFunction` method.\n",
+    "\n",
+    "\n",
+    "The `NumberOperator` class has also overridden the `preprocess` method, to\n",
+    "ensure that all values handled by the `Operator` are numbers. This method gets\n",
+    "called within the `Operator.run` method - for a `NumberOperator` instance, the\n",
+    "`NumberOperator.preprocess` method will get called<sup>1</sup>.\n",
+    "\n",
+    "\n",
+    "> <sup>1</sup> When a sub-class overrides a base-class method, it is still\n",
+    "> possible to access the base-class implementation [via the `super()`\n",
+    "> function](https://stackoverflow.com/a/4747427) (the preferred method), or by\n",
+    "> [explicitly calling the base-class\n",
+    "> implementation](https://stackoverflow.com/a/2421325).\n",
+    "\n",
+    "\n",
+    "Now let's see what our `NumberOperator` class does:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "no = NumberOperator()\n",
+    "no.do('add', 10)\n",
+    "no.do('mul', 2)\n",
+    "no.do('negate')\n",
+    "\n",
+    "print('Operations on {}: {}'.format(10,  no.run(10)))\n",
+    "print('Operations on {}: {}'.format(2.5, no.run(5)))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "It works! While this is a contrived example, hopefully you can see how\n",
+    "inheritance can be used to break a problem down into sub-problems:\n",
+    "\n",
+    "- The `Operator` class provides all of the logic needed to manage and execute\n",
+    "  operations, without caring about what those operations are actually doing.\n",
+    "\n",
+    "- This leaves the `NumberOperator` class free to concentrate on implementing\n",
+    "  the functions that are specific to its task, and not having to worry about\n",
+    "  how they are executed.\n",
+    "\n",
+    "\n",
+    "We could also easily implement other `Operator` sub-classes to work on\n",
+    "different data types, such as arrays, images, or even non-numeric data such as\n",
+    "strings:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class StringOperator(Operator):\n",
+    "    def __init__(self):\n",
+    "        super().__init__()\n",
+    "        self.addFunction('capitalise', self.capitalise)\n",
+    "        self.addFunction('concat',     self.concat)\n",
+    "\n",
+    "    def preprocess(self, value):\n",
+    "        return str(value)\n",
+    "\n",
+    "    def capitalise(self, s):\n",
+    "        return ' '.join([w[0].upper() + w[1:] for w in s.split()])\n",
+    "\n",
+    "    def concat(self, s1, s2):\n",
+    "        return s1 + s2\n",
+    "\n",
+    "so = StringOperator()\n",
+    "so.do('capitalise')\n",
+    "so.do('concat', '!')\n",
+    "\n",
+    "print(so.run('python is an ok language'))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"polymorphism\"></a>\n",
+    "### Polymorphism\n",
+    "\n",
+    "\n",
+    "Inheritance also allows us to take advantage of _polymorphism_, which refers\n",
+    "to idea that, in an object-oriented language, we should be able to use an\n",
+    "object without having complete knowledge about the class, or type, of that\n",
+    "object. For example, we should be able to write a function which expects an\n",
+    "`Operator` instance, but which will work on an instance of any `Operator`\n",
+    "sub-classs. As an example, let's write a function which prints a summary of an\n",
+    "`Operator` instance:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def operatorSummary(o):\n",
+    "    print(type(o).__name__)\n",
+    "    print('  All functions: ')\n",
+    "    for fname in o.functions.keys():\n",
+    "        print('    {}'.format(fname))\n",
+    "    print('  Staged operations: ')\n",
+    "    for i, (fname, vals) in enumerate(o.operations):\n",
+    "        vals = ', '.join([str(v) for v in vals])\n",
+    "        print('    {}: {}({})'.format(i + 1, fname, vals))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Because the `operatorSummary` function only uses methods that are defined\n",
+    "in the `Operator` base-class, we can use it on _any_ `Operator` instance,\n",
+    "regardless of its specific type:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "operatorSummary(no)\n",
+    "operatorSummary(so)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"multiple-inheritance\"></a>\n",
+    "### Multiple inheritance\n",
+    "\n",
+    "\n",
+    "Python allows you to define a class which has multiple base classes - this is\n",
+    "known as _multiple inheritance_. For example, we might want to build a\n",
+    "notification mechanisim into our `StringOperator` class, so that listeners can\n",
+    "be notified whenever the `capitalise` method gets called. It so happens that\n",
+    "our old colleague of `Operator` class fame also wrote a `Notifier` class which\n",
+    "allows listeners to register to be notified when an event occurs:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Notifier(object):\n",
+    "\n",
+    "    def __init__(self):\n",
+    "        super().__init__()\n",
+    "        self.__listeners = {}\n",
+    "\n",
+    "    def register(self, name, func):\n",
+    "        self.__listeners[name] = func\n",
+    "\n",
+    "    def notify(self, *args, **kwargs):\n",
+    "        for func in self.__listeners.values():\n",
+    "            func(*args, **kwargs)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Let's modify the `StringOperator` class to use the functionality of the\n",
+    "`Notifier ` class:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class StringOperator(Operator, Notifier):\n",
+    "\n",
+    "    def __init__(self):\n",
+    "        super().__init__()\n",
+    "        self.addFunction('capitalise', self.capitalise)\n",
+    "        self.addFunction('concat',     self.concat)\n",
+    "\n",
+    "    def preprocess(self, value):\n",
+    "        return str(value)\n",
+    "\n",
+    "    def capitalise(self, s):\n",
+    "        result = ' '.join([w[0].upper() + w[1:] for w in s.split()])\n",
+    "        self.notify(result)\n",
+    "        return result\n",
+    "\n",
+    "    def concat(self, s1, s2):\n",
+    "        return s1 + s2"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now, anything which is interested in uses of the `capitalise` method can\n",
+    "register as a listener on a `StringOperator` instance:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "so = StringOperator()\n",
+    "\n",
+    "def capitaliseCalled(result):\n",
+    "    print('Capitalise operation called: {}'.format(result))\n",
+    "\n",
+    "so.register('mylistener', capitaliseCalled)\n",
+    "\n",
+    "so = StringOperator()\n",
+    "so.do('capitalise')\n",
+    "so.do('concat', '?')\n",
+    "\n",
+    "print(so.run('did you notice that functions are objects too'))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "If you wish to use multiple inheritance in your design, it is important to be\n",
+    "aware of the mechanism that Python uses to determine how base class methods\n",
+    "are called (and which base class method will be called, in the case of naming\n",
+    "conflicts). This is referred to as the Method Resolution Order (MRO) - further\n",
+    "details on the topic can be found\n",
+    "[here](https://www.python.org/download/releases/2.3/mro/), and a more concise\n",
+    "summary\n",
+    "[here](http://python-history.blogspot.co.uk/2010/06/method-resolution-order.html).\n",
+    "\n",
+    "\n",
+    "Note also that for base class `__init__` methods to be correctly called in a\n",
+    "design which uses multiple inheritance, _all_ classes in the hierarchy must\n",
+    "invoke `super().__init__()`. This can become complicated when some base\n",
+    "classes expect to be passed arguments to their `__init__` method. In scenarios\n",
+    "like this it may be prefereable to manually invoke the base class `__init__`\n",
+    "methods instead of using `super()`. For example:\n",
+    "\n",
+    "\n",
+    "> ```\n",
+    "> class StringOperator(Operator, Notifier):\n",
+    ">     def __init__(self):\n",
+    ">         Operator.__init__(self)\n",
+    ">         Notifier.__init__(self)\n",
+    "> ```\n",
+    "\n",
+    "\n",
+    "This approach has the disadvantage that if the base classes change, you will\n",
+    "have to change these invocations. But the advantage is that you know exactly\n",
+    "how the class hierarchy will be initialised. In general though, doing\n",
+    "everything with `super()` will result in more maintainable code.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"class-attributes-and-methods\"></a>\n",
+    "## Class attributes and methods\n",
+    "\n",
+    "\n",
+    "Up to this point we have been covering how to add attributes and methods to an\n",
+    "_object_. But it is also possible to add methods and attributes to a _class_\n",
+    "(`static` methods and fields, for those of you familiar with C++ or Java).\n",
+    "\n",
+    "\n",
+    "Class attributes and methods can be accessed without having to create an\n",
+    "instance of the class - they are not associated with individual objects, but\n",
+    "rather with the class itself.\n",
+    "\n",
+    "\n",
+    "Class methods and attributes can be useful in several scenarios - as a\n",
+    "hypothetical, not very useful example, let's say that we want to gain usage\n",
+    "statistics for how many times each type of operation is used on instances of\n",
+    "our `FSLMaths` class. We might, for example, use this information in a grant\n",
+    "application to show evidence that more research is needed to optimise the\n",
+    "performance of the `add` operation.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"class-attributes\"></a>\n",
+    "### Class attributes\n",
+    "\n",
+    "\n",
+    "Let's add a `dict` called `opCounters` as a class attribute to the `FSLMaths`\n",
+    "class - whenever an operation is called on a `FSLMaths` instance, the counter\n",
+    "for that operation will be incremented:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy   as np\n",
+    "import nibabel as nib\n",
+    "\n",
+    "class FSLMaths(object):\n",
+    "\n",
+    "    # It's this easy to add a class-level\n",
+    "    # attribute. This dict is associated\n",
+    "    # with the FSLMaths *class*, not with\n",
+    "    # any individual FSLMaths instance.\n",
+    "    opCounters = {}\n",
+    "\n",
+    "    def __init__(self, inimg):\n",
+    "        self.img        = inimg\n",
+    "        self.operations = []\n",
+    "\n",
+    "    def add(self, value):\n",
+    "        self.operations.append(('add', value))\n",
+    "\n",
+    "    def mul(self, value):\n",
+    "        self.operations.append(('mul', value))\n",
+    "\n",
+    "    def div(self, value):\n",
+    "        self.operations.append(('div', value))\n",
+    "\n",
+    "    def run(self, output=None):\n",
+    "\n",
+    "        data = np.array(self.img.get_data())\n",
+    "\n",
+    "        for oper, value in self.operations:\n",
+    "\n",
+    "            # Code omitted for brevity\n",
+    "\n",
+    "            # Increment the usage counter\n",
+    "            # for this operation. We can\n",
+    "            # access class attributes (and\n",
+    "            # methods) through the class\n",
+    "            # itself.\n",
+    "            FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "So let's see it in action:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n",
+    "fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n",
+    "inimg = nib.load(fpath)\n",
+    "mask  = nib.load(fmask)\n",
+    "\n",
+    "fm1 = FSLMaths(inimg)\n",
+    "fm2 = FSLMaths(inimg)\n",
+    "\n",
+    "fm1.mul(mask)\n",
+    "fm1.add(15)\n",
+    "\n",
+    "fm2.add(25)\n",
+    "fm1.div(1.5)\n",
+    "\n",
+    "fm1.run()\n",
+    "fm2.run()\n",
+    "\n",
+    "print('FSLMaths usage statistics')\n",
+    "for oper in ('add', 'div', 'mul'):\n",
+    "    print('  {} : {}'.format(oper, FSLMaths.opCounters.get(oper, 0)))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"class-methods\"></a>\n",
+    "### Class methods\n",
+    "\n",
+    "\n",
+    "It is just as easy to add a method to a class - let's take our reporting code\n",
+    "from above, and add it as a method to the `FSLMaths` class.\n",
+    "\n",
+    "\n",
+    "A class method is denoted by the\n",
+    "[`@classmethod`](https://docs.python.org/3.5/library/functions.html#classmethod)\n",
+    "decorator. Note that, where a regular method which is called on an instance\n",
+    "will be passed the instance as its first argument (`self`), a class method\n",
+    "will be passed the class itself as the first argument - the standard\n",
+    "convention is to call this argument `cls`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class FSLMaths(object):\n",
+    "\n",
+    "    opCounters = {}\n",
+    "\n",
+    "    @classmethod\n",
+    "    def usage(cls):\n",
+    "        print('FSLMaths usage statistics')\n",
+    "        for oper in ('add', 'div', 'mul'):\n",
+    "            print('  {} : {}'.format(oper, FSLMaths.opCounters.get(oper, 0)))\n",
+    "\n",
+    "    def __init__(self, inimg):\n",
+    "        self.img        = inimg\n",
+    "        self.operations = []\n",
+    "\n",
+    "    def add(self, value):\n",
+    "        self.operations.append(('add', value))\n",
+    "\n",
+    "    def mul(self, value):\n",
+    "        self.operations.append(('mul', value))\n",
+    "\n",
+    "    def div(self, value):\n",
+    "        self.operations.append(('div', value))\n",
+    "\n",
+    "    def run(self, output=None):\n",
+    "\n",
+    "        data = np.array(self.img.get_data())\n",
+    "\n",
+    "        for oper, value in self.operations:\n",
+    "            FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "> There is another decorator -\n",
+    "> [`@staticmethod`](https://docs.python.org/3.5/library/functions.html#staticmethod) -\n",
+    "> which can be used on methods defined within a class. The difference\n",
+    "> between a `@classmethod` and a `@staticmethod` is that the latter will _not_\n",
+    "> be passed the class (`cls`).\n",
+    "\n",
+    "\n",
+    "calling a class method is the same as accessing a class attribute:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')\n",
+    "fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')\n",
+    "inimg = nib.load(fpath)\n",
+    "mask  = nib.load(fmask)\n",
+    "\n",
+    "fm1 = FSLMaths(inimg)\n",
+    "fm2 = FSLMaths(inimg)\n",
+    "\n",
+    "fm1.mul(mask)\n",
+    "fm1.add(15)\n",
+    "\n",
+    "fm2.add(25)\n",
+    "fm1.div(1.5)\n",
+    "\n",
+    "fm1.run()\n",
+    "fm2.run()\n",
+    "\n",
+    "FSLMaths.usage()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Note that it is also possible to access class attributes and methods through\n",
+    "instances:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(fm1.opCounters)\n",
+    "fm1.usage()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"appendix-the-object-base-class\"></a>\n",
+    "## Appendix: The `object` base-class\n",
+    "\n",
+    "\n",
+    "When you are defining a class, you need to specify the base-class from which\n",
+    "your class inherits. If your class does not inherit from a particular class,\n",
+    "then it should inherit from the built-in `object` class:\n",
+    "\n",
+    "\n",
+    "> ```\n",
+    "> class MyClass(object):\n",
+    ">     ...\n",
+    "> ```\n",
+    "\n",
+    "\n",
+    "However, in older code bases, you might see class definitions that look like\n",
+    "this, without explicitly inheriting from the `object` base class:\n",
+    "\n",
+    "\n",
+    "> ```\n",
+    "> class MyClass:\n",
+    ">     ...\n",
+    "> ```\n",
+    "\n",
+    "\n",
+    "This syntax is a [throwback to older versions of\n",
+    "Python](https://docs.python.org/2/reference/datamodel.html#new-style-and-classic-classes).\n",
+    "In Python 3 there is actually no difference in defining classes in the\n",
+    "\"new-style\" way we have used throughout this tutorial, or the \"old-style\" way\n",
+    "mentioned in this appendix.\n",
+    "\n",
+    "\n",
+    "But if you are writing code which needs to run on both Python 2 and 3, you\n",
+    "__must__ define your classes in the new-style convention, i.e. by explicitly\n",
+    "inheriting from the `object` base class. Therefore, the safest approach is to\n",
+    "always use the new-style format.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"appendix-init-versus-new\"></a>\n",
+    "## Appendix: `__init__` versus `__new__`\n",
+    "\n",
+    "\n",
+    "In Python, object creation is actually a two-stage process - _creation_, and\n",
+    "then _initialisation_. The `__init__` method gets called during the\n",
+    "_initialisation_ stage - its job is to initialise the state of the object. But\n",
+    "note that, by the time `__init__` gets called, the object has already been\n",
+    "created.\n",
+    "\n",
+    "\n",
+    "You can also define a method called `__new__` if you need to control the\n",
+    "creation stage, although this is very rarely needed. A brief explanation on\n",
+    "the difference between `__new__` and `__init__` can be found\n",
+    "[here](https://www.reddit.com/r/learnpython/comments/2s3pms/what_is_the_difference_between_init_and_new/cnm186z/),\n",
+    "and you may also wish to take a look at the [official Python\n",
+    "docs](https://docs.python.org/3.5/reference/datamodel.html#basic-customization).\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"appendix-monkey-patching\"></a>\n",
+    "## Appendix: Monkey-patching\n",
+    "\n",
+    "\n",
+    "The act of run-time modification of objects or class definitions is referred\n",
+    "to as [_monkey-patching_](https://en.wikipedia.org/wiki/Monkey_patch) and,\n",
+    "whilst it is allowed by the Python programming language, it is generally\n",
+    "considered quite bad practice.\n",
+    "\n",
+    "\n",
+    "Just because you _can_ do something doesn't mean that you _should_. Python\n",
+    "gives you the flexibility to write your software in whatever manner you deem\n",
+    "suitable.  __But__ if you want to write software that will be used, adopted,\n",
+    "maintained, and enjoyed by other people, you should be polite, write your code\n",
+    "in a clear, readable fashion, and avoid the use of devious tactics such as\n",
+    "monkey-patching.\n",
+    "\n",
+    "\n",
+    "__However__, while monkey-patching may seem like a horrific programming\n",
+    "practice to those of you coming from the realms of C++, Java, and the like,\n",
+    "(and it is horrific in many cases), it can be _extremely_ useful in certain\n",
+    "circumstances.  For instance, monkey-patching makes [unit testing a\n",
+    "breeze in Python](https://docs.python.org/3.5/library/unittest.mock.html).\n",
+    "\n",
+    "\n",
+    "As another example, consider the scenario where you are dependent on a third\n",
+    "party library which has bugs in it. No problem - while you are waiting for the\n",
+    "library author to release a new version of the library, you can write your own\n",
+    "working implementation and [monkey-patch it\n",
+    "in!](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/blob/0.21.0/fsleyes/views/viewpanel.py#L726)\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"appendix-method-overloading\"></a>\n",
+    "## Appendix: Method overloading\n",
+    "\n",
+    "\n",
+    "Method overloading (defining multiple methods with the same name in a class,\n",
+    "but each accepting different arguments) is one of the only object-oriented\n",
+    "features that is not present in Python. Becuase Python does not perform any\n",
+    "runtime checks on the types of arguments that are passed to a method, or the\n",
+    "compatibility of the method to accept the arguments, it would not be possible\n",
+    "to determine which implementation of a method is to be called. In other words,\n",
+    "in Python only the name of a method is used to identify that method, unlike in\n",
+    "C++ and Java, where the full method signature (name, input types and return\n",
+    "types) is used.\n",
+    "\n",
+    "\n",
+    "However, because a Python method can be written to accept any number or type\n",
+    "of arguments, it is very easy to to build your own overloading logic by\n",
+    "writing a \"dispatch\" method. Here is YACE (Yet Another Contrived Example):"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class Adder(object):\n",
+    "\n",
+    "    def add(self, *args):\n",
+    "        if   len(args) == 2: return self.__add2(*args)\n",
+    "        elif len(args) == 3: return self.__add3(*args)\n",
+    "        elif len(args) == 4: return self.__add4(*args)\n",
+    "        else:\n",
+    "            raise AttributeError('No method available to accept {} '\n",
+    "                                 'arguments'.format(len(args)))\n",
+    "\n",
+    "    def __add2(self, a, b):\n",
+    "        return a + b\n",
+    "\n",
+    "    def __add3(self, a, b, c):\n",
+    "        return a + b + c\n",
+    "\n",
+    "    def __add4(self, a, b, c, d):\n",
+    "        return a + b + c + d\n",
+    "\n",
+    "a = Adder()\n",
+    "\n",
+    "print('Add two:   {}'.format(a.add(1, 2)))\n",
+    "print('Add three: {}'.format(a.add(1, 2, 3)))\n",
+    "print('Add four:  {}'.format(a.add(1, 2, 3, 4)))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"useful-references\"></a>\n",
+    "## Useful references\n",
+    "\n",
+    "\n",
+    "The official Python documentation has a wealth of information on the internal\n",
+    "workings of classes and objects, so these pages are worth a read:\n",
+    "\n",
+    "\n",
+    "* https://docs.python.org/3.5/tutorial/classes.html\n",
+    "* https://docs.python.org/3.5/reference/datamodel.html"
+   ]
+  }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/advanced_topics/object_oriented_programming.md b/advanced_topics/object_oriented_programming.md
new file mode 100644
index 0000000000000000000000000000000000000000..59aa3e99bad9496e81cd44695011871969352f0a
--- /dev/null
+++ b/advanced_topics/object_oriented_programming.md
@@ -0,0 +1,1397 @@
+# Object-oriented programming in Python
+
+
+By now you might have realised that __everything__ in Python is an
+object. Strings are objects, numbers are objects, functions are objects,
+modules are objects - __everything__ is an object!
+
+
+But this does not mean that you have to use Python in an object-oriented
+fashion. You can stick with functions and statements, and get quite a lot
+done. But some problems are just easier to solve, and to reason about, when
+you use an object-oriented approach.
+
+
+* [Objects versus classes](#objects-versus-classes)
+* [Defining a class](#defining-a-class)
+* [Object creation - the `__init__` method](#object-creation-the-init-method)
+ * [Our method is called `__init__`, but we didn't actually call the `__init__` method!](#our-method-is-called-init)
+ * [We didn't specify the `self` argument - what gives?!?](#we-didnt-specify-the-self-argument)
+* [Attributes](#attributes)
+* [Methods](#methods)
+* [Protecting attribute access](#protecting-attribute-access)
+ * [A better way - properties](#a-better-way-properties])
+* [Inheritance](#inheritance)
+ * [The basics](#the-basics)
+ * [Code re-use and problem decomposition](#code-re-use-and-problem-decomposition)
+ * [Polymorphism](#polymorphism)
+ * [Multiple inheritance](#multiple-inheritance)
+* [Class attributes and methods](#class-attributes-and-methods)
+ * [Class attributes](#class-attributes)
+ * [Class methods](#class-methods)
+* [Appendix: The `object` base-class](#appendix-the-object-base-class)
+* [Appendix: `__init__` versus `__new__`](#appendix-init-versus-new)
+* [Appendix: Monkey-patching](#appendix-monkey-patching)
+* [Appendix: Method overloading](#appendix-method-overloading)
+* [Useful references](#useful-references)
+
+
+<a class="anchor" id="objects-versus-classes"></a>
+## Objects versus classes
+
+
+If you are versed in C++, Java, C#, or some other object-oriented language,
+then this should all hopefully sound familiar, and you can skip to the next
+section.
+
+
+If you have not done any object-oriented programming before, your first step
+is to understand the difference between _objects_ (also known as
+_instances_) and _classes_ (also known as _types_).
+
+
+If you have some experience in C, then you can start off by thinking of a
+class as like a `struct` definition - a `struct` is a specification for the
+layout of a chunk of memory. For example, here is a typical struct definition:
+
+> ```
+> /**
+>  * Struct representing a stack.
+>  */
+> typedef struct __stack {
+>   uint8_t capacity; /**< the maximum capacity of this stack */
+>   uint8_t size;     /**< the current size of this stack     */
+>   void  **top;      /**< pointer to the top of this stack   */
+> } stack_t;
+> ```
+
+
+Now, an _object_ is not a definition, but rather a thing which resides in
+memory. An object can have _attributes_ (pieces of information), and _methods_
+(functions associated with the object). You can pass objects around your code,
+manipulate their attributes, and call their methods.
+
+
+Returning to our C metaphor, you can think of an object as like an
+instantiation of a struct:
+
+
+> ```
+> stack_t stack;
+> stack.capacity = 10;
+> ```
+
+
+One of the major differences between a `struct` in C, and a `class` in Python
+and other object oriented languages, is that you can't (easily) add functions
+to a `struct` - it is just a chunk of memory. Whereas in Python, you can add
+functions to your class definition, which will then be added as methods when
+you create an object from that class.
+
+
+Of course there are many more differences between C structs and classes (most
+notably [inheritance](todo), [polymorphism](todo), and [access
+protection](todo)). But if you can understand the difference between a
+_definition_ of a C struct, and an _instantiation_ of that struct, then you
+are most of the way towards understanding the difference between a _class_,
+and an _object_.
+
+
+> But just to confuse you, remember that in Python, __everything__ is an
+> object - even classes!
+
+
+<a class="anchor" id="defining-a-class"></a>
+## Defining a class
+
+
+Defining a class in Python is simple. Let's take on a small project, by
+developing a class which can be used in place of the `fslmaths` shell command.
+
+
+```
+class FSLMaths(object):
+    pass
+```
+
+
+In this statement, we defined a new class called `FSLMaths`, which inherits
+from the built-in `object` base-class (see [below](inheritance) for more
+details on inheritance).
+
+
+Now that we have defined our class, we can create objects - instances of that
+class - by calling the class itself, as if it were a function:
+
+
+```
+fm1 = FSLMaths()
+fm2 = FSLMaths()
+print(fm1)
+print(fm2)
+```
+
+
+Although these objects are not of much use at this stage. Let's do some more
+work.
+
+
+<a class="anchor" id="object-creation-the-init-method"></a>
+## Object creation - the `__init__` method
+
+
+The first thing that our `fslmaths` replacement will need is an input image.
+It makes sense to pass this in when we create an `FSLMaths` object:
+
+
+```
+class FSLMaths(object):
+    def __init__(self, inimg):
+        self.img = inimg
+```
+
+
+Here we have added a _method_ called `__init__` to our class (remember that a
+_method_ is just a function which is defined in a class, and which can be
+called on instances of that class).  This method expects two arguments -
+`self`, and `inimg`. So now, when we create an instance of the `FSLMaths`
+class, we will need to provide an input image:
+
+
+```
+import nibabel as nib
+import os.path as op
+
+fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')
+inimg = nib.load(fpath)
+fm    = FSLMaths(inimg)
+```
+
+
+There are a couple of things to note here...
+
+
+<a class="anchor" id="our-method-is-called-init"></a>
+### Our method is called `__init__`, but we didn't actually call the `__init__` method!
+
+
+`__init__` is a special method in Python - it is called when an instance of a
+class is created. And recall that we can create an instance of a class by
+calling the class in the same way that we call a function.
+
+
+There are a number of "special" methods that you can add to a class in Python
+to customise various aspects of how instances of the class behave.  One of the
+first ones you may come across is the `__str__` method, which defines how an
+object should be printed (more specifically, how an object gets converted into
+a string). For example, we could add a `__str__` method to our `FSLMaths`
+class like so:
+
+
+```
+class FSLMaths(object):
+
+    def __init__(self, inimg):
+        self.img = inimg
+
+    def __str__(self):
+        return 'FSLMaths({})'.format(self.img.get_filename())
+
+fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')
+inimg = nib.load(fpath)
+fm    = FSLMaths(inimg)
+
+print(fm)
+```
+
+
+Refer to the [official
+docs](https://docs.python.org/3.5/reference/datamodel.html#special-method-names)
+for details on all of the special methods that can be defined in a class. And
+take a look at the appendix for some more details on [how Python objects get
+created](appendix-init-versus-new).
+
+
+<a class="anchor" id="we-didnt-specify-the-self-argument"></a>
+### We didn't specify the `self` argument - what gives?!?
+
+
+The `self` argument is a special argument for methods in Python. If you are
+coming from C++, Java, C# or similar, `self` in Python is equivalent to `this`
+in those languages.
+
+
+In a method, the `self` argument is a reference to the object that the method
+was called on. So in this line of code:
+
+
+```
+fm = FSLMaths(inimg)
+```
+
+
+the `self` argument in `__init__` will be a reference to the `FSLMaths` object
+that has been created (and is then assigned to the `fm` variable, after the
+`__init__` method has finished).
+
+
+But note that you __do not__ need to explicitly provide the `self` argument
+when you call a method on an object, or when you create a new object. The
+Python runtime will take care of passing the instance to its method, as the
+first argument to the method.
+
+
+But when you are writing a class, you __do__ need to explicitly list `self` as
+the first argument to all of the methods of the class.
+
+
+<a class="anchor" id="attributes"></a>
+## Attributes
+
+
+In Python, the term __attribute__ is used to refer to a piece of information
+that is associated with an object. An attribute is generally a reference to
+another object (which might be a string, a number, or a list, or some other
+more complicated object).
+
+
+Remember that we modified our `FSLMaths` class so that it is passed an input
+image on creation:
+
+
+```
+class FSLMaths(object):
+    def __init__(self, inimg):
+        self.img = inimg
+
+fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')
+fm    = FSLMaths(nib.load(fpath))
+```
+
+
+Take a look at what is going on in the `__init__` method - we take the `inimg`
+argument, and create a reference to it called `self.img`. We have added an
+_attribute_ to the `FSLMaths` instance, called `img`, and we can access that
+attribute like so:
+
+
+```
+print('Input for our FSLMaths instance: {}'.format(fm.img.get_filename()))
+```
+
+
+And that concludes the section on adding attributes to Python objects.
+
+
+Just kidding. But it really is that simple. This is one aspect of Python which
+might be quite jarring to you if you are coming from a language with more
+rigid semantics, such as C++ or Java. In those languages, you must pre-specify
+all of the attributes and methods that are a part of a class. But Python is
+much more flexible - you can simply add attributes to an object after it has
+been created.  In fact, you can even do this outside of the class
+definition<sup>1</sup>:
+
+
+```
+fm = FSLMaths(inimg)
+fm.another_attribute = 'Haha'
+print(fm.another_attribute)
+```
+
+
+__But ...__ while attributes can be added to a Python object at any time, it is
+good practice (and makes for more readable and maintainable code) to add all
+of an object's attributes within the `__init__` method.
+
+
+> <sup>1</sup>This not possible with many of the built-in types, such as
+> `list` and `dict` objects, nor with types that are defined in Python
+> extensions (Python modules that are written in C).
+
+
+<a class="anchor" id="methods"></a>
+## Methods
+
+
+We've been dilly-dallying on this little `FSLMaths` project for a while now,
+but our class still can't actually do anything. Let's start adding some
+functionality:
+
+
+```
+class FSLMaths(object):
+
+    def __init__(self, inimg):
+        self.img        = inimg
+        self.operations = []
+
+    def add(self, value):
+        self.operations.append(('add', value))
+
+    def mul(self, value):
+        self.operations.append(('mul', value))
+
+    def div(self, value):
+        self.operations.append(('div', value))
+```
+
+
+Woah woah, [slow down egg-head!](https://www.youtube.com/watch?v=yz-TemWooa4)
+We've modified `__init__` so that a second attribute called `operations` is
+added to our object - this `operations` attribute is simply a list.
+
+
+Then, we added a handful of methods - `add`, `mul`, and `div` - which each
+append a tuple to that `operations` list.
+
+
+> Note that, just like in the `__init__` method, the first argument that will
+> be passed to these methods is `self` - a reference to the object that the
+> method has been called on.
+
+
+The idea behind this design is that our `FSLMaths` class will not actually do
+anything when we call the `add`, `mul` or `div` methods. Instead, it will
+"stage" each operation, and then perform them all in one go. So let's add
+another method, `run`, which actually does the work:
+
+
+```
+import numpy   as np
+import nibabel as nib
+
+class FSLMaths(object):
+
+    def __init__(self, inimg):
+        self.img        = inimg
+        self.operations = []
+
+    def add(self, value):
+        self.operations.append(('add', value))
+
+    def mul(self, value):
+        self.operations.append(('mul', value))
+
+    def div(self, value):
+        self.operations.append(('div', value))
+
+    def run(self, output=None):
+
+        data = np.array(self.img.get_data())
+
+        for oper, value in self.operations:
+
+            # Value could be an image.
+            # If not, we assume that
+            # it is a scalar/numpy array.
+            if isinstance(value, nib.nifti1.Nifti1Image):
+                value = value.get_data()
+
+
+            if oper == 'add':
+                data = data + value
+            elif oper == 'mul':
+                data = data * value
+            elif oper == 'div':
+                data = data / value
+
+        # turn final output into a nifti,
+        # and save it to disk if an
+        # 'output' has been specified.
+        outimg = nib.nifti1.Nifti1Image(data, inimg.affine)
+
+        if output is not None:
+            nib.save(outimg, output)
+
+        return outimg
+```
+
+
+We now have a useable (but not very useful) `FSLMaths` class!
+
+
+```
+fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')
+fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')
+inimg = nib.load(fpath)
+mask  = nib.load(fmask)
+fm    = FSLMaths(inimg)
+
+fm.mul(mask)
+fm.add(-10)
+
+outimg = fm.run()
+
+norigvox = (inimg .get_data() > 0).sum()
+nmaskvox = (outimg.get_data() > 0).sum()
+
+print('Number of voxels >0 in original image: {}'.format(norigvox))
+print('Number of voxels >0 in masked image:   {}'.format(nmaskvox))
+```
+
+
+<a class="anchor" id="protecting-attribute-access"></a>
+## Protecting attribute access
+
+
+In our `FSLMaths` class, the input image was added as an attribute called
+`img` to `FSLMaths` objects. We saw that it is easy to read the attributes
+of an object - if we have a `FSLMaths` instance called `fm`, we can read its
+input image via `fm.img`.
+
+
+But it is just as easy to write the attributes of an object. What's to stop
+some sloppy research assistant from overwriting our `img` attribute?
+
+
+```
+inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz'))
+fm = FSLMaths(inimg)
+fm.img = None
+fm.run()
+```
+
+
+Well, the scary answer is ... there is __nothing__ stopping you from doing
+whatever you want to a Python object. You can add, remove, and modify
+attributes at will. You can even replace the methods of an existing object if
+you like:
+
+
+```
+fm = FSLMaths(inimg)
+
+def myadd(value):
+    print('Oh no, I\'m not going to add {} to '
+          'your image. Go away!'.format(value))
+
+fm.add = myadd
+fm.add(123)
+
+fm.mul = None
+fm.mul(123)
+```
+
+
+But you really shouldn't get into the habit of doing devious things like
+this. Think of the poor souls who inherit your code years after you have left
+the lab - if you go around overwriting all of the methods and attributes of
+your objects, they are not going to have a hope in hell of understanding what
+your code is actually doing, and they are not going to like you very
+much. Take a look at the appendix for a [brief discussion on this
+topic](appendix-monkey-patching).
+
+
+Python tends to assume that programmers are "responsible adults", and hence
+doesn't do much in the way of restricting access to the attributes or methods
+of an object. This is in contrast to languages like C++ and Java, where the
+notion of a private attribute or method is strictly enforced by the language.
+
+
+However, there are a couple of conventions in Python that are [universally
+adhered
+to](https://docs.python.org/3.5/tutorial/classes.html#private-variables):
+
+* Class-level attributes and methods, and module-level attributes, functions,
+  and classes, which begin with a single underscore (`_`), should be
+  considered __protected__ - they are intended for internal use only, and
+  should not be considered part of the public API of a class or module.  This
+  is not enforced by the language in any way<sup>2</sup> - remember, we are
+  all responsible adults here!
+
+* Class-level attributes and methods which begin with a double-underscore
+  (`__`) should be considered __private__. Python provides a weak form of
+  enforcement for this rule - any attribute or method with such a name will
+  actually be _renamed_ (in a standardised manner) at runtime, so that it is
+  not accessible through its original name (it is still accessible via its
+  [mangled
+  name](https://docs.python.org/3.5/tutorial/classes.html#private-variables)
+  though).
+
+
+> <sup>2</sup> With the exception that module-level fields which begin with a
+> single underscore will not be imported into the local scope via the
+> `from [module] import *` techinque.
+
+
+So with all of this in mind, we can adjust our `FSLMaths` class to discourage
+our sloppy research assistant from overwriting the `img` attribute:
+
+
+```
+# remainder of definition omitted for brevity
+class FSLMaths(object):
+    def __init__(self, inimg):
+        self.__img        = inimg
+        self.__operations = []
+```
+
+But now we have lost the ability to read our `__img` attribute:
+
+
+```
+inimg = nib.load(op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz'))
+fm = FSLMaths(inimg)
+print(fm.__img)
+```
+
+
+<a class="anchor" id="a-better-way-properties"></a>
+### A better way - properties
+
+
+Python has a feature called
+[`properties`](https://docs.python.org/3.5/library/functions.html#property),
+which is a nice way of controlling access to the attributes of an object. We
+can use properties by defining a "getter" method which can be used to access
+our attributes, and "decorating" them with the `@property` decorator (we will
+cover decorators in a later practical).
+
+
+```
+class FSLMaths(object):
+    def __init__(self, inimg):
+        self.__img        = inimg
+        self.__operations = []
+
+    @property
+    def img(self):
+        return self.__img
+```
+
+
+So we are still storing our input image as a private attribute, but now we
+have made it available in a read-only manner via the public `img` property:
+
+
+```
+fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')
+inimg = nib.load(fpath)
+fm    = FSLMaths(inimg)
+
+print(fm.img.get_filename())
+```
+
+
+Note that, even though we have defined `img` as a method, we can access it
+like an attribute - this is due to the magic behind the `@property` decorator.
+
+
+We can also define "setter" methods for a property. For example, we might wish
+to add the ability for a user of our `FSLMaths` class to change the input
+image after creation.
+
+
+```
+class FSLMaths(object):
+    def __init__(self, inimg):
+        self.__img        = None
+        self.__operations = []
+        self.img          = inimg
+
+    @property
+    def img(self):
+        return self.__img
+
+    @img.setter
+    def img(self, value):
+        if not isinstance(value, nib.nifti1.Nifti1Image):
+            raise ValueError('value must be a NIFTI image!')
+        self.__img = value
+```
+
+
+> Note that we used the `img` setter method within `__init__` to validate the
+> initial `inimg` that was passed in during creation.
+
+
+Property setters are a nice way to add validation logic for when an attribute
+is assigned a value. In this example, an error will be raised if the new input
+is not a NIFTI image.
+
+
+```
+fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')
+inimg = nib.load(fpath)
+fm    = FSLMaths(inimg)
+
+print('Input:     ', fm.img.get_filename())
+
+# let's change the input
+# to a different image
+fpath2 = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain.nii.gz')
+inimg2 = nib.load(fpath2)
+fm.img = inimg2
+
+print('New input: ', fm.img.get_filename())
+
+print('This is going to explode')
+fm.img = 'abcde'
+```
+
+
+<a class="anchor" id="inheritance"></a>
+## Inheritance
+
+
+One of the major advantages of an object-oriented programming approach is
+_inheritance_ - the ability to define hierarchical relationships between
+classes and instances.
+
+
+<a class="anchor" id="the-basics"></a>
+### The basics
+
+
+My local veterinary surgery runs some Python code which looks like the
+following, to assist the nurses in identifying an animal when it arrives at
+the surgery:
+
+
+```
+class Animal(object):
+    def noiseMade(self):
+        raise NotImplementedError('This method must be '
+                                  'implemented by sub-classes')
+
+class Dog(Animal):
+    def noiseMade(self):
+        return 'Woof'
+
+class TalkingDog(Dog):
+    def noiseMade(self):
+        return 'Hi Homer, find your soulmate!'
+
+class Cat(Animal):
+    def noiseMade(self):
+        return 'Meow'
+
+class Labrador(Dog):
+    pass
+
+class Chihuahua(Dog):
+    def noiseMade(self):
+        return 'Yap yap yap'
+```
+
+
+Hopefully this example doesn't need much in the way of explanation - this
+collection of classes captures a hierarchical relationship which exists in the
+real world (and also captures the inherently annoying nature of
+chihuahuas). For example, in the real world, all dogs are animals, but not all
+animals are dogs.  Therefore in our model, the `Dog` class has specified
+`Animal` as its base class. We say that the `Dog` class _extends_, _derives
+from_, or _inherits from_, the `Animal` class, and that all `Dog` instances
+are also `Animal` instances (but not vice-versa).
+
+
+What does that `noiseMade` method do?  There is a `noiseMade` method defined
+on the `Animal` class, but it has been re-implemented, or _overridden_ in the
+`Dog`,
+[`TalkingDog`](https://twitter.com/simpsonsqotd/status/427941665836630016?lang=en),
+`Cat`, and `Chihuahua` classes (but not on the `Labrador` class).  We can call
+the `noiseMade` method on any `Animal` instance, but the specific behaviour
+that we get is dependent on the specific type of animal.
+
+
+```
+d  = Dog()
+l  = Labrador()
+c  = Cat()
+ch = Chihuahua()
+
+print('Noise made by dogs:       {}'.format(d .noiseMade()))
+print('Noise made by labradors:  {}'.format(l .noiseMade()))
+print('Noise made by cats:       {}'.format(c .noiseMade()))
+print('Noise made by chihuahuas: {}'.format(ch.noiseMade()))
+```
+
+
+Note that calling the `noiseMade` method on a `Labrador` instance resulted in
+the `Dog.noiseMade` implementation being called.
+
+
+<a class="anchor" id="code-re-use-and-problem-decomposition"></a>
+### Code re-use and problem decomposition
+
+
+Inheritance allows us to split a problem into smaller problems, and to re-use
+code.  Let's demonstrate this with a more involved (and even more contrived)
+example.  Imagine that a former colleague had written a class called
+`Operator`:
+
+
+```
+class Operator(object):
+
+    def __init__(self):
+        super().__init__() # this line will be explained later
+        self.__operations = []
+        self.__opFuncs    = {}
+
+    @property
+    def operations(self):
+        return list(self.__operations)
+
+    @property
+    def functions(self):
+        return dict(self.__opFuncs)
+
+    def addFunction(self, name, func):
+        self.__opFuncs[name] = func
+
+    def do(self, name, *values):
+        self.__operations.append((name, values))
+
+    def preprocess(self, value):
+        return value
+
+    def run(self, input):
+        data = self.preprocess(input)
+        for oper, vals in self.__operations:
+            func = self.__opFuncs[oper]
+            vals = [self.preprocess(v) for v in vals]
+            data = func(data, *vals)
+        return data
+```
+
+
+This `Operator` class provides an interface and logic to execute a chain of
+operations - an operation is some function which accepts one or more inputs,
+and produce one output.
+
+
+But it stops short of defining any operations. Instead, we can create another
+class - a sub-class - which derives from the `Operator` class. This sub-class
+will define the operations that will ultimately be executed by the `Operator`
+class. All that the `Operator` class does is:
+
+- Allow functions to be registered with the `addFunction` method - all
+  registered functions can be used via the `do` method.
+
+- Stage an operation (using a registered function) via the `do` method. Note
+  that `do` allows any number of values to be passed to it, as we used the `*`
+  operator when specifying the `values` argument.
+
+- Run all staged operations via the `run` method - it passes an input through
+  all of the operations that have been staged, and then returns the final
+  result.
+
+
+Let's define a sub-class:
+
+
+```
+class NumberOperator(Operator):
+
+    def __init__(self):
+        super().__init__()
+        self.addFunction('add',    self.add)
+        self.addFunction('mul',    self.mul)
+        self.addFunction('negate', self.negate)
+
+    def preprocess(self, value):
+        return float(value)
+
+    def add(self, a, b):
+        return a + b
+
+    def mul(self, a, b):
+        return a * b
+
+    def negate(self, a):
+        return -a
+```
+
+
+The `NumberOperator` is a sub-class of `Operator`, which we can use for basic
+numerical calculations. It provides a handful of simple numerical methods, but
+the most interesting stuff is inside `__init__`.
+
+
+> ```
+> super().__init__()
+> ```
+
+
+This line invokes `Operator.__init__` - the initialisation method for the
+`Operator` base-class.
+
+
+In Python, we can use the [built-in `super`
+method](https://docs.python.org/3.5/library/functions.html#super) to take care
+of correctly calling methods that are defined in an object's base-class (or
+classes, in the case of [multiple inheritance](multiple-inheritance)).
+
+
+> The `super` function is one thing which changed between Python 2 and 3 -
+> in Python 2, it was necessary to pass both the type and the instance
+> to `super`. So it is common to see code that looks like this:
+>
+> ```
+> def __init__(self):
+>     super(NumberOperator, self).__init__()
+> ```
+>
+> Fortunately things are a lot cleaner in Python 3.
+
+
+Let's move on to the next few lines in `__init__`:
+
+
+> ```
+> self.addFunction('add',    self.add)
+> self.addFunction('mul',    self.mul)
+> self.addFunction('negate', self.negate)
+> ```
+
+
+Here we are registering all of the functionality that is provided by the
+`NumberOperator` class, via the `Operator.addFunction` method.
+
+
+The `NumberOperator` class has also overridden the `preprocess` method, to
+ensure that all values handled by the `Operator` are numbers. This method gets
+called within the `Operator.run` method - for a `NumberOperator` instance, the
+`NumberOperator.preprocess` method will get called<sup>1</sup>.
+
+
+> <sup>1</sup> When a sub-class overrides a base-class method, it is still
+> possible to access the base-class implementation [via the `super()`
+> function](https://stackoverflow.com/a/4747427) (the preferred method), or by
+> [explicitly calling the base-class
+> implementation](https://stackoverflow.com/a/2421325).
+
+
+Now let's see what our `NumberOperator` class does:
+
+
+```
+no = NumberOperator()
+no.do('add', 10)
+no.do('mul', 2)
+no.do('negate')
+
+print('Operations on {}: {}'.format(10,  no.run(10)))
+print('Operations on {}: {}'.format(2.5, no.run(5)))
+```
+
+
+It works! While this is a contrived example, hopefully you can see how
+inheritance can be used to break a problem down into sub-problems:
+
+- The `Operator` class provides all of the logic needed to manage and execute
+  operations, without caring about what those operations are actually doing.
+
+- This leaves the `NumberOperator` class free to concentrate on implementing
+  the functions that are specific to its task, and not having to worry about
+  how they are executed.
+
+
+We could also easily implement other `Operator` sub-classes to work on
+different data types, such as arrays, images, or even non-numeric data such as
+strings:
+
+
+```
+class StringOperator(Operator):
+    def __init__(self):
+        super().__init__()
+        self.addFunction('capitalise', self.capitalise)
+        self.addFunction('concat',     self.concat)
+
+    def preprocess(self, value):
+        return str(value)
+
+    def capitalise(self, s):
+        return ' '.join([w[0].upper() + w[1:] for w in s.split()])
+
+    def concat(self, s1, s2):
+        return s1 + s2
+
+so = StringOperator()
+so.do('capitalise')
+so.do('concat', '!')
+
+print(so.run('python is an ok language'))
+```
+
+<a class="anchor" id="polymorphism"></a>
+### Polymorphism
+
+
+Inheritance also allows us to take advantage of _polymorphism_, which refers
+to idea that, in an object-oriented language, we should be able to use an
+object without having complete knowledge about the class, or type, of that
+object. For example, we should be able to write a function which expects an
+`Operator` instance, but which will work on an instance of any `Operator`
+sub-classs. As an example, let's write a function which prints a summary of an
+`Operator` instance:
+
+
+```
+def operatorSummary(o):
+    print(type(o).__name__)
+    print('  All functions: ')
+    for fname in o.functions.keys():
+        print('    {}'.format(fname))
+    print('  Staged operations: ')
+    for i, (fname, vals) in enumerate(o.operations):
+        vals = ', '.join([str(v) for v in vals])
+        print('    {}: {}({})'.format(i + 1, fname, vals))
+```
+
+
+Because the `operatorSummary` function only uses methods that are defined
+in the `Operator` base-class, we can use it on _any_ `Operator` instance,
+regardless of its specific type:
+
+
+```
+operatorSummary(no)
+operatorSummary(so)
+```
+
+
+<a class="anchor" id="multiple-inheritance"></a>
+### Multiple inheritance
+
+
+Python allows you to define a class which has multiple base classes - this is
+known as _multiple inheritance_. For example, we might want to build a
+notification mechanisim into our `StringOperator` class, so that listeners can
+be notified whenever the `capitalise` method gets called. It so happens that
+our old colleague of `Operator` class fame also wrote a `Notifier` class which
+allows listeners to register to be notified when an event occurs:
+
+
+```
+class Notifier(object):
+
+    def __init__(self):
+        super().__init__()
+        self.__listeners = {}
+
+    def register(self, name, func):
+        self.__listeners[name] = func
+
+    def notify(self, *args, **kwargs):
+        for func in self.__listeners.values():
+            func(*args, **kwargs)
+```
+
+
+Let's modify the `StringOperator` class to use the functionality of the
+`Notifier ` class:
+
+
+```
+class StringOperator(Operator, Notifier):
+
+    def __init__(self):
+        super().__init__()
+        self.addFunction('capitalise', self.capitalise)
+        self.addFunction('concat',     self.concat)
+
+    def preprocess(self, value):
+        return str(value)
+
+    def capitalise(self, s):
+        result = ' '.join([w[0].upper() + w[1:] for w in s.split()])
+        self.notify(result)
+        return result
+
+    def concat(self, s1, s2):
+        return s1 + s2
+```
+
+
+Now, anything which is interested in uses of the `capitalise` method can
+register as a listener on a `StringOperator` instance:
+
+
+```
+so = StringOperator()
+
+def capitaliseCalled(result):
+    print('Capitalise operation called: {}'.format(result))
+
+so.register('mylistener', capitaliseCalled)
+
+so = StringOperator()
+so.do('capitalise')
+so.do('concat', '?')
+
+print(so.run('did you notice that functions are objects too'))
+```
+
+If you wish to use multiple inheritance in your design, it is important to be
+aware of the mechanism that Python uses to determine how base class methods
+are called (and which base class method will be called, in the case of naming
+conflicts). This is referred to as the Method Resolution Order (MRO) - further
+details on the topic can be found
+[here](https://www.python.org/download/releases/2.3/mro/), and a more concise
+summary
+[here](http://python-history.blogspot.co.uk/2010/06/method-resolution-order.html).
+
+
+Note also that for base class `__init__` methods to be correctly called in a
+design which uses multiple inheritance, _all_ classes in the hierarchy must
+invoke `super().__init__()`. This can become complicated when some base
+classes expect to be passed arguments to their `__init__` method. In scenarios
+like this it may be prefereable to manually invoke the base class `__init__`
+methods instead of using `super()`. For example:
+
+
+> ```
+> class StringOperator(Operator, Notifier):
+>     def __init__(self):
+>         Operator.__init__(self)
+>         Notifier.__init__(self)
+> ```
+
+
+This approach has the disadvantage that if the base classes change, you will
+have to change these invocations. But the advantage is that you know exactly
+how the class hierarchy will be initialised. In general though, doing
+everything with `super()` will result in more maintainable code.
+
+
+<a class="anchor" id="class-attributes-and-methods"></a>
+## Class attributes and methods
+
+
+Up to this point we have been covering how to add attributes and methods to an
+_object_. But it is also possible to add methods and attributes to a _class_
+(`static` methods and fields, for those of you familiar with C++ or Java).
+
+
+Class attributes and methods can be accessed without having to create an
+instance of the class - they are not associated with individual objects, but
+rather with the class itself.
+
+
+Class methods and attributes can be useful in several scenarios - as a
+hypothetical, not very useful example, let's say that we want to gain usage
+statistics for how many times each type of operation is used on instances of
+our `FSLMaths` class. We might, for example, use this information in a grant
+application to show evidence that more research is needed to optimise the
+performance of the `add` operation.
+
+
+<a class="anchor" id="class-attributes"></a>
+### Class attributes
+
+
+Let's add a `dict` called `opCounters` as a class attribute to the `FSLMaths`
+class - whenever an operation is called on a `FSLMaths` instance, the counter
+for that operation will be incremented:
+
+
+```
+import numpy   as np
+import nibabel as nib
+
+class FSLMaths(object):
+
+    # It's this easy to add a class-level
+    # attribute. This dict is associated
+    # with the FSLMaths *class*, not with
+    # any individual FSLMaths instance.
+    opCounters = {}
+
+    def __init__(self, inimg):
+        self.img        = inimg
+        self.operations = []
+
+    def add(self, value):
+        self.operations.append(('add', value))
+
+    def mul(self, value):
+        self.operations.append(('mul', value))
+
+    def div(self, value):
+        self.operations.append(('div', value))
+
+    def run(self, output=None):
+
+        data = np.array(self.img.get_data())
+
+        for oper, value in self.operations:
+
+            # Code omitted for brevity
+
+            # Increment the usage counter
+            # for this operation. We can
+            # access class attributes (and
+            # methods) through the class
+            # itself.
+            FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1
+```
+
+
+So let's see it in action:
+
+
+```
+fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')
+fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')
+inimg = nib.load(fpath)
+mask  = nib.load(fmask)
+
+fm1 = FSLMaths(inimg)
+fm2 = FSLMaths(inimg)
+
+fm1.mul(mask)
+fm1.add(15)
+
+fm2.add(25)
+fm1.div(1.5)
+
+fm1.run()
+fm2.run()
+
+print('FSLMaths usage statistics')
+for oper in ('add', 'div', 'mul'):
+    print('  {} : {}'.format(oper, FSLMaths.opCounters.get(oper, 0)))
+```
+
+
+<a class="anchor" id="class-methods"></a>
+### Class methods
+
+
+It is just as easy to add a method to a class - let's take our reporting code
+from above, and add it as a method to the `FSLMaths` class.
+
+
+A class method is denoted by the
+[`@classmethod`](https://docs.python.org/3.5/library/functions.html#classmethod)
+decorator. Note that, where a regular method which is called on an instance
+will be passed the instance as its first argument (`self`), a class method
+will be passed the class itself as the first argument - the standard
+convention is to call this argument `cls`:
+
+
+```
+class FSLMaths(object):
+
+    opCounters = {}
+
+    @classmethod
+    def usage(cls):
+        print('FSLMaths usage statistics')
+        for oper in ('add', 'div', 'mul'):
+            print('  {} : {}'.format(oper, FSLMaths.opCounters.get(oper, 0)))
+
+    def __init__(self, inimg):
+        self.img        = inimg
+        self.operations = []
+
+    def add(self, value):
+        self.operations.append(('add', value))
+
+    def mul(self, value):
+        self.operations.append(('mul', value))
+
+    def div(self, value):
+        self.operations.append(('div', value))
+
+    def run(self, output=None):
+
+        data = np.array(self.img.get_data())
+
+        for oper, value in self.operations:
+            FSLMaths.opCounters[oper] = self.opCounters.get(oper, 0) + 1
+```
+
+
+> There is another decorator -
+> [`@staticmethod`](https://docs.python.org/3.5/library/functions.html#staticmethod) -
+> which can be used on methods defined within a class. The difference
+> between a `@classmethod` and a `@staticmethod` is that the latter will _not_
+> be passed the class (`cls`).
+
+
+calling a class method is the same as accessing a class attribute:
+
+
+```
+fpath = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm.nii.gz')
+fmask = op.expandvars('$FSLDIR/data/standard/MNI152_T1_2mm_brain_mask.nii.gz')
+inimg = nib.load(fpath)
+mask  = nib.load(fmask)
+
+fm1 = FSLMaths(inimg)
+fm2 = FSLMaths(inimg)
+
+fm1.mul(mask)
+fm1.add(15)
+
+fm2.add(25)
+fm1.div(1.5)
+
+fm1.run()
+fm2.run()
+
+FSLMaths.usage()
+```
+
+
+Note that it is also possible to access class attributes and methods through
+instances:
+
+
+```
+print(fm1.opCounters)
+fm1.usage()
+```
+
+
+<a class="anchor" id="appendix-the-object-base-class"></a>
+## Appendix: The `object` base-class
+
+
+When you are defining a class, you need to specify the base-class from which
+your class inherits. If your class does not inherit from a particular class,
+then it should inherit from the built-in `object` class:
+
+
+> ```
+> class MyClass(object):
+>     ...
+> ```
+
+
+However, in older code bases, you might see class definitions that look like
+this, without explicitly inheriting from the `object` base class:
+
+
+> ```
+> class MyClass:
+>     ...
+> ```
+
+
+This syntax is a [throwback to older versions of
+Python](https://docs.python.org/2/reference/datamodel.html#new-style-and-classic-classes).
+In Python 3 there is actually no difference in defining classes in the
+"new-style" way we have used throughout this tutorial, or the "old-style" way
+mentioned in this appendix.
+
+
+But if you are writing code which needs to run on both Python 2 and 3, you
+__must__ define your classes in the new-style convention, i.e. by explicitly
+inheriting from the `object` base class. Therefore, the safest approach is to
+always use the new-style format.
+
+
+<a class="anchor" id="appendix-init-versus-new"></a>
+## Appendix: `__init__` versus `__new__`
+
+
+In Python, object creation is actually a two-stage process - _creation_, and
+then _initialisation_. The `__init__` method gets called during the
+_initialisation_ stage - its job is to initialise the state of the object. But
+note that, by the time `__init__` gets called, the object has already been
+created.
+
+
+You can also define a method called `__new__` if you need to control the
+creation stage, although this is very rarely needed. A brief explanation on
+the difference between `__new__` and `__init__` can be found
+[here](https://www.reddit.com/r/learnpython/comments/2s3pms/what_is_the_difference_between_init_and_new/cnm186z/),
+and you may also wish to take a look at the [official Python
+docs](https://docs.python.org/3.5/reference/datamodel.html#basic-customization).
+
+
+<a class="anchor" id="appendix-monkey-patching"></a>
+## Appendix: Monkey-patching
+
+
+The act of run-time modification of objects or class definitions is referred
+to as [_monkey-patching_](https://en.wikipedia.org/wiki/Monkey_patch) and,
+whilst it is allowed by the Python programming language, it is generally
+considered quite bad practice.
+
+
+Just because you _can_ do something doesn't mean that you _should_. Python
+gives you the flexibility to write your software in whatever manner you deem
+suitable.  __But__ if you want to write software that will be used, adopted,
+maintained, and enjoyed by other people, you should be polite, write your code
+in a clear, readable fashion, and avoid the use of devious tactics such as
+monkey-patching.
+
+
+__However__, while monkey-patching may seem like a horrific programming
+practice to those of you coming from the realms of C++, Java, and the like,
+(and it is horrific in many cases), it can be _extremely_ useful in certain
+circumstances.  For instance, monkey-patching makes [unit testing a
+breeze in Python](https://docs.python.org/3.5/library/unittest.mock.html).
+
+
+As another example, consider the scenario where you are dependent on a third
+party library which has bugs in it. No problem - while you are waiting for the
+library author to release a new version of the library, you can write your own
+working implementation and [monkey-patch it
+in!](https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/blob/0.21.0/fsleyes/views/viewpanel.py#L726)
+
+
+<a class="anchor" id="appendix-method-overloading"></a>
+## Appendix: Method overloading
+
+
+Method overloading (defining multiple methods with the same name in a class,
+but each accepting different arguments) is one of the only object-oriented
+features that is not present in Python. Becuase Python does not perform any
+runtime checks on the types of arguments that are passed to a method, or the
+compatibility of the method to accept the arguments, it would not be possible
+to determine which implementation of a method is to be called. In other words,
+in Python only the name of a method is used to identify that method, unlike in
+C++ and Java, where the full method signature (name, input types and return
+types) is used.
+
+
+However, because a Python method can be written to accept any number or type
+of arguments, it is very easy to to build your own overloading logic by
+writing a "dispatch" method. Here is YACE (Yet Another Contrived Example):
+
+
+```
+class Adder(object):
+
+    def add(self, *args):
+        if   len(args) == 2: return self.__add2(*args)
+        elif len(args) == 3: return self.__add3(*args)
+        elif len(args) == 4: return self.__add4(*args)
+        else:
+            raise AttributeError('No method available to accept {} '
+                                 'arguments'.format(len(args)))
+
+    def __add2(self, a, b):
+        return a + b
+
+    def __add3(self, a, b, c):
+        return a + b + c
+
+    def __add4(self, a, b, c, d):
+        return a + b + c + d
+
+a = Adder()
+
+print('Add two:   {}'.format(a.add(1, 2)))
+print('Add three: {}'.format(a.add(1, 2, 3)))
+print('Add four:  {}'.format(a.add(1, 2, 3, 4)))
+```
+
+
+<a class="anchor" id="useful-references"></a>
+## Useful references
+
+
+The official Python documentation has a wealth of information on the internal
+workings of classes and objects, so these pages are worth a read:
+
+
+* https://docs.python.org/3.5/tutorial/classes.html
+* https://docs.python.org/3.5/reference/datamodel.html
diff --git a/getting_started/basics.ipynb b/getting_started/01_basics.ipynb
similarity index 56%
rename from getting_started/basics.ipynb
rename to getting_started/01_basics.ipynb
index 4cd6d7951303564e791df3d80c95220afd2ce159..a3210a7e20a0249eb372f786bdbd83688074a0d2 100644
--- a/getting_started/basics.ipynb
+++ b/getting_started/01_basics.ipynb
@@ -30,17 +30,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 63,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "4\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "a = 4\n",
     "b = 3.6\n",
@@ -59,19 +51,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 64,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[10, 20, 30]\n",
-      "{'b': 20, 'a': 10}\n",
-      "4 3.6 abc\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "print(d)\n",
     "print(e)\n",
@@ -100,17 +82,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 65,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "test string  ::  another test string\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "s1 = \"test string\"\n",
     "s2 = 'another test string'\n",
@@ -126,20 +100,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 66,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "This is\n",
-      "a string over\n",
-      "multiple lines\n",
-      "\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "s3 = '''This is\n",
     "a string over\n",
@@ -159,18 +122,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 67,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "The numerical value is 1 and a name is PyTreat\n",
-      "A name is PyTreat and a number is 1\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "x = 1\n",
     "y = 'PyTreat'\n",
@@ -192,18 +146,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 68,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "THIS IS A TEST STRING\n",
-      "this is a test string\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "s = 'This is a Test String'\n",
     "print(s.upper())\n",
@@ -219,17 +164,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 69,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "This is a Better String\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "s = 'This is a Test String'\n",
     "s2 = s.replace('Test', 'Better')\n",
@@ -245,17 +182,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 70,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "This is an example of an example String\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "import re\n",
     "s = 'This is a test of a Test String'\n",
@@ -277,17 +206,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 71,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "['This', 'is', 'a', 'test', 'of', 'a', 'Test', 'String']\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "print(s.split())"
    ]
@@ -311,18 +232,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 72,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "(3, 7.6, 'str')\n",
-      "[1, 'mj', -5.4]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "xtuple = (3, 7.6, 'str')\n",
     "xlist = [1, 'mj', -5.4]\n",
@@ -339,18 +251,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 73,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "x2 is:  ((3, 7.6, 'str'), [1, 'mj', -5.4])\n",
-      "x3 is:  [(3, 7.6, 'str'), [1, 'mj', -5.4]]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "x2 = (xtuple, xlist)\n",
     "x3 = [xtuple, xlist]\n",
@@ -369,17 +272,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 74,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[10, 20, 30, 70, 80]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "a = [10, 20, 30]\n",
     "a = a + [70]\n",
@@ -398,17 +293,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 75,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "20\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "d = [10, 20, 30]\n",
     "print(d[1])"
@@ -424,18 +311,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 76,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "10\n",
-      "30\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "a = [10, 20, 30, 40, 50, 60]\n",
     "print(a[0])\n",
@@ -451,18 +329,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 77,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "60\n",
-      "10\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "print(a[-1])\n",
     "print(a[-6])"
@@ -477,42 +346,18 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 78,
-   "metadata": {},
-   "outputs": [
-    {
-     "ename": "IndexError",
-     "evalue": "list index out of range",
-     "traceback": [
-      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
-      "\u001b[0;31mIndexError\u001b[0m                                Traceback (most recent call last)",
-      "\u001b[0;32m<ipython-input-78-f4cf4536701c>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m7\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
-      "\u001b[0;31mIndexError\u001b[0m: list index out of range"
-     ],
-     "output_type": "error"
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "print(a[-7])"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 79,
-   "metadata": {},
-   "outputs": [
-    {
-     "ename": "IndexError",
-     "evalue": "list index out of range",
-     "traceback": [
-      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
-      "\u001b[0;31mIndexError\u001b[0m                                Traceback (most recent call last)",
-      "\u001b[0;32m<ipython-input-79-52d95fbe5286>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m6\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
-      "\u001b[0;31mIndexError\u001b[0m: list index out of range"
-     ],
-     "output_type": "error"
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "print(a[6])"
    ]
@@ -526,17 +371,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 80,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "6\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "print(len(a))"
    ]
@@ -550,18 +387,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 81,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "20\n",
-      "40\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "b = [[10, 20, 30], [40, 50, 60]]\n",
     "print(b[0][1])\n",
@@ -584,17 +412,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 82,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[10, 20, 30]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "print(a[0:3])"
    ]
@@ -611,18 +431,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 83,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[10, 20, 30]\n",
-      "[20, 30]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "a = [10, 20, 30, 40, 50, 60]\n",
     "print(a[0:3])    # same as a(1:3) in MATLAB\n",
@@ -641,21 +452,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 84,
-   "metadata": {},
-   "outputs": [
-    {
-     "ename": "TypeError",
-     "evalue": "list indices must be integers or slices, not list",
-     "traceback": [
-      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
-      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
-      "\u001b[0;32m<ipython-input-84-aad7915ae3d8>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0mb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mb\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
-      "\u001b[0;31mTypeError\u001b[0m: list indices must be integers or slices, not list"
-     ],
-     "output_type": "error"
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "b = [3, 4]\n",
     "print(a[b])"
@@ -672,17 +471,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 85,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[10, 20, 30, 10, 20, 30, 10, 20, 30, 10, 20, 30]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "d = [10, 20, 30]\n",
     "print(d * 4)"
@@ -697,19 +488,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 86,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[10, 20, 30, 40]\n",
-      "[10, 30, 40]\n",
-      "[30, 40]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "d.append(40)\n",
     "print(d)\n",
@@ -728,19 +509,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 87,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "10\n",
-      "20\n",
-      "30\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "d = [10, 20, 30]\n",
     "for x in d:\n",
@@ -760,134 +531,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 88,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Help on list object:\n",
-      "\n",
-      "class list(object)\n",
-      " |  list() -> new empty list\n",
-      " |  list(iterable) -> new list initialized from iterable's items\n",
-      " |  \n",
-      " |  Methods defined here:\n",
-      " |  \n",
-      " |  __add__(self, value, /)\n",
-      " |      Return self+value.\n",
-      " |  \n",
-      " |  __contains__(self, key, /)\n",
-      " |      Return key in self.\n",
-      " |  \n",
-      " |  __delitem__(self, key, /)\n",
-      " |      Delete self[key].\n",
-      " |  \n",
-      " |  __eq__(self, value, /)\n",
-      " |      Return self==value.\n",
-      " |  \n",
-      " |  __ge__(self, value, /)\n",
-      " |      Return self>=value.\n",
-      " |  \n",
-      " |  __getattribute__(self, name, /)\n",
-      " |      Return getattr(self, name).\n",
-      " |  \n",
-      " |  __getitem__(...)\n",
-      " |      x.__getitem__(y) <==> x[y]\n",
-      " |  \n",
-      " |  __gt__(self, value, /)\n",
-      " |      Return self>value.\n",
-      " |  \n",
-      " |  __iadd__(self, value, /)\n",
-      " |      Implement self+=value.\n",
-      " |  \n",
-      " |  __imul__(self, value, /)\n",
-      " |      Implement self*=value.\n",
-      " |  \n",
-      " |  __init__(self, /, *args, **kwargs)\n",
-      " |      Initialize self.  See help(type(self)) for accurate signature.\n",
-      " |  \n",
-      " |  __iter__(self, /)\n",
-      " |      Implement iter(self).\n",
-      " |  \n",
-      " |  __le__(self, value, /)\n",
-      " |      Return self<=value.\n",
-      " |  \n",
-      " |  __len__(self, /)\n",
-      " |      Return len(self).\n",
-      " |  \n",
-      " |  __lt__(self, value, /)\n",
-      " |      Return self<value.\n",
-      " |  \n",
-      " |  __mul__(self, value, /)\n",
-      " |      Return self*value.n\n",
-      " |  \n",
-      " |  __ne__(self, value, /)\n",
-      " |      Return self!=value.\n",
-      " |  \n",
-      " |  __new__(*args, **kwargs) from builtins.type\n",
-      " |      Create and return a new object.  See help(type) for accurate signature.\n",
-      " |  \n",
-      " |  __repr__(self, /)\n",
-      " |      Return repr(self).\n",
-      " |  \n",
-      " |  __reversed__(...)\n",
-      " |      L.__reversed__() -- return a reverse iterator over the list\n",
-      " |  \n",
-      " |  __rmul__(self, value, /)\n",
-      " |      Return self*value.\n",
-      " |  \n",
-      " |  __setitem__(self, key, value, /)\n",
-      " |      Set self[key] to value.\n",
-      " |  \n",
-      " |  __sizeof__(...)\n",
-      " |      L.__sizeof__() -- size of L in memory, in bytes\n",
-      " |  \n",
-      " |  append(...)\n",
-      " |      L.append(object) -> None -- append object to end\n",
-      " |  \n",
-      " |  clear(...)\n",
-      " |      L.clear() -> None -- remove all items from L\n",
-      " |  \n",
-      " |  copy(...)\n",
-      " |      L.copy() -> list -- a shallow copy of L\n",
-      " |  \n",
-      " |  count(...)\n",
-      " |      L.count(value) -> integer -- return number of occurrences of value\n",
-      " |  \n",
-      " |  extend(...)\n",
-      " |      L.extend(iterable) -> None -- extend list by appending elements from the iterable\n",
-      " |  \n",
-      " |  index(...)\n",
-      " |      L.index(value, [start, [stop]]) -> integer -- return first index of value.\n",
-      " |      Raises ValueError if the value is not present.\n",
-      " |  \n",
-      " |  insert(...)\n",
-      " |      L.insert(index, object) -- insert object before index\n",
-      " |  \n",
-      " |  pop(...)\n",
-      " |      L.pop([index]) -> item -- remove and return item at index (default last).\n",
-      " |      Raises IndexError if list is empty or index is out of range.\n",
-      " |  \n",
-      " |  remove(...)\n",
-      " |      L.remove(value) -> None -- remove first occurrence of value.\n",
-      " |      Raises ValueError if the value is not present.\n",
-      " |  \n",
-      " |  reverse(...)\n",
-      " |      L.reverse() -- reverse *IN PLACE*\n",
-      " |  \n",
-      " |  sort(...)\n",
-      " |      L.sort(key=None, reverse=False) -> None -- stable sort *IN PLACE*\n",
-      " |  \n",
-      " |  ----------------------------------------------------------------------\n",
-      " |  Data and other attributes defined here:\n",
-      " |  \n",
-      " |  __hash__ = None\n",
-      "\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "help(d)"
    ]
@@ -901,64 +547,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 89,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "['__add__',\n",
-       " '__class__',\n",
-       " '__contains__',\n",
-       " '__delattr__',\n",
-       " '__delitem__',\n",
-       " '__dir__',\n",
-       " '__doc__',\n",
-       " '__eq__',\n",
-       " '__format__',\n",
-       " '__ge__',\n",
-       " '__getattribute__',\n",
-       " '__getitem__',\n",
-       " '__gt__',\n",
-       " '__hash__',\n",
-       " '__iadd__',\n",
-       " '__imul__',\n",
-       " '__init__',\n",
-       " '__iter__',\n",
-       " '__le__',\n",
-       " '__len__',\n",
-       " '__lt__',\n",
-       " '__mul__',\n",
-       " '__ne__',\n",
-       " '__new__',\n",
-       " '__reduce__',\n",
-       " '__reduce_ex__',\n",
-       " '__repr__',\n",
-       " '__reversed__',\n",
-       " '__rmul__',\n",
-       " '__setattr__',\n",
-       " '__setitem__',\n",
-       " '__sizeof__',\n",
-       " '__str__',\n",
-       " '__subclasshook__',\n",
-       " 'append',\n",
-       " 'clear',\n",
-       " 'copy',\n",
-       " 'count',\n",
-       " 'extend',\n",
-       " 'index',\n",
-       " 'insert',\n",
-       " 'pop',\n",
-       " 'remove',\n",
-       " 'reverse',\n",
-       " 'sort']"
-      ]
-     },
-     "execution_count": 89,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "dir(d)"
    ]
@@ -978,20 +569,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 90,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "2\n",
-      "dict_keys(['b', 'a'])\n",
-      "dict_values([20, 10])\n",
-      "10\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "e = {'a' : 10, 'b': 20}\n",
     "print(len(e))\n",
@@ -1015,17 +595,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 91,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "{'c': 555, 'b': 20, 'a': 10}\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "e['c'] = 555   # just like in Biobank!  ;)\n",
     "print(e)"
@@ -1042,18 +614,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 92,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "{'c': 555, 'a': 10}\n",
-      "{'a': 10}\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "e.pop('b')\n",
     "print(e)\n",
@@ -1072,19 +635,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 93,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "('c', 555)\n",
-      "('b', 20)\n",
-      "('a', 10)\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "e = {'a' : 10, 'b': 20, 'c':555}\n",
     "for k, v in e.items():\n",
@@ -1102,19 +655,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 94,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "('c', 555)\n",
-      "('b', 20)\n",
-      "('a', 10)\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "for k in e:\n",
     "    print((k, e[k]))"
@@ -1137,17 +680,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 95,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "7\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "a = 7\n",
     "b = a\n",
@@ -1164,17 +699,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 96,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[8888]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "a = [7]\n",
     "b = a\n",
@@ -1191,17 +718,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 97,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[7, 7]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "a = [7]\n",
     "b = a * 2\n",
@@ -1218,17 +737,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 98,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[7]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "a = [7]\n",
     "b = list(a)\n",
@@ -1245,18 +756,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 99,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "(2, 5, 7)\n",
-      "[2, 5, 7]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "xt = (2, 5, 7)\n",
     "xl = list(xt)\n",
@@ -1275,20 +777,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 100,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[5]\n",
-      "[5, 10]\n",
-      "[5, 10]\n",
-      "[5, 10]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "def foo1(x):\n",
     "   x.append(10)\n",
@@ -1326,21 +817,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 101,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Not a is: False\n",
-      "Not 1 is: False\n",
-      "Not 0 is: True\n",
-      "Not {} is: True\n",
-      "{}==0 is: False\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "a = True\n",
     "print('Not a is:', not a)\n",
@@ -1359,19 +838,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 102,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "False\n",
-      "True\n",
-      "True\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "print('the' in 'a number of words')\n",
     "print('of' in 'a number of words')\n",
@@ -1389,18 +858,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 103,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "0.9933407850276534\n",
-      "Positive\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "import random\n",
     "a = random.uniform(-1, 1)\n",
@@ -1422,17 +882,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 104,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Variable is true, or at least not empty\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "a = []    # just one of many examples\n",
     "if not a:\n",
@@ -1454,21 +906,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 105,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "2\n",
-      "is\n",
-      "more\n",
-      "than\n",
-      "1\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "for x in [2, 'is', 'more', 'than', 1]:\n",
     "   print(x)"
@@ -1485,23 +925,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 106,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "2\n",
-      "3\n",
-      "4\n",
-      "5\n",
-      "6\n",
-      "7\n",
-      "8\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "for x in range(2, 9):\n",
     "  print(x)"
@@ -1518,18 +944,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 107,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "4\n",
-      "7\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "x, y = [4, 7]\n",
     "print(x)\n",
@@ -1545,21 +962,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 108,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[('Some', 0), ('set', 1), ('of', 2), ('items', 3)]\n",
-      "0 Some\n",
-      "1 set\n",
-      "2 of\n",
-      "3 items\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "alist = ['Some', 'set', 'of', 'items']\n",
     "blist = list(range(len(alist)))\n",
@@ -1581,17 +986,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 109,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "35.041627991396396\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "import random\n",
     "n = 0\n",
@@ -1625,17 +1022,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 110,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "0.6576892259376057 0.11717666603919556\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "import random\n",
     "x = random.uniform(0, 1)\n",
@@ -1654,18 +1043,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 111,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]\n",
-      "[0, 1, 4, 9, 16, 25, 36, 64, 81]\n"
-     ]
-    }
-   ],
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "v1 = [ x**2 for x in range(10) ]\n",
     "print(v1)\n",
@@ -1679,34 +1059,9 @@
    "source": [
     "You'll find that python programmers use this kind of construction _*a lot*_."
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
   }
  ],
- "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.5.2"
-  }
- },
+ "metadata": {},
  "nbformat": 4,
  "nbformat_minor": 2
 }
diff --git a/getting_started/basics.md b/getting_started/01_basics.md
similarity index 100%
rename from getting_started/basics.md
rename to getting_started/01_basics.md
diff --git a/getting_started/04_numpy.ipynb b/getting_started/04_numpy.ipynb
index e877863b0d6105f54087da9a1759aab2a4cc8b31..23b4131ecdd19d26f411a08610cdbc0c851138f5 100644
--- a/getting_started/04_numpy.ipynb
+++ b/getting_started/04_numpy.ipynb
@@ -51,6 +51,7 @@
     "* [Appendix A: Generating random numbers](#appendix-generating-random-numbers)\n",
     "* [Appendix B: Importing Numpy](#appendix-importing-numpy)\n",
     "* [Appendix C: Vectors in Numpy](#appendix-vectors-in-numpy)\n",
+    "* [Appendix D: The Numpy `matrix`](#appendix-the-numpy-matrix)\n",
     "\n",
     "* [Useful references](#useful-references)\n",
     "\n",
@@ -120,7 +121,7 @@
     "unwieldy when you have more than a couple of dimensions.\n",
     "\n",
     "\n",
-    "___Numy array == Matlab matrix:___ These are in contrast to the Numpy array\n",
+    "___Numpy array == Matlab matrix:___ These are in contrast to the Numpy array\n",
     "and Matlab matrix, which are both thin wrappers around a contiguous chunk of\n",
     "memory, and which provide blazing-fast performance (because behind the scenes\n",
     "in both Numpy and Matlab, it's C, C++ and FORTRAN all the way down).\n",
@@ -186,7 +187,8 @@
     "array, thus completely bypassing the use of Python lists and the costly\n",
     "list-to-array conversion.  I'm emphasising this to help you understand the\n",
     "difference between Python lists and Numpy arrays. Apologies if you've already\n",
-    "got it, forgiveness please.\n",
+    "got it, [forgiveness\n",
+    "please](https://www.youtube.com/watch?v=ZeHflFNR4kQ&feature=youtu.be&t=128).\n",
     "\n",
     "\n",
     "<a class=\"anchor\" id=\"numpy-basics\"></a>\n",
@@ -314,7 +316,7 @@
     "> for more information.\n",
     "\n",
     "\n",
-    "  Of course you can also save data out to a text file just as easily, with\n",
+    "Of course you can also save data out to a text file just as easily, with\n",
     "[`numpy.savetxt`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html):"
    ]
   },
@@ -336,6 +338,14 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
+    "> The `fmt` argument to the `numpy.savetxt` function uses a specification\n",
+    "> language similar to that used in the C `printf` function - in the example\n",
+    "> above, `'%i`' indicates that the values of the array should be output as\n",
+    "> signed integers. See the [`numpy.savetxt`\n",
+    "> documentation](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html)\n",
+    "> for more details on specifying the output format.\n",
+    "\n",
+    "\n",
     "<a class=\"anchor\" id=\"array-properties\"></a>\n",
     "### Array properties\n",
     "\n",
@@ -368,7 +378,9 @@
    "source": [
     "> As depicted above, passing a Numpy array to the built-in `len` function will\n",
     "> only give you the length of the first dimension, so you will typically want\n",
-    "> to avoid using it - use the `size` attribute instead.\n",
+    "> to avoid using it - instead, use the `size` attribute if you want to know\n",
+    "> how many elements are in an array, or the `shape` attribute if you want to\n",
+    "> know the array shape.\n",
     "\n",
     "\n",
     "<a class=\"anchor\" id=\"descriptive-statistics\"></a>\n",
@@ -402,6 +414,35 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
+    "These methods can also be applied to arrays with multiple dimensions:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.random.randint(1, 10, (3, 3))\n",
+    "print('a:')\n",
+    "print(a)\n",
+    "print('min:             ', a.min())\n",
+    "print('row mins:        ', a.min(axis=1))\n",
+    "print('col mins:        ', a.min(axis=0))\n",
+    "print('Min index      : ', a.argmin())\n",
+    "print('Row min indices: ', a.argmin(axis=1))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Note that, for a multi-dimensional array, the `argmin` and `argmax` methods\n",
+    "will return the (0-based) index of the minimum/maximum values into a\n",
+    "[flattened](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.ndarray.flatten.html)\n",
+    "view of the array.\n",
+    "\n",
+    "\n",
     "> <sup>2</sup> Python, being an object-oriented language, distinguishes\n",
     "> between _functions_ and _methods_. Hopefully we all know what a function is\n",
     "> - a _method_ is simply the term used to refer to a function that is\n",
@@ -685,8 +726,8 @@
     "froth coming out of your mouth. I guess you're angry that `a * b` didn't give\n",
     "you the matrix product, like it would have in Matlab.  Well all I can say is\n",
     "that Numpy is not Matlab. Matlab operations are typically consistent with\n",
-    "linear algebra notation. This is not the case in Numpy. Get over it. Take a\n",
-    "calmative.\n",
+    "linear algebra notation. This is not the case in Numpy. Get over it.\n",
+    "[Get yourself a calmative](https://youtu.be/M_w_n-8w3IQ?t=32).\n",
     "\n",
     "\n",
     "<a class=\"anchor\" id=\"matrix-multiplication\"></a>\n",
@@ -732,6 +773,48 @@
     "> backwards-compatibility, go ahead and use it!\n",
     "\n",
     "\n",
+    "One potential source of confusion for those of you who are used to Matlab's\n",
+    "linear algebra-based take on things is that Numpy treats row and column\n",
+    "vectors differently - you should take a break now and skim over the [appendix\n",
+    "on vectors in Numpy](#appendix-vectors-in-numpy).\n",
+    "\n",
+    "\n",
+    "For matrix-by-vector multiplications, a 1-dimensional Numpy array may be\n",
+    "treated as _either_ a row vector _or_ a column vector, depending on where\n",
+    "it is in the expression:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.arange(1, 5).reshape((2, 2))\n",
+    "b = np.random.randint(1, 10, 2)\n",
+    "\n",
+    "print('a:')\n",
+    "print(a)\n",
+    "print('b:', b)\n",
+    "\n",
+    "print('a @ b - b is a column vector:')\n",
+    "print(a @ b)\n",
+    "print('b @ a - b is a row vector:')\n",
+    "print(b @ a)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "If you really can't stand using `@` to denote matrix multiplication, and just\n",
+    "want things to be like they were back in Matlab-land, you do have the option\n",
+    "of using a different Numpy data type - the `matrix` - which behaves a bit more\n",
+    "like what you might expect from Matlab.  You can find a brief overview of the\n",
+    "`matrix` data type in [the appendix](appendix-the-numpy-matrix).\n",
+    "\n",
+    "\n",
+    "\n",
     "<a class=\"anchor\" id=\"broadcasting\"></a>\n",
     "### Broadcasting\n",
     "\n",
@@ -775,8 +858,7 @@
    "source": [
     "> Here we used a handy feature of the `reshape` method - if you pass `-1` for\n",
     "> the size of one dimension, it will automatically determine the size to use\n",
-    "> for that dimension. Take a look at [the\n",
-    "> appendix](#appendix-vectors-in-numpy) for a discussion on vectors in Numpy.\n",
+    "> for that dimension.\n",
     "\n",
     "\n",
     "Here is a more useful example, where we use broadcasting to de-mean the rows\n",
@@ -1119,7 +1201,7 @@
    "metadata": {},
    "source": [
     "The `numpy.where` function can be combined with boolean arrays to easily\n",
-    "generate of coordinate arrays for values which meet some condition:"
+    "generate coordinate arrays for values which meet some condition:"
    ]
   },
   {
@@ -1408,6 +1490,47 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
+    "<a class=\"anchor\" id=\"appendix-the-numpy-matrix\"></a>\n",
+    "## Appendix D: The Numpy `matrix`\n",
+    "\n",
+    "\n",
+    "By now you should be aware that a Numpy `array` does not behave in quite the\n",
+    "same way as a Matlab matrix. The primary difference between Numpy and Matlab\n",
+    "is that in Numpy, the `*` operator denotes element-wise multiplication,\n",
+    "gwhereas in Matlab, `*` denotes matrix multiplication.\n",
+    "\n",
+    "\n",
+    "Numpy does support the `@` operator for matrix multiplication, but if this is\n",
+    "a complete show-stopper for you - if you just can't bring yourself to write `A\n",
+    "@ B` to denote the matrix product of `A` and `B` - if you _must_ have your\n",
+    "code looking as Matlab-like as possible, then you should look into the Numpy\n",
+    "[`matrix`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.matrix.html)\n",
+    "data type.\n",
+    "\n",
+    "\n",
+    "The `matrix` is an alternative to the `array` which essentially behaves more\n",
+    "like a Matlab matrix:\n",
+    "\n",
+    "* `matrix` objects always have exactly two dimensions.\n",
+    "* `a * b` denotes matrix multiplication, rather than elementwise\n",
+    "  multiplication.\n",
+    "* `matrix` objects have `.H` and `.I` attributes, which are convenient ways to\n",
+    "  access the conjugate transpose and inverse of the matrix respectively.\n",
+    "\n",
+    "\n",
+    "Note however that use of the `matrix` type is _not_ widespread, and if you use\n",
+    "it you will risk confusing others who are familiar with the much more commonly\n",
+    "used `array`, and who need to work with your code. In fact, the official Numpy\n",
+    "documentation [recommends against using the `matrix`\n",
+    "type](https://docs.scipy.org/doc/numpy-dev/user/numpy-for-matlab-users.html#array-or-matrix-which-should-i-use).\n",
+    "\n",
+    "\n",
+    "But if you are writing some very maths-heavy code, and you want your code to\n",
+    "be as clear and concise, and maths/Matlab-like as possible, then the `matrix`\n",
+    "type is there for you. Just make sure you document your code well to make it\n",
+    "clear to others what is going on!\n",
+    "\n",
+    "\n",
     "<a class=\"anchor\" id=\"useful-references\"></a>\n",
     "## Useful references\n",
     "\n",
@@ -1417,7 +1540,8 @@
     "* [Broadcasting in Numpy](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html)\n",
     "* [Indexing in Numpy](https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html)\n",
     "* [Random sampling in `numpy.random`](https://docs.scipy.org/doc/numpy/reference/routines.random.html)\n",
-    "* [Python slicing](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/)"
+    "* [Python slicing](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/)\n",
+    "* [Numpy for Matlab users](https://docs.scipy.org/doc/numpy-dev/user/numpy-for-matlab-users.html)"
    ]
   }
  ],
diff --git a/getting_started/04_numpy.md b/getting_started/04_numpy.md
index dce3532bb6e37ba55f749cd3f7d25cf97878c7da..8ea10a8521108c14c0c4a37a39dca132e961da4d 100644
--- a/getting_started/04_numpy.md
+++ b/getting_started/04_numpy.md
@@ -45,6 +45,7 @@ out of date, but we will update it for the next release of FSL.
 * [Appendix A: Generating random numbers](#appendix-generating-random-numbers)
 * [Appendix B: Importing Numpy](#appendix-importing-numpy)
 * [Appendix C: Vectors in Numpy](#appendix-vectors-in-numpy)
+* [Appendix D: The Numpy `matrix`](#appendix-the-numpy-matrix)
 
 * [Useful references](#useful-references)
 
@@ -98,7 +99,7 @@ array in Matlab - they can store anything, but are extremely inefficient, and
 unwieldy when you have more than a couple of dimensions.
 
 
-___Numy array == Matlab matrix:___ These are in contrast to the Numpy array
+___Numpy array == Matlab matrix:___ These are in contrast to the Numpy array
 and Matlab matrix, which are both thin wrappers around a contiguous chunk of
 memory, and which provide blazing-fast performance (because behind the scenes
 in both Numpy and Matlab, it's C, C++ and FORTRAN all the way down).
@@ -148,7 +149,8 @@ will be loading our data from text or binary files directly into a Numpy
 array, thus completely bypassing the use of Python lists and the costly
 list-to-array conversion.  I'm emphasising this to help you understand the
 difference between Python lists and Numpy arrays. Apologies if you've already
-got it, forgiveness please.
+got it, [forgiveness
+please](https://www.youtube.com/watch?v=ZeHflFNR4kQ&feature=youtu.be&t=128).
 
 
 <a class="anchor" id="numpy-basics"></a>
@@ -236,7 +238,7 @@ print(data)
 > for more information.
 
 
-  Of course you can also save data out to a text file just as easily, with
+Of course you can also save data out to a text file just as easily, with
 [`numpy.savetxt`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html):
 
 
@@ -250,6 +252,13 @@ with open('mydata.txt', 'rt') as f:
 ```
 
 
+> The `fmt` argument to the `numpy.savetxt` function uses a specification
+> language similar to that used in the C `printf` function - in the example
+> above, `'%i`' indicates that the values of the array should be output as
+> signed integers. See the [`numpy.savetxt`
+> documentation](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html)
+> for more details on specifying the output format.
+
 
 <a class="anchor" id="array-properties"></a>
 ### Array properties
@@ -275,7 +284,9 @@ print('Length of first dimension: ', len(z))
 
 > As depicted above, passing a Numpy array to the built-in `len` function will
 > only give you the length of the first dimension, so you will typically want
-> to avoid using it - use the `size` attribute instead.
+> to avoid using it - instead, use the `size` attribute if you want to know
+> how many elements are in an array, or the `shape` attribute if you want to
+> know the array shape.
 
 
 <a class="anchor" id="descriptive-statistics"></a>
@@ -301,6 +312,27 @@ print('prod:         ', a.prod())
 ```
 
 
+These methods can also be applied to arrays with multiple dimensions:
+
+
+```
+a = np.random.randint(1, 10, (3, 3))
+print('a:')
+print(a)
+print('min:             ', a.min())
+print('row mins:        ', a.min(axis=1))
+print('col mins:        ', a.min(axis=0))
+print('Min index      : ', a.argmin())
+print('Row min indices: ', a.argmin(axis=1))
+```
+
+
+Note that, for a multi-dimensional array, the `argmin` and `argmax` methods
+will return the (0-based) index of the minimum/maximum values into a
+[flattened](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.ndarray.flatten.html)
+view of the array.
+
+
 > <sup>2</sup> Python, being an object-oriented language, distinguishes
 > between _functions_ and _methods_. Hopefully we all know what a function is
 > - a _method_ is simply the term used to refer to a function that is
@@ -502,8 +534,8 @@ Wait ... what's that you say? Oh, I couldn't understand because of all the
 froth coming out of your mouth. I guess you're angry that `a * b` didn't give
 you the matrix product, like it would have in Matlab.  Well all I can say is
 that Numpy is not Matlab. Matlab operations are typically consistent with
-linear algebra notation. This is not the case in Numpy. Get over it. Take a
-calmative.
+linear algebra notation. This is not the case in Numpy. Get over it.
+[Get yourself a calmative](https://youtu.be/M_w_n-8w3IQ?t=32).
 
 
 <a class="anchor" id="matrix-multiplication"></a>
@@ -541,6 +573,40 @@ print(b.dot(a))
 > backwards-compatibility, go ahead and use it!
 
 
+One potential source of confusion for those of you who are used to Matlab's
+linear algebra-based take on things is that Numpy treats row and column
+vectors differently - you should take a break now and skim over the [appendix
+on vectors in Numpy](#appendix-vectors-in-numpy).
+
+
+For matrix-by-vector multiplications, a 1-dimensional Numpy array may be
+treated as _either_ a row vector _or_ a column vector, depending on where
+it is in the expression:
+
+
+```
+a = np.arange(1, 5).reshape((2, 2))
+b = np.random.randint(1, 10, 2)
+
+print('a:')
+print(a)
+print('b:', b)
+
+print('a @ b - b is a column vector:')
+print(a @ b)
+print('b @ a - b is a row vector:')
+print(b @ a)
+```
+
+
+If you really can't stand using `@` to denote matrix multiplication, and just
+want things to be like they were back in Matlab-land, you do have the option
+of using a different Numpy data type - the `matrix` - which behaves a bit more
+like what you might expect from Matlab.  You can find a brief overview of the
+`matrix` data type in [the appendix](appendix-the-numpy-matrix).
+
+
+
 <a class="anchor" id="broadcasting"></a>
 ### Broadcasting
 
@@ -576,8 +642,7 @@ print(a * b.reshape(-1, 1))
 
 > Here we used a handy feature of the `reshape` method - if you pass `-1` for
 > the size of one dimension, it will automatically determine the size to use
-> for that dimension. Take a look at [the
-> appendix](#appendix-vectors-in-numpy) for a discussion on vectors in Numpy.
+> for that dimension.
 
 
 Here is a more useful example, where we use broadcasting to de-mean the rows
@@ -834,7 +899,7 @@ for r, c, v in zip(rows, cols, indexed):
 
 
 The `numpy.where` function can be combined with boolean arrays to easily
-generate of coordinate arrays for values which meet some condition:
+generate coordinate arrays for values which meet some condition:
 
 
 ```
@@ -1052,6 +1117,47 @@ print(np.atleast_2d(r).T)
 ```
 
 
+<a class="anchor" id="appendix-the-numpy-matrix"></a>
+## Appendix D: The Numpy `matrix`
+
+
+By now you should be aware that a Numpy `array` does not behave in quite the
+same way as a Matlab matrix. The primary difference between Numpy and Matlab
+is that in Numpy, the `*` operator denotes element-wise multiplication,
+gwhereas in Matlab, `*` denotes matrix multiplication.
+
+
+Numpy does support the `@` operator for matrix multiplication, but if this is
+a complete show-stopper for you - if you just can't bring yourself to write `A
+@ B` to denote the matrix product of `A` and `B` - if you _must_ have your
+code looking as Matlab-like as possible, then you should look into the Numpy
+[`matrix`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.matrix.html)
+data type.
+
+
+The `matrix` is an alternative to the `array` which essentially behaves more
+like a Matlab matrix:
+
+* `matrix` objects always have exactly two dimensions.
+* `a * b` denotes matrix multiplication, rather than elementwise
+  multiplication.
+* `matrix` objects have `.H` and `.I` attributes, which are convenient ways to
+  access the conjugate transpose and inverse of the matrix respectively.
+
+
+Note however that use of the `matrix` type is _not_ widespread, and if you use
+it you will risk confusing others who are familiar with the much more commonly
+used `array`, and who need to work with your code. In fact, the official Numpy
+documentation [recommends against using the `matrix`
+type](https://docs.scipy.org/doc/numpy-dev/user/numpy-for-matlab-users.html#array-or-matrix-which-should-i-use).
+
+
+But if you are writing some very maths-heavy code, and you want your code to
+be as clear and concise, and maths/Matlab-like as possible, then the `matrix`
+type is there for you. Just make sure you document your code well to make it
+clear to others what is going on!
+
+
 <a class="anchor" id="useful-references"></a>
 ## Useful references
 
@@ -1061,4 +1167,5 @@ print(np.atleast_2d(r).T)
 * [Broadcasting in Numpy](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html)
 * [Indexing in Numpy](https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html)
 * [Random sampling in `numpy.random`](https://docs.scipy.org/doc/numpy/reference/routines.random.html)
-* [Python slicing](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/)
\ No newline at end of file
+* [Python slicing](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/)
+* [Numpy for Matlab users](https://docs.scipy.org/doc/numpy-dev/user/numpy-for-matlab-users.html)
diff --git a/getting_started/nifti.ipynb b/getting_started/05_nifti.ipynb
similarity index 78%
rename from getting_started/nifti.ipynb
rename to getting_started/05_nifti.ipynb
index 994edd80c08830bcf5e2d0ab2bb116f1ac5f4029..f157ec0113f48fb0a2d900017b926ed1db17116d 100644
--- a/getting_started/nifti.ipynb
+++ b/getting_started/05_nifti.ipynb
@@ -15,17 +15,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "(182, 218, 182)\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "import numpy as np\n",
     "import nibabel as nib\n",
@@ -65,17 +57,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "(1.0, 1.0, 1.0)\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "voxsize = imhdr.get_zooms()\n",
     "print(voxsize)"
@@ -92,21 +76,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "4\n",
-      "[[  -1.    0.    0.   90.]\n",
-      " [   0.    1.    0. -126.]\n",
-      " [   0.    0.    1.  -72.]\n",
-      " [   0.    0.    0.    1.]]\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "sform = imhdr.get_sform()\n",
     "sformcode = imhdr['sform_code']\n",
@@ -127,7 +99,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -147,25 +119,7 @@
    ]
   }
  ],
- "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.5.2"
-  }
- },
+ "metadata": {},
  "nbformat": 4,
  "nbformat_minor": 2
 }
diff --git a/getting_started/nifti.md b/getting_started/05_nifti.md
similarity index 100%
rename from getting_started/nifti.md
rename to getting_started/05_nifti.md
diff --git a/getting_started/scripts.ipynb b/getting_started/08_scripts.ipynb
similarity index 63%
rename from getting_started/scripts.ipynb
rename to getting_started/08_scripts.ipynb
index 7b87ec6388573c9cebb864bdbea98da654593e9c..1d3d705d5b636c281ce97e6a527431c69075847d 100644
--- a/getting_started/scripts.ipynb
+++ b/getting_started/08_scripts.ipynb
@@ -18,9 +18,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "#!/usr/bin/env python"
@@ -38,9 +36,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "#!/usr/bin/env fslpython"
@@ -60,9 +56,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "import subprocess as sp\n",
@@ -79,9 +73,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "spobj = sp.run(['ls'], stdout = sp.PIPE)"
@@ -97,9 +89,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "spobj = sp.run('ls -la'.split(), stdout = sp.PIPE)\n",
@@ -119,9 +109,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "import os\n",
@@ -143,9 +131,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "commands = \"\"\"\n",
@@ -176,9 +162,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "import sys\n",
@@ -200,19 +184,10 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Usage: bash <input image> <output image>\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
-    "%%bash\n",
     "#!/bin/bash\n",
     "if [ $# -lt 2 ] ; then\n",
     "  echo \"Usage: $0 <input image> <output image>\"\n",
@@ -238,21 +213,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "ename": "IndexError",
-     "evalue": "list index out of range",
-     "traceback": [
-      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
-      "\u001b[0;31mIndexError\u001b[0m                                Traceback (most recent call last)",
-      "\u001b[0;32m<ipython-input-2-f7378930c369>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[1;32m     13\u001b[0m \u001b[0mspobj\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mfsldir\u001b[0m\u001b[0;34m+\u001b[0m\u001b[0;34m'/bin/fslstats'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0moutfile\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'-V'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstdout\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPIPE\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     14\u001b[0m \u001b[0msout\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mspobj\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstdout\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdecode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'utf-8'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 15\u001b[0;31m \u001b[0mvol_vox\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfloat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msout\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\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     16\u001b[0m \u001b[0mvol_mm\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfloat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msout\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     17\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Volumes are: '\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvol_vox\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m' in voxels and '\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvol_mm\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m' in mm'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
-      "\u001b[0;31mIndexError\u001b[0m: list index out of range"
-     ],
-     "output_type": "error"
-    }
-   ],
+   "outputs": [],
    "source": [
     "#!/usr/bin/env fslpython\n",
     "import os, sys\n",
@@ -272,55 +235,9 @@
     "vol_mm = float(sout.split()[1])\n",
     "print('Volumes are: ', vol_vox, ' in voxels and ', vol_mm, ' in mm')"
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
-   "outputs": [],
-   "source": []
   }
  ],
- "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.6.2"
-  },
-  "toc": {
-   "colors": {
-    "hover_highlight": "#DAA520",
-    "running_highlight": "#FF0000",
-    "selected_highlight": "#FFD700"
-   },
-   "moveMenuLeft": true,
-   "nav_menu": {
-    "height": "105px",
-    "width": "252px"
-   },
-   "navigate_menu": true,
-   "number_sections": true,
-   "sideBar": true,
-   "threshold": 4.0,
-   "toc_cell": false,
-   "toc_section_display": "block",
-   "toc_window_display": false
-  }
- },
+ "metadata": {},
  "nbformat": 4,
  "nbformat_minor": 2
 }
diff --git a/getting_started/scripts.md b/getting_started/08_scripts.md
similarity index 100%
rename from getting_started/scripts.md
rename to getting_started/08_scripts.md