diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index 4b37a93911f13ee40254bd5816c6743a257bf0d4..8065522a88ac7078518f1527c35289854626009d 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -22,6 +22,7 @@ add_library(pyarb MODULE
     identifiers.cpp
     mpi.cpp
     pyarb.cpp
+    recipe.cpp
     strings.cpp
 )
 
diff --git a/python/context.cpp b/python/context.cpp
index 4e7dde4f49c3e3b9d2f107176f799946039fb958..04cb3ab0404434d0c1a11d1de7d7b8bdd703b5cc 100644
--- a/python/context.cpp
+++ b/python/context.cpp
@@ -38,11 +38,11 @@ struct proc_allocation_shim {
 
     // getter and setter (in order to assert when being set)
     void set_gpu_id(pybind11::object gpu) {
-        gpu_id = py2optional<int>(gpu, "gpu id must be None, or a non-negative integer.", is_nonneg_int);
+        gpu_id = py2optional<int>(gpu, "gpu_id must be None, or a non-negative integer", is_nonneg_int);
     };
 
     void set_num_threads(int threads) {
-        pyarb::assert_throw([](int n) { return n>0; }(threads), "threads must be a positive integer.");
+        pyarb::assert_throw([](int n) { return n>0; }(threads), "threads must be a positive integer");
         num_threads = threads;
     };
 
@@ -153,8 +153,7 @@ void register_contexts(pybind11::module& m) {
 #else
         .def(pybind11::init(
             [](int threads, pybind11::object gpu){
-                auto gpu_id = py2optional<int>(gpu,
-                        "gpu id must be None, or a non-negative integer.", is_nonneg_int);
+                auto gpu_id = py2optional<int>(gpu, "gpu_id must be None, or a non-negative integer", is_nonneg_int);
                 return context_shim(arb::make_context(arb::proc_allocation(threads, gpu_id.value_or(-1))));
             }),
              "threads"_a=1, "gpu_id"_a=pybind11::none(),
diff --git a/python/event_generator.cpp b/python/event_generator.cpp
index f31bbe15b0591f8b75e55e80291776a472d20680..9344a3fd1a9b8df90e2d2bf0eb3a5ec58c660181 100644
--- a/python/event_generator.cpp
+++ b/python/event_generator.cpp
@@ -3,7 +3,6 @@
 #include <string>
 
 #include <arbor/common_types.hpp>
-#include <arbor/event_generator.hpp>
 #include <arbor/schedule.hpp>
 
 #include <pybind11/pybind11.h>
@@ -12,6 +11,7 @@
 
 #include "conversion.hpp"
 #include "error.hpp"
+#include "event_generator.hpp"
 
 namespace pyarb {
 
@@ -41,13 +41,13 @@ struct regular_schedule_shim {
 
     // getter and setter (in order to assert when being set)
     void set_tstart(pybind11::object t) {
-        tstart = py2optional<time_type>(t, "tstart must a non-negative number, or None.", is_nonneg);
+        tstart = py2optional<time_type>(t, "tstart must a non-negative number, or None", is_nonneg);
     };
     void set_tstop(pybind11::object t) {
-        tstop = py2optional<time_type>(t, "tstop must a non-negative number, or None.", is_nonneg);
+        tstop = py2optional<time_type>(t, "tstop must a non-negative number, or None", is_nonneg);
     };
     void set_dt(time_type delta_t) {
-        pyarb::assert_throw(is_nonneg(delta_t), "dt must be a non-negative number.");
+        pyarb::assert_throw(is_nonneg(delta_t), "dt must be a non-negative number");
         dt = delta_t;
     };
 
@@ -90,7 +90,7 @@ struct explicit_schedule_shim {
         // Assert that there are no negative times
         if (times.size()) {
             pyarb::assert_throw(is_nonneg(times[0]),
-                    "explicit time schedule can not contain negative values.");
+                    "explicit time schedule can not contain negative values");
         }
     };
 
@@ -123,12 +123,12 @@ struct poisson_schedule_shim {
     }
 
     void set_tstart(time_type t) {
-        pyarb::assert_throw(is_nonneg(t), "tstart must be a non-negative number.");
+        pyarb::assert_throw(is_nonneg(t), "tstart must be a non-negative number");
         tstart = t;
     };
 
     void set_freq(time_type f) {
-        pyarb::assert_throw(is_nonneg(f), "frequency must be a non-negative number.");
+        pyarb::assert_throw(is_nonneg(f), "frequency must be a non-negative number");
         freq = f;
     };
 
@@ -141,6 +141,15 @@ struct poisson_schedule_shim {
     }
 };
 
+template <typename Sched>
+event_generator_shim make_event_generator(
+        arb::cell_member_type target,
+        double weight,
+        const Sched& sched)
+{
+    return event_generator_shim(target, weight, sched.schedule());
+}
+
 // Helper template for printing C++ optional types in Python.
 // Prints either the value, or None if optional value is not set.
 template <typename T>
@@ -184,27 +193,6 @@ std::string schedule_poisson_string(const poisson_schedule_shim& p) {
     return s.str();
 };
 
-struct event_generator_shim {
-    arb::cell_member_type target;
-    double weight;
-    arb::schedule time_sched;
-
-    event_generator_shim(arb::cell_member_type cell, double event_weight, arb::schedule sched):
-        target(cell),
-        weight(event_weight),
-        time_sched(std::move(sched))
-    {}
-};
-
-template <typename Sched>
-event_generator_shim make_event_generator(
-        arb::cell_member_type target,
-        double weight,
-        const Sched& sched)
-{
-    return event_generator_shim(target, weight, sched.schedule());
-}
-
 void register_event_generators(pybind11::module& m) {
     using namespace pybind11::literals;
     using time_type = arb::time_type;
diff --git a/python/event_generator.hpp b/python/event_generator.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..ed41c10ee4a349817180c48288ec934ea166fc10
--- /dev/null
+++ b/python/event_generator.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <arbor/common_types.hpp>
+#include <arbor/schedule.hpp>
+
+#include <pybind11/pybind11.h>
+
+namespace pyarb {
+
+struct event_generator_shim {
+    arb::cell_member_type target;
+    double weight;
+    arb::schedule time_sched;
+
+    event_generator_shim(arb::cell_member_type cell, double event_weight, arb::schedule sched):
+        target(cell),
+        weight(event_weight),
+        time_sched(std::move(sched))
+    {}
+};
+
+} // namespace pyarb
diff --git a/python/identifiers.cpp b/python/identifiers.cpp
index cb6159a2c750fa15f8e1d91f19b733c6a99f3217..3aae5a61c8790d798a825c97e804e1139dc23f96 100644
--- a/python/identifiers.cpp
+++ b/python/identifiers.cpp
@@ -38,6 +38,17 @@ void register_identifiers(pybind11::module& m) {
             "Cell-local index of the item.")
         .def("__str__",  &cell_member_string)
         .def("__repr__", &cell_member_string);
+
+    pybind11::enum_<arb::cell_kind>(m, "cell_kind",
+        "Enumeration used to identify the cell kind, used by the model to group equal kinds in the same cell group.")
+        .value("benchmark", arb::cell_kind::benchmark,
+            "Proxy cell used for benchmarking.")
+        .value("cable", arb::cell_kind::cable,
+            "A cell with morphology described by branching 1D cable segments.")
+        .value("lif", arb::cell_kind::lif,
+            "Leaky-integrate and fire neuron.")
+        .value("spike_source", arb::cell_kind::spike_source,
+            "Proxy cell that generates spikes from a spike sequence provided by the user.");
 }
 
 } // namespace pyarb
diff --git a/python/mpi.cpp b/python/mpi.cpp
index 16a2b36260336b5060ebb0dbd43a5977ca9cc651..15f1b5df9d582ed756f155b3dc0f0d8aa06a2762 100644
--- a/python/mpi.cpp
+++ b/python/mpi.cpp
@@ -32,7 +32,7 @@ MPI_Comm convert_to_mpi_comm(pybind11::object o) {
         return *PyMPIComm_Get(o.ptr());
     }
 #endif
-    throw arb::mpi_error(MPI_ERR_OTHER, "Unable to convert to an MPI Communicatior.");
+    throw arb::mpi_error(MPI_ERR_OTHER, "Unable to convert to an MPI Communicatior");
 }
 
 mpi_comm_shim::mpi_comm_shim(pybind11::object o) {
diff --git a/python/pyarb.cpp b/python/pyarb.cpp
index e085141ea0157b0fd157a1d2033b1ac22daac8cc..c964caa4ea346627dff6e804dc974eefb0834f90 100644
--- a/python/pyarb.cpp
+++ b/python/pyarb.cpp
@@ -13,6 +13,7 @@ void register_identifiers(pybind11::module& m);
 #ifdef ARB_MPI_ENABLED
 void register_mpi(pybind11::module& m);
 #endif
+void register_recipe(pybind11::module& m);
 }
 
 PYBIND11_MODULE(arbor, m) {
@@ -26,4 +27,5 @@ PYBIND11_MODULE(arbor, m) {
     #ifdef ARB_MPI_ENABLED
     pyarb::register_mpi(m);
     #endif
+    pyarb::register_recipe(m);
 }
diff --git a/python/recipe.cpp b/python/recipe.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..93d1e2bfb00ccbf1b8ef7594e503b24d0943f6e9
--- /dev/null
+++ b/python/recipe.cpp
@@ -0,0 +1,232 @@
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <pybind11/pybind11.h>
+#include <pybind11/pytypes.h>
+#include <pybind11/stl.h>
+
+#include <arbor/benchmark_cell.hpp>
+#include <arbor/cable_cell.hpp>
+#include <arbor/event_generator.hpp>
+#include <arbor/lif_cell.hpp>
+#include <arbor/recipe.hpp>
+#include <arbor/spike_source_cell.hpp>
+
+#include "error.hpp"
+#include "event_generator.hpp"
+#include "recipe.hpp"
+
+namespace pyarb {
+
+// ========================================= Unwrap =========================================
+// 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 {
+    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();
+
+    // Get the python object pyarb::cell_description from the python front end
+    pybind11::object o = impl_->cell_description(gid);
+
+    if (isinstance<arb::cable_cell>(o)) {
+        return arb::util::unique_any(cast<arb::cable_cell>(o));
+    }
+
+    else if (isinstance<arb::lif_cell>(o)) {
+        return arb::util::unique_any(cast<arb::lif_cell>(o));
+    }
+
+    else if (isinstance<arb::spike_source_cell>(o)) {
+        return arb::util::unique_any(cast<arb::spike_source_cell>(o));
+    }
+
+    else if (isinstance<arb::benchmark_cell>(o)) {
+        return arb::util::unique_any(cast<arb::benchmark_cell>(o));
+    }
+
+    throw pyarb_error(
+                        "recipe.cell_description returned \""
+                        + std::string(pybind11::str(o))
+                        + "\" which does not describe a known Arbor cell type");
+}
+
+// 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::isinstance;
+    using pybind11::cast;
+
+    // Aquire the GIL because it must be held when calling isinstance and 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();
+
+    // Get the python list of pyarb::event_generator_shim from the python front end.
+    auto pygens = impl_->event_generators(gid);
+
+    std::vector<arb::event_generator> gens;
+    gens.reserve(pygens.size());
+
+    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());
+        }
+        // get a reference to the python event_generator
+        auto& p = cast<const pyarb::event_generator_shim&>(g);
+
+        // convert the event_generator to an arb::event_generator
+        gens.push_back(arb::schedule_generator({gid, p.target.index}, p.weight, std::move(p.time_sched)));
+    }
+
+    return gens;
+}
+
+// TODO: implement py_recipe_shim::probe_info
+
+std::string connection_string(const arb::cell_connection& c) {
+    std::stringstream s;
+    s << "<connection: (" << c.source.gid << "," << c.source.index << ")"
+      << " -> (" << c.dest.gid << "," << c.dest.index << ")"
+      << " , delay " << c.delay << ", weight " << c.weight << ">";
+    return s.str();
+}
+
+std::string gap_junction_connection_string(const arb::gap_junction_connection& gc) {
+    std::stringstream s;
+    s << "<connection: (" << gc.local.gid << "," << gc.local.index << ")"
+    << " -> (" << gc.peer.gid << "," << gc.peer.index << ")"
+    << " , conductance " << gc.ggap << ">";
+    return s.str();
+}
+
+void register_recipe(pybind11::module& m) {
+    using namespace pybind11::literals;
+
+    // Connections
+    pybind11::class_<arb::cell_connection> cell_connection(m, "cell_connection",
+        "Describes a connection between two cells:\n"
+        "a pre-synaptic source and a post-synaptic destination.");
+    cell_connection
+        .def(pybind11::init<>(
+            [](){return arb::cell_connection({0u,0u}, {0u,0u}, 0.f, 0.f);}),
+            "Construct a connection with default arguments:\n"
+            "  source:      gid 0, index 0.\n"
+            "  destination: gid 0, index 0.\n"
+            "  weight:      0.\n"
+            "  delay:       0 ms.\n")
+        .def(pybind11::init<arb::cell_member_type, arb::cell_member_type, float, float>(),
+            "source"_a, "destination"_a, "weight"_a = 0.f, "delay"_a = 0.f,
+            "Construct a connection with arguments:\n"
+            "  source:      The source end point of the connection.\n"
+            "  destination: The destination end point of the connection.\n"
+            "  weight:      The weight delivered to the target synapse (dimensionless with interpretation specific to synapse type of target, default 0.).\n"
+            "  delay:       The delay of the connection (unit: ms, default 0.).\n")
+        .def_readwrite("source", &arb::cell_connection::source,
+            "The source of the connection.")
+        .def_readwrite("destination", &arb::cell_connection::dest,
+            "The destination of the connection.")
+        .def_readwrite("weight", &arb::cell_connection::weight,
+            "The weight of the connection (unit: S⋅cm⁻²).")
+        .def_readwrite("delay", &arb::cell_connection::delay,
+            "The delay time of the connection (unit: ms).")
+        .def("__str__", &connection_string)
+        .def("__repr__", &connection_string);
+
+    // Gap Junction Connections
+    pybind11::class_<arb::gap_junction_connection> gap_junction_connection(m, "gap_junction_connection",
+        "Describes a gap junction between two gap junction sites.");
+    gap_junction_connection
+        .def(pybind11::init<>(
+            [](){return arb::gap_junction_connection({0u,0u}, {0u,0u}, 0.f);}),
+            "Construct a gap junction connection with default arguments:\n"
+            "  local: gid 0, index 0.\n"
+            "  peer:  gid 0, index 0.\n"
+            "  ggap:  0 μS.\n")
+        .def(pybind11::init<arb::cell_member_type, arb::cell_member_type, double>(),
+            "local"_a, "peer"_a, "ggap"_a = 0.f,
+            "Construct a gap junction connection with arguments:\n"
+            "  local: One half of the gap junction connection.\n"
+            "  peer:  Other half of the gap junction connection.\n"
+            "  ggap:  Gap junction conductance (unit: μS, default 0.).\n")
+        .def_readwrite("local", &arb::gap_junction_connection::local,
+            "One half of the gap junction connection.")
+        .def_readwrite("peer", &arb::gap_junction_connection::peer,
+            "Other half of the gap junction connection.")
+        .def_readwrite("ggap", &arb::gap_junction_connection::ggap,
+            "Gap junction conductance (unit: μS).")
+        .def("__str__", &gap_junction_connection_string)
+        .def("__repr__", &gap_junction_connection_string);
+
+    // Recipes
+    pybind11::class_<py_recipe,
+                     py_recipe_trampoline,
+                     std::shared_ptr<py_recipe>>
+        recipe(m, "recipe", pybind11::dynamic_attr(),
+        "A description of a model, describing the cells and the network via a cell-centric interface.");
+    recipe
+        .def(pybind11::init<>())
+        .def("num_cells", &py_recipe::num_cells, "The number of cells in the model.")
+        .def("cell_description", &py_recipe::cell_description, pybind11::return_value_policy::copy,
+            "gid"_a,
+            "High level description of the cell with global identifier gid.")
+        .def("cell_kind", &py_recipe::cell_kind,
+            "gid"_a,
+            "The kind of cell with global identifier gid.")
+        .def("num_sources", &py_recipe::num_sources,
+            "gid"_a,
+            "The number of spike sources on gid (default 0).")
+        .def("num_targets", &py_recipe::num_targets,
+            "gid"_a,
+            "The number of post-synaptic sites on gid (default 0).")
+        // TODO: py_recipe::num_probes
+        .def("num_gap_junction_sites", &py_recipe::num_gap_junction_sites,
+            "gid"_a,
+            "The number of gap junction sites on gid (default 0).")
+        .def("event_generators", &py_recipe::event_generators,
+            "gid"_a,
+            "A list of all the event generators that are attached to gid (default []).")
+        .def("connections_on", &py_recipe::connections_on,
+            "gid"_a,
+            "A list of all the incoming connections to gid (default []).")
+        .def("gap_junctions_on", &py_recipe::gap_junctions_on,
+            "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 "<pyarb.recipe>";})
+        .def("__repr__", [](const py_recipe&){return "<pyarb.recipe>";});
+}
+} // namespace pyarb
diff --git a/python/recipe.hpp b/python/recipe.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4e83c276d37eedb497be973871c0b1d25cb3c168
--- /dev/null
+++ b/python/recipe.hpp
@@ -0,0 +1,163 @@
+#pragma once
+
+#include <vector>
+
+#include <pybind11/pybind11.h>
+#include <pybind11/pytypes.h>
+#include <pybind11/stl.h>
+
+#include <arbor/event_generator.hpp>
+#include <arbor/recipe.hpp>
+
+namespace pyarb {
+
+// pyarb::recipe is the recipe interface used by Python.
+// Calls that return generic types return pybind11::object, to avoid
+// having to wrap some C++ types used by the C++ interface (specifically
+// util::unique_any, util::any, std::unique_ptr, etc.)
+// For example, requests for cell description return pybind11::object, instead
+// of util::unique_any used by the C++ recipe interface.
+// The py_recipe_shim unwraps the python objects, and forwards them
+// to the C++ back end.
+
+class py_recipe {
+public:
+    py_recipe() = default;
+    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_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 {}; }
+
+    //TODO: virtual pybind11::object get_probe (arb::cell_member_type id) const {...}
+
+    virtual pybind11::object global_properties(arb::cell_kind kind) const = 0;
+};
+
+class py_recipe_trampoline: public py_recipe {
+public:
+    arb::cell_size_type num_cells() const override {
+        PYBIND11_OVERLOAD_PURE(arb::cell_size_type, py_recipe, num_cells);
+    }
+
+    pybind11::object cell_description(arb::cell_gid_type gid) const override {
+        PYBIND11_OVERLOAD_PURE(pybind11::object, py_recipe, cell_description, gid);
+    }
+
+    arb::cell_kind cell_kind(arb::cell_gid_type gid) const override {
+        PYBIND11_OVERLOAD_PURE(arb::cell_kind, py_recipe, cell_kind, gid);
+    }
+    
+    arb::cell_size_type num_sources(arb::cell_gid_type gid) const override {
+        PYBIND11_OVERLOAD(arb::cell_size_type, py_recipe, num_sources, gid);
+    }
+
+    arb::cell_size_type num_targets(arb::cell_gid_type gid) const override {
+        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);
+    }
+
+    std::vector<pybind11::object> event_generators(arb::cell_gid_type gid) const override {
+        PYBIND11_OVERLOAD(std::vector<pybind11::object>, py_recipe, event_generators, gid);
+    }
+
+    std::vector<arb::cell_connection> connections_on(arb::cell_gid_type gid) const override {
+        PYBIND11_OVERLOAD(std::vector<arb::cell_connection>, py_recipe, connections_on, gid);
+    }
+
+    std::vector<arb::gap_junction_connection> gap_junctions_on(arb::cell_gid_type gid) const override {
+        PYBIND11_OVERLOAD(std::vector<arb::gap_junction_connection>, py_recipe, gap_junctions_on, gid);
+    }
+
+    //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.
+// Unwraps/translates python-side output from pyarb::recipe and forwards
+// to arb::recipe.
+// For example, unwrap cell descriptions stored in PyObject, and rewrap
+// in util::unique_any.
+
+class py_recipe_shim: public arb::recipe {
+    // pointer to the python recipe implementation
+    std::shared_ptr<py_recipe> impl_;
+
+public:
+    using recipe::recipe;
+
+    py_recipe_shim(std::shared_ptr<py_recipe> r): impl_(std::move(r)) {}
+
+    arb::cell_size_type num_cells() const override {
+        return impl_->num_cells();
+    }
+
+    // The pyarb::recipe::cell_decription returns a pybind11::object, that is
+    // unwrapped and copied into a util::unique_any.
+    arb::util::unique_any get_cell_description(arb::cell_gid_type gid) const override;
+
+    arb::cell_kind get_cell_kind(arb::cell_gid_type gid) const override {
+        return impl_->cell_kind(gid);
+    }
+
+    arb::cell_size_type num_sources(arb::cell_gid_type gid) const override {
+        return impl_->num_sources(gid);
+    }
+
+    arb::cell_size_type num_targets(arb::cell_gid_type gid) const override {
+        return impl_->num_targets(gid);
+    }
+
+/* //TODO: arb::cell_size_type num_probes(arb::cell_gid_type gid) const override {
+        return impl_->num_probes(gid);
+    }
+*/
+    arb::cell_size_type num_gap_junction_sites(arb::cell_gid_type gid) const override {
+        return impl_->num_gap_junction_sites(gid);
+    }
+
+    std::vector<arb::event_generator> event_generators(arb::cell_gid_type gid) const override;
+
+    std::vector<arb::cell_connection> connections_on(arb::cell_gid_type gid) const override {
+        return impl_->connections_on(gid);
+    }
+
+    std::vector<arb::gap_junction_connection> gap_junctions_on(arb::cell_gid_type gid) const override {
+        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;
+
+};
+
+} // namespace pyarb
diff --git a/python/test/unit/test_contexts.py b/python/test/unit/test_contexts.py
index e007207f680b2fdf56e122a5442d128f7b374b0b..a445bb153936b02a887c462995f5087356bcd9a6 100644
--- a/python/test/unit/test_contexts.py
+++ b/python/test/unit/test_contexts.py
@@ -42,18 +42,18 @@ class Contexts(unittest.TestCase):
 
     def test_exceptions_allocation(self):
         with self.assertRaisesRegex(RuntimeError,
-            "gpu id must be None, or a non-negative integer."):
+            "gpu_id must be None, or a non-negative integer"):
             arb.proc_allocation(gpu_id = 1.)
         with self.assertRaisesRegex(RuntimeError,
-            "gpu id must be None, or a non-negative integer."):
+            "gpu_id must be None, or a non-negative integer"):
             arb.proc_allocation(gpu_id = -1)
         with self.assertRaisesRegex(RuntimeError,
-            "gpu id must be None, or a non-negative integer."):
+            "gpu_id must be None, or a non-negative integer"):
             arb.proc_allocation(gpu_id = 'gpu_id')
         with self.assertRaises(TypeError):
             arb.proc_allocation(threads = 1.)
         with self.assertRaisesRegex(RuntimeError,
-            "threads must be a positive integer."):
+            "threads must be a positive integer"):
              arb.proc_allocation(threads = 0)
         with self.assertRaises(TypeError):
             arb.proc_allocation(threads = None)
