diff --git a/.ycm_extra_conf.py b/.ycm_extra_conf.py
index 27093e79cf20ceebace8b14edc3b38c091991508..8fb825ba3b1abe8630ae830f74807e7897bfda8e 100644
--- a/.ycm_extra_conf.py
+++ b/.ycm_extra_conf.py
@@ -66,6 +66,8 @@ flags = [
     '/usr/include/python3.6m', # TODO: run a command to find this on "any" system
     '-I',
     'sup/include',
+    '-I',
+    '/usr/include/python3.7m',
 ]
 
 # Set this to the absolute path to the folder (NOT the file!) containing the
diff --git a/arbor/arbexcept.cpp b/arbor/arbexcept.cpp
index 45a8fb2709956aca661ada96908bb1dc58dd0dfc..0a3a382c7fd0cb2e36c3c54d798c3a4659347aa7 100644
--- a/arbor/arbexcept.cpp
+++ b/arbor/arbexcept.cpp
@@ -11,7 +11,7 @@ namespace arb {
 using arb::util::pprintf;
 
 bad_cell_description::bad_cell_description(cell_kind kind, cell_gid_type gid):
-    arbor_exception(pprintf("bad description for cell kind {} on gid {}", kind, gid)),
+    arbor_exception(pprintf("recipe::get_cell_kind(gid={}) -> {} does not match the cell type provided by recipe::get_cell_description(gid={})", gid, kind, gid)),
     gid(gid),
     kind(kind)
 {}
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index da987714b528c7a7883d3f44219274aef9399c98..e03878ecc0e83e2e99d79faa9f820c963c4cbdca 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -25,8 +25,9 @@ add_library(pyarb MODULE
     mpi.cpp
     pyarb.cpp
     recipe.cpp
-    simulation.cpp
     schedule.cpp
+    simulation.cpp
+    spikes.cpp
 )
 
 target_link_libraries(pyarb PRIVATE arbor pybind11::module)
diff --git a/python/cells.cpp b/python/cells.cpp
index 190b45b0341676da2e1a746962eec67499536a7f..d8b4341847849a818e5da4c153378f08bc56a05f 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -24,6 +24,7 @@ arb::util::unique_any convert_cell(pybind11::object o) {
     using pybind11::isinstance;
     using pybind11::cast;
 
+    pybind11::gil_scoped_acquire guard;
     if (isinstance<arb::spike_source_cell>(o)) {
         return arb::util::unique_any(cast<arb::spike_source_cell>(o));
     }
@@ -132,7 +133,7 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
     // Add a synapse to the mid point of the first dendrite.
     cell.add_synapse({1, 0.5}, "expsyn");
 
-    // Add additional synapses that will not be connected to anything.
+    // Add additional synapses.
     for (unsigned i=1u; i<params.synapses; ++i) {
         cell.add_synapse({1, 0.5}, "expsyn");
     }
diff --git a/python/example/ring.py b/python/example/ring.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef80aa63db36f5b61716239ad1a7011849956e45
--- /dev/null
+++ b/python/example/ring.py
@@ -0,0 +1,65 @@
+import sys
+import arbor
+
+class ring_recipe (arbor.recipe):
+
+    def __init__(self, n=4):
+        # The base C++ class constructor must be called first, to ensure that
+        # all memory in the C++ class is initialized correctly.
+        arbor.recipe.__init__(self)
+        self.ncells = n
+        self.params = arbor.cell_parameters()
+
+    # The num_cells method that returns the total number of cells in the model
+    # must be implemented.
+    def num_cells(self):
+        return self.ncells
+
+    # The cell_description method returns a cell
+    def cell_description(self, gid):
+        return arbor.branch_cell(gid, self.params)
+
+    def num_targets(self, gid):
+        return 1
+
+    def num_sources(self, gid):
+        return 1
+
+    # The kind method returns the type of cell with gid.
+    # Note: this must agree with the type returned by cell_description.
+    def cell_kind(self, gid):
+        return arbor.cell_kind.cable
+
+    # Make a ring network
+    def connections_on(self, gid):
+        src = (gid-1)%self.ncells
+        w = 0.01
+        d = 10
+        return [arbor.connection(arbor.cell_member(src,0), arbor.cell_member(gid,0), w, d)]
+
+    # Attach a generator to the first cell in the ring.
+    def event_generators(self, gid):
+        if gid==0:
+            sched = arbor.explicit_schedule([1])
+            return [arbor.event_generator(arbor.cell_member(0,0), 0.1, sched)]
+        return []
+
+
+context = arbor.context(threads=4, gpu_id=None)
+print(context)
+
+recipe = ring_recipe(100)
+print(recipe)
+
+decomp = arbor.partition_load_balance(recipe, context)
+print(decomp)
+
+sim = arbor.simulation(recipe, decomp, context)
+print(sim)
+
+recorder = arbor.attach_spike_recorder(sim)
+
+sim.run(1000)
+
+for s in recorder.spikes:
+    print(s)
diff --git a/python/identifiers.cpp b/python/identifiers.cpp
index 11895074d3bf4ec92017ef460371043fd32db581..43b6b9c4cbbf081575e1da29a9fc5ba085d7a887 100644
--- a/python/identifiers.cpp
+++ b/python/identifiers.cpp
@@ -48,7 +48,7 @@ void register_identifiers(pybind11::module& m) {
             "Proxy cell that generates spikes from a spike sequence provided by the user.");
 
     pybind11::enum_<arb::backend_kind>(m, "backend",
-        "Enumeration used to indicate which hardware backend to use for running a cell_group.")
+        "Enumeration used to indicate which hardware backend to execute a cell group on.")
         .value("gpu", arb::backend_kind::gpu,
             "Use GPU backend.")
         .value("multicore", arb::backend_kind::multicore,
diff --git a/python/pyarb.cpp b/python/pyarb.cpp
index 143317d2d1a7efa191bfa50c89042d5a8a732347..c369c2ab2cc85345463a6ec98dd7fc42de9af997 100644
--- a/python/pyarb.cpp
+++ b/python/pyarb.cpp
@@ -15,6 +15,7 @@ void register_identifiers(pybind11::module& m);
 void register_recipe(pybind11::module& m);
 void register_schedules(pybind11::module& m);
 void register_simulation(pybind11::module& m);
+void register_spike_handling(pybind11::module& m);
 
 #ifdef ARB_MPI_ENABLED
 void register_mpi(pybind11::module& m);
@@ -37,4 +38,5 @@ PYBIND11_MODULE(arbor, m) {
     pyarb::register_recipe(m);
     pyarb::register_schedules(m);
     pyarb::register_simulation(m);
+    pyarb::register_spike_handling(m);
 }
diff --git a/python/recipe.cpp b/python/recipe.cpp
index fc1f45697d889cad57b37e2ef1d18635ae08f251..49b03ec30391e164b6d61bfe8456ba458bcaff1a 100644
--- a/python/recipe.cpp
+++ b/python/recipe.cpp
@@ -22,43 +22,17 @@ namespace pyarb {
 // The py::recipe::cell_decription returns a pybind11::object, that is
 // unwrapped and copied into a arb::util::unique_any.
 arb::util::unique_any py_recipe_shim::get_cell_description(arb::cell_gid_type gid) const {
-    // Aquire the GIL because it must be held when calling isinstance and cast.
-    auto guard = pybind11::gil_scoped_acquire();
-
-    // Get the python object pyarb::cell_description from the python front end
+    pybind11::gil_scoped_acquire guard;
     return convert_cell(impl_->cell_description(gid));
 }
 
-// The py::recipe::global_properties returns a pybind11::object, that is
-// unwrapped and copied into a arb::util::any.
-arb::util::any py_recipe_shim::get_global_properties(arb::cell_kind kind) const {
-    using pybind11::cast;
-
-    // Aquire the GIL because it must be held when calling cast.
-    auto guard = pybind11::gil_scoped_acquire();
-
-    // Get the python object pyarb::global_properties from the python front end
-    pybind11::object o = impl_->global_properties(kind);
-
-    if (kind == arb::cell_kind::cable) {
-        return arb::util::any(cast<arb::cable_cell_global_properties>(o));
-    }
-
-    else return arb::util::any{};
-
-    throw pyarb_error( "recipe.global_properties returned \""
-                       + std::string(pybind11::str(o))
-                       + "\" which does not describe a known Arbor global property description");
-
-}
-
 std::vector<arb::event_generator> py_recipe_shim::event_generators(arb::cell_gid_type gid) const {
     using namespace std::string_literals;
     using pybind11::isinstance;
     using pybind11::cast;
 
     // Aquire the GIL because it must be held when calling isinstance and cast.
-    auto guard = pybind11::gil_scoped_acquire();
+    pybind11::gil_scoped_acquire guard;
 
     // Get the python list of pyarb::event_generator_shim from the python front end.
     auto pygens = impl_->event_generators(gid);
@@ -69,10 +43,9 @@ std::vector<arb::event_generator> py_recipe_shim::event_generators(arb::cell_gid
     for (auto& g: pygens) {
         // check that a valid Python event_generator was passed.
         if (!isinstance<pyarb::event_generator_shim>(g)) {
-            std::stringstream s;
-            s << "recipe supplied an invalid event generator for gid "
-            << gid << ": " << pybind11::str(g);
-            throw pyarb_error(s.str());
+            throw pyarb_error(
+                util::pprintf(
+                    "recipe supplied an invalid event generator for gid {}: {}", gid, pybind11::str(g)));
         }
         // get a reference to the python event_generator
         auto& p = cast<const pyarb::event_generator_shim&>(g);
@@ -128,24 +101,24 @@ void register_recipe(pybind11::module& m) {
     using namespace pybind11::literals;
 
     // Connections
-    pybind11::class_<cell_connection_shim> cell_connection(m, "connection",
+    pybind11::class_<arb::cell_connection> cell_connection(m, "connection",
         "Describes a connection between two cells:\n"
         "  Defined by source and destination end points (that is pre-synaptic and post-synaptic respectively), a connection weight and a delay time.");
     cell_connection
         .def(pybind11::init<arb::cell_member_type, arb::cell_member_type, float, arb::time_type>(),
-            "source"_a = arb::cell_member_type{0,0}, "dest"_a = arb::cell_member_type{0,0}, "weight"_a = 0.f, "delay"_a,
+            "source"_a, "dest"_a, "weight"_a, "delay"_a,
             "Construct a connection with arguments:\n"
-            "  source:      The source end point of the connection (default (0,0)).\n"
-            "  dest:        The destination end point of the connection (default (0,0)).\n"
-            "  weight:      The weight delivered to the target synapse (dimensionless with interpretation specific to synapse type of target, default 0.).\n"
+            "  source:      The source end point of the connection.\n"
+            "  dest:        The destination end point of the connection.\n"
+            "  weight:      The weight delivered to the target synapse (unit: defined by the type of synapse target).\n"
             "  delay:       The delay of the connection (unit: ms).")
-        .def_readwrite("source", &cell_connection_shim::source,
+        .def_readwrite("source", &arb::cell_connection::source,
             "The source of the connection.")
-        .def_readwrite("dest", &cell_connection_shim::destination,
+        .def_readwrite("dest", &arb::cell_connection::dest,
             "The destination of the connection.")
-        .def_readwrite("weight", &cell_connection_shim::weight,
+        .def_readwrite("weight", &arb::cell_connection::weight,
             "The weight of the connection.")
-        .def_property("delay", &cell_connection_shim::get_delay, &cell_connection_shim::set_delay,
+        .def_readwrite("delay", &arb::cell_connection::delay,
             "The delay time of the connection (unit: ms).")
         .def("__str__",  &con_to_string)
         .def("__repr__", &con_to_string);
@@ -155,11 +128,11 @@ void register_recipe(pybind11::module& m) {
         "Describes a gap junction between two gap junction sites.");
     gap_junction_connection
         .def(pybind11::init<arb::cell_member_type, arb::cell_member_type, double>(),
-            "local"_a = arb::cell_member_type{0,0}, "peer"_a = arb::cell_member_type{0,0}, "ggap"_a = 0.f,
+            "local"_a, "peer"_a, "ggap"_a,
             "Construct a gap junction connection with arguments:\n"
-            "  local: One half of the gap junction connection (default (0,0)).\n"
-            "  peer:  Other half of the gap junction connection (default (0,0)).\n"
-            "  ggap:  Gap junction conductance (unit: μS, default 0.).")
+            "  local: One half of the gap junction connection.\n"
+            "  peer:  Other half of the gap junction connection.\n"
+            "  ggap:  Gap junction conductance (unit: μS).")
         .def_readwrite("local", &arb::gap_junction_connection::local,
             "One half of the gap junction connection.")
         .def_readwrite("peer", &arb::gap_junction_connection::peer,
@@ -204,10 +177,8 @@ void register_recipe(pybind11::module& m) {
             "gid"_a,
             "A list of the gap junctions connected to gid (default []).")
         // TODO: py_recipe::get_probe
-        .def("global_properties", &py_recipe::global_properties, pybind11::return_value_policy::copy,
-            "cell_kind"_a,
-            "Global property type specific to a given cell kind.")
-        .def("__str__", [](const py_recipe&){return "<arbor.recipe>";})
+        // TODO: py_recipe::global_properties
+        .def("__str__",  [](const py_recipe&){return "<arbor.recipe>";})
         .def("__repr__", [](const py_recipe&){return "<arbor.recipe>";});
 }
 } // namespace pyarb
diff --git a/python/recipe.hpp b/python/recipe.hpp
index 6057fb6d0227650c2f3f58097dff40bbd64c9ea2..f8ccaef3448481c78ff38efc453491ccdb21506b 100644
--- a/python/recipe.hpp
+++ b/python/recipe.hpp
@@ -26,30 +26,31 @@ public:
     virtual ~py_recipe() {}
 
     virtual arb::cell_size_type num_cells() const = 0;
-
     virtual pybind11::object cell_description(arb::cell_gid_type gid) const = 0;
     virtual arb::cell_kind cell_kind(arb::cell_gid_type gid) const = 0;
 
-    virtual arb::cell_size_type num_sources(arb::cell_gid_type) const { return 0; }
-    virtual arb::cell_size_type num_targets(arb::cell_gid_type) const { return 0; }
-
-    //TODO: virtual arb::cell_size_type num_probes(arb::cell_gid_type) const { return 0; }
-
+    virtual arb::cell_size_type num_sources(arb::cell_gid_type) const {
+        return 0;
+    }
+    virtual arb::cell_size_type num_targets(arb::cell_gid_type) const {
+        return 0;
+    }
     virtual arb::cell_size_type num_gap_junction_sites(arb::cell_gid_type gid) const {
         return gap_junctions_on(gid).size();
     }
-
     virtual std::vector<pybind11::object> event_generators(arb::cell_gid_type gid) const {
-        auto guard = pybind11::gil_scoped_acquire();
+        return {};
+    }
+    virtual std::vector<arb::cell_connection> connections_on(arb::cell_gid_type gid) const {
+        return {};
+    }
+    virtual std::vector<arb::gap_junction_connection> gap_junctions_on(arb::cell_gid_type) const {
         return {};
     }
 
-    virtual std::vector<arb::cell_connection> connections_on(arb::cell_gid_type gid) const { return {}; }
-    virtual std::vector<arb::gap_junction_connection> gap_junctions_on(arb::cell_gid_type) const { return {}; }
-
+    //TODO: virtual arb::cell_size_type num_probes(arb::cell_gid_type) const { return 0; }
     //TODO: virtual pybind11::object get_probe (arb::cell_member_type id) const {...}
-
-    virtual pybind11::object global_properties(arb::cell_kind kind) const = 0;
+    //TODO: virtual pybind11::object global_properties(arb::cell_kind kind) const {return pybind11::none();};
 };
 
 class py_recipe_trampoline: public py_recipe {
@@ -74,8 +75,6 @@ public:
         PYBIND11_OVERLOAD(arb::cell_size_type, py_recipe, num_targets, gid);
     }
 
-    //TODO: arb::cell_size_type num_probes(arb::cell_gid_type)
-
     arb::cell_size_type num_gap_junction_sites(arb::cell_gid_type gid) const override {
         PYBIND11_OVERLOAD(arb::cell_size_type, py_recipe, num_gap_junction_sites, gid);
     }
@@ -92,11 +91,8 @@ public:
         PYBIND11_OVERLOAD(std::vector<arb::gap_junction_connection>, py_recipe, gap_junctions_on, gid);
     }
 
+    //TODO: arb::cell_size_type num_probes(arb::cell_gid_type)
     //TODO: pybind11::object get_probe(arb::cell_member_type id)
-
-    pybind11::object global_properties(arb::cell_kind kind) const override {
-        PYBIND11_OVERLOAD_PURE(pybind11::object, py_recipe, global_properties, kind);
-    }
 };
 
 // A recipe shim that holds a pyarb::recipe implementation.
@@ -134,10 +130,8 @@ public:
         return impl_->num_targets(gid);
     }
 
-/* //TODO: arb::cell_size_type num_probes(arb::cell_gid_type gid) const override {
-        return impl_->num_probes(gid);
-    }
-*/
+    //TODO: arb::cell_size_type num_probes(arb::cell_gid_type gid)
+
     arb::cell_size_type num_gap_junction_sites(arb::cell_gid_type gid) const override {
         return impl_->num_gap_junction_sites(gid);
     }
@@ -152,12 +146,12 @@ public:
         return impl_->gap_junctions_on(gid);
     }
 
-    //TODO: arb::probe_info get_probe(arb::cell_member_type id) const override
-
-    // The pyarb::recipe::global_properties returns a pybind11::object, that is
-    // unwrapped and copied into a util::any.
-    arb::util::any get_global_properties(arb::cell_kind kind) const override;
+    //TODO: arb::probe_info get_probe(arb::cell_member_type id)
 
+    // TODO: wrap
+    arb::util::any get_global_properties(arb::cell_kind kind) const override {
+        return arb::util::any{};
+    }
 };
 
 } // namespace pyarb
diff --git a/python/simulation.cpp b/python/simulation.cpp
index 07af633b540ff1f6a7623f09380a4b2e99c59cea..6ed19f8c4d0a08ecd92855e98e6bc039ec846646 100644
--- a/python/simulation.cpp
+++ b/python/simulation.cpp
@@ -15,14 +15,13 @@ void register_simulation(pybind11::module& m) {
         "The executable form of a model.\n"
         "A simulation is constructed from a recipe, and then used to update and monitor model state.");
     simulation
-        // A custom constructor that wraps a python recipe with
-        // arb::py_recipe_shim before forwarding it to the arb::recipe constructor.
+        // A custom constructor that wraps a python recipe with arb::py_recipe_shim
+        // before forwarding it to the arb::recipe constructor.
         .def(pybind11::init(
             [](std::shared_ptr<py_recipe>& rec, const arb::domain_decomposition& decomp, const context_shim& ctx) {
                 return new arb::simulation(py_recipe_shim(rec), decomp, ctx.context);
             }),
-            // Release the python gil, so that callbacks into the python
-            // recipe don't deadlock.
+            // Release the python gil, so that callbacks into the python recipe don't deadlock.
             pybind11::call_guard<pybind11::gil_scoped_release>(),
             "Initialize the model described by a recipe, with cells and network distributed\n"
             "according to the domain decomposition and computational resources described by a context.",
@@ -32,10 +31,10 @@ void register_simulation(pybind11::module& m) {
             "Reset the state of the simulation to its initial state.")
         .def("run", &arb::simulation::run,
             pybind11::call_guard<pybind11::gil_scoped_release>(),
-            "Run the simulation from current simulation time to tfinal, with maximum time step size dt.",
-            "tfinal"_a, "dt"_a)
+            "Run the simulation from current simulation time to tfinal (unit: ms), with maximum time step size dt (unit: ms).",
+            "tfinal"_a, "dt"_a=0.025)
         .def("set_binning_policy", &arb::simulation::set_binning_policy,
-            "Set event binning policy on all our groups.",
+            "Set the binning policy for event delivery, and the binning time interval if applicable (unit: ms).",
             "policy"_a, "bin_interval"_a)
         .def("__str__",  [](const arb::simulation&){ return "<arbor.simulation>"; })
         .def("__repr__", [](const arb::simulation&){ return "<arbor.simulation>"; });
diff --git a/python/spikes.cpp b/python/spikes.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e362bb507d15a4db0dcf4f1db434450fcefa6be1
--- /dev/null
+++ b/python/spikes.cpp
@@ -0,0 +1,92 @@
+#include <memory>
+#include <vector>
+
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
+#include <arbor/spike.hpp>
+#include <arbor/simulation.hpp>
+
+#include "strprintf.hpp"
+
+namespace pyarb {
+
+// A functor that models arb::spike_export_function.
+// Holds a shared pointer to the spike_vec used to store the spikes, so that if
+// the spike_vec in spike_recorder is garbage collected in Python, stores will
+// not seg fault.
+struct spike_callback {
+    using spike_vec = std::vector<arb::spike>;
+
+    std::shared_ptr<spike_vec> spike_store;
+
+    spike_callback(const std::shared_ptr<spike_vec>& state):
+        spike_store(state)
+    {}
+
+    void operator() (const spike_vec& spikes) {
+        spike_store->insert(spike_store->end(), spikes.begin(), spikes.end());
+    };
+};
+
+// Helper type for recording spikes from a simulation.
+// This type is wrapped in Python, to expose spike_recorder::spike_store.
+struct spike_recorder {
+    using spike_vec = std::vector<arb::spike>;
+    std::shared_ptr<spike_vec> spike_store;
+
+    spike_callback callback() {
+        // initialize the spike_store
+        spike_store = std::make_shared<spike_vec>();
+
+        // The callback holds a copy of spike_store, i.e. the shared
+        // pointer is held by both the spike_recorder and the callback, so if
+        // the spike_recorder is destructed in the calling Python code, attempts
+        // to write to spike_store inside the callback will not seg fault.
+        return spike_callback(spike_store);
+    }
+
+    const spike_vec& spikes() const {
+        return *spike_store;
+    }
+};
+
+std::shared_ptr<spike_recorder> attach_spike_recorder(arb::simulation& sim) {
+    auto r = std::make_shared<spike_recorder>();
+    sim.set_global_spike_callback(r->callback());
+    return r;
+}
+
+std::string spike_str(const arb::spike& s) {
+    return util::pprintf(
+            "<arbor.spike: source ({},{}), time {}>",
+            s.source.gid, s.source.index, s.time);
+}
+
+void register_spike_handling(pybind11::module& m) {
+    using namespace pybind11::literals;
+
+    pybind11::class_<arb::spike> spike(m, "spike");
+    spike
+        .def(pybind11::init<>())
+        .def_readwrite("source", &arb::spike::source)
+        .def_readwrite("time", &arb::spike::time)
+        .def("__str__",  &spike_str)
+        .def("__repr__", &spike_str);
+
+    // Use shared_ptr for spike_recorder, so that all copies of a recorder will
+    // see the spikes from the simulation with which the recorder's callback has been
+    // registered.
+    pybind11::class_<spike_recorder, std::shared_ptr<spike_recorder>> sprec(m, "spike_recorder");
+    sprec
+        .def(pybind11::init<>())
+        .def_property_readonly("spikes", &spike_recorder::spikes);
+
+    m.def("attach_spike_recorder", &attach_spike_recorder,
+          "sim"_a,
+          "Attach a spike recorder to an arbor simulation.\n"
+          "The recorder that is returned will record all spikes generated after it has been\n"
+          "attached (spikes generated before attaching are not recorded).");
+}
+
+} // namespace pyarb