diff --git a/python/test/unit/test_event_generators.py b/python/test/unit/test_event_generators.py
index 5febad17e93584c897eca8c3e47a4962795fea7e..b64119c8bcd8670cb6ea43a3fe9ab685a8e621a2 100644
--- a/python/test/unit/test_event_generators.py
+++ b/python/test/unit/test_event_generators.py
@@ -51,17 +51,17 @@ class RegularSchedule(unittest.TestCase):
 
     def test_exceptions_regular_schedule(self):
         with self.assertRaisesRegex(RuntimeError,
-            "tstart must a non-negative number, or None."):
+            "tstart must a non-negative number, or None"):
             arb.regular_schedule(tstart = -1.)
         with self.assertRaisesRegex(RuntimeError,
-            "dt must be a non-negative number."):
+            "dt must be a non-negative number"):
             arb.regular_schedule(dt = -0.1)
         with self.assertRaises(TypeError):
             arb.regular_schedule(dt = None)
         with self.assertRaises(TypeError):
             arb.regular_schedule(dt = 'dt')
         with self.assertRaisesRegex(RuntimeError,
-            "tstop must a non-negative number, or None."):
+            "tstop must a non-negative number, or None"):
             arb.regular_schedule(tstop = 'tstop')
 
 class ExplicitSchedule(unittest.TestCase):
@@ -86,7 +86,7 @@ class ExplicitSchedule(unittest.TestCase):
 
     def test_exceptions_explicit_schedule(self):
         with self.assertRaisesRegex(RuntimeError,
-            "explicit time schedule can not contain negative values."):
+            "explicit time schedule can not contain negative values"):
             arb.explicit_schedule([-1])
         with self.assertRaises(TypeError):
             arb.explicit_schedule(['times'])
@@ -128,14 +128,14 @@ class PoissonSchedule(unittest.TestCase):
 
     def test_exceptions_poisson_schedule(self):
         with self.assertRaisesRegex(RuntimeError,
-            "tstart must be a non-negative number."):
+            "tstart must be a non-negative number"):
             arb.poisson_schedule(tstart = -10.)
         with self.assertRaises(TypeError):
             arb.poisson_schedule(tstart = None)
         with self.assertRaises(TypeError):
             arb.poisson_schedule(tstart = 'tstart')
         with self.assertRaisesRegex(RuntimeError,
-            "frequency must be a non-negative number."):
+            "frequency must be a non-negative number"):
             arb.poisson_schedule(freq = -100.)
         with self.assertRaises(TypeError):
             arb.poisson_schedule(freq = 'freq')