diff --git a/arbor/cable_cell_param.cpp b/arbor/cable_cell_param.cpp
index 2f48b0a76ab44e41d451560de130c0b6b22d5cec..e272781b781bfc17d212a21f073b4452117450cb 100644
--- a/arbor/cable_cell_param.cpp
+++ b/arbor/cable_cell_param.cpp
@@ -114,15 +114,17 @@ std::vector<defaultable> cable_cell_parameter_set::serialize() const {
     return D;
 }
 
-void decor::paint(region where, paintable what) {
-    paintings_.push_back({std::move(where), std::move(what)});
+decor& decor::paint(region where, paintable what) {
+    paintings_.emplace_back(std::move(where), std::move(what));
+    return *this;
 }
 
-void decor::place(locset where, placeable what, cell_tag_type label) {
-    placements_.push_back({std::move(where), std::move(what), std::move(label)});
+decor& decor::place(locset where, placeable what, cell_tag_type label) {
+    placements_.emplace_back(std::move(where), std::move(what), std::move(label));
+    return *this;
 }
 
-void decor::set_default(defaultable what) {
+decor& decor::set_default(defaultable what) {
     std::visit(
             [this] (auto&& p) {
                 using T = std::decay_t<decltype(p)>;
@@ -158,6 +160,7 @@ void decor::set_default(defaultable what) {
                 }
             },
             what);
+    return *this;
 }
 
 } // namespace arb
diff --git a/arbor/include/arbor/cable_cell_param.hpp b/arbor/include/arbor/cable_cell_param.hpp
index 2c6cca399a0598375fcd26d86516d668ec2f480c..83a7e3deb3c1e49fafbea5545f12863bd2dd7129 100644
--- a/arbor/include/arbor/cable_cell_param.hpp
+++ b/arbor/include/arbor/cable_cell_param.hpp
@@ -311,9 +311,9 @@ public:
     const auto& placements() const {return placements_; }
     const auto& defaults()   const {return defaults_;   }
 
-    void paint(region, paintable);
-    void place(locset, placeable, cell_tag_type);
-    void set_default(defaultable);
+    decor& paint(region, paintable);
+    decor& place(locset, placeable, cell_tag_type);
+    decor& set_default(defaultable);
 };
 
 ARB_ARBOR_API extern cable_cell_parameter_set neuron_parameter_defaults;
diff --git a/doc/concepts/decor.rst b/doc/concepts/decor.rst
index 03a255fcf794ee9c199f6112ef6218b517334686..630af679323239f29d0346b5fb43f4c597577564 100644
--- a/doc/concepts/decor.rst
+++ b/doc/concepts/decor.rst
@@ -31,6 +31,14 @@ Decorations are described by a **decor** object in Arbor. It provides facilities
 * setting properties defined over the whole cell;
 * descriptions of dynamics applied to regions and locsets.
 
+.. note::
+
+   All methods on decor objects (``paint``, ``place``, and ``set_default``)
+   return a reference to the objects so you can chain them together. This saves
+   some repetition. You can break long statements over multiple lines, but in
+   Python this requires use of continuation lines ``\`` or wrapping the whole
+   expression into parentheses.
+
 .. _cablecell-paint:
 
 Painted dynamics
diff --git a/doc/tutorial/network_ring.rst b/doc/tutorial/network_ring.rst
index 4e0ba577a4e57c00642be443a63c17e774d5b384..43c5d5a3af1bcf7e017fd3cf2cd27b910ed9b451 100644
--- a/doc/tutorial/network_ring.rst
+++ b/doc/tutorial/network_ring.rst
@@ -40,7 +40,7 @@ These locations will form the endpoints of the connections between the cells.
 
 .. literalinclude:: ../../python/example/network_ring.py
    :language: python
-   :lines: 48-51
+   :lines: 48-58
 
 After we've created a basic :py:class:`arbor.decor`, step **(3)** places a synapse with an exponential decay (``'expsyn'``) on the ``'synapse_site'``.
 The synapse is given the label ``'syn'``, which is later used to form :py:class:`arbor.connection` objects terminating *at* the cell.
@@ -63,7 +63,7 @@ Step **(4)** places a spike detector at the ``'root'``. The detector is given th
 
 .. literalinclude:: ../../python/example/network_ring.py
    :language: python
-   :lines: 53-73
+   :lines: 60-69
 
 The recipe
 **********
@@ -107,7 +107,7 @@ Step **(11)** instantiates the recipe with 4 cells.
 
 .. literalinclude:: ../../python/example/network_ring.py
    :language: python
-   :lines: 76-124
+   :lines: 74-122
 
 The execution
 *************
@@ -143,7 +143,7 @@ Step **(15)** executes the simulation for a duration of 100 ms.
 
 .. literalinclude:: ../../python/example/network_ring.py
    :language: python
-   :lines: 126-138
+   :lines: 124-136
 
 The results
 ***********
@@ -152,7 +152,7 @@ Step **(16)** prints the timestamps of the spikes:
 
 .. literalinclude:: ../../python/example/network_ring.py
    :language: python
-   :lines: 140-143
+   :lines: 138-141
 
 Step **(17)** generates a plot of the sampling data.
 :py:func:`arbor.simulation.samples` takes a ``handle`` of the probe we wish to examine. It returns a list
@@ -164,7 +164,7 @@ It could have described a :term:`locset`.)
 
 .. literalinclude:: ../../python/example/network_ring.py
    :language: python
-   :lines: 145-
+   :lines: 143-
 
 Since we have created ``ncells`` cells, we have ``ncells`` traces. We should be seeing phase shifted traces, as the action potential propagated through the network.
 
diff --git a/doc/tutorial/single_cell_detailed.rst b/doc/tutorial/single_cell_detailed.rst
index 68c7b9c8e80a99549925e9f8ee0f3467812a5ded..c61ad38715d790d5df5a75a115f45eb604dec67c 100644
--- a/doc/tutorial/single_cell_detailed.rst
+++ b/doc/tutorial/single_cell_detailed.rst
@@ -95,13 +95,9 @@ to :ref:`Arbor's specifications <morph-formats>`). We can save the following in
 
 The morphology can then be loaded from ``single_cell_detailed.swc`` in the following way:
 
-.. code-block:: python
-
-    import arbor
-
-    # (1) Read the morphology from an SWC file
-
-    morph = arbor.load_swc_arbor("single_cell_detailed.swc")
+.. literalinclude:: ../../python/example/single_cell_detailed.py
+   :language: python
+   :lines: 10-20
 
 The label dictionary
 ^^^^^^^^^^^^^^^^^^^^
@@ -172,7 +168,7 @@ in the following way:
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 29-32
+   :lines: 30-31
 
 This will generate the following regions when applied to the previously defined morphology:
 
@@ -190,7 +186,7 @@ be done as follows:
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 33-34
+   :lines: 32-33
 
 This will generate the following region when applied to the previously defined morphology:
 
@@ -204,7 +200,7 @@ terminal points of the morphology.
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 38-40
+   :lines: 35-37
 
 This will generate the following **locsets** (sets of one or more locations) when applied to the
 previously defined morphology:
@@ -221,7 +217,7 @@ previously defined "custom" region; and, separately, the terminal points which b
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 41-44
+   :lines: 38-41
 
 This will generate the following 2 locsets when applied to the previously defined morphology:
 
@@ -254,7 +250,7 @@ step:
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 46-53
+   :lines: 48-52
 
 We have set the default initial membrane voltage to -55 mV; the default initial
 temperature to 300 K; the default axial resistivity to 35.4 Ω·cm; and the default membrane
@@ -273,8 +269,8 @@ We can override the default properties by *painting* new values on the relevant
 :meth:`arbor.decor.paint`.
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
-   :language: python
-   :lines: 55-57
+  :language: python
+  :lines: 53-55
 
 With the default and initial values taken care of, we now add some density mechanisms. Let's *paint*
 a *pas* density mechanism everywhere on the cell using the previously defined "all" region; an *hh*
@@ -283,7 +279,7 @@ mechanism has a custom 'gbar' parameter.
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 8,59-62
+   :lines: 8,56-59
 
 The decor object is also used to *place* stimuli and spike detectors on the cell using :meth:`arbor.decor.place`.
 We place 3 current clamps of 2 nA on the "root" locset defined earlier, starting at time = 10, 30, 50 ms and
@@ -293,7 +289,7 @@ in the recipe.
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 64-68
+   :lines: 60-64
 
 .. Note::
 
@@ -313,13 +309,13 @@ to be a single CV, and the rest of the morphology to be comprised of CVs with a
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 70-77
+   :lines: 65-66
 
 Finally, we create the cell.
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 79-81
+   :lines: 68-70
 
 The model
 *********
@@ -328,7 +324,7 @@ Having created the cell, we construct an :class:`arbor.single_cell_model`.
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 83-85
+   :lines: 72-74
 
 The global properties
 ^^^^^^^^^^^^^^^^^^^^^
@@ -370,7 +366,7 @@ model:
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 87-91
+   :lines: 76-80
 
 We set the same properties as we did earlier when we were creating the *decor* of the cell, except
 for the initial membrane voltage, which is -65 mV as opposed to -55 mV.
@@ -381,7 +377,7 @@ the "allen" catalogue. We can extend the default catalogue as follow:
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 93-97
+   :lines: 82-86
 
 Now all three mechanisms in the *decor* object have been made available to the model.
 
@@ -396,7 +392,7 @@ We can indicate the location we would like to probe using labels from the :class
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 99-103
+   :lines: 88-92
 
 The simulation
 ^^^^^^^^^^^^^^
@@ -405,7 +401,7 @@ The cell and model descriptions are now complete and we can run the simulation:
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 105-107
+   :lines: 94-102
 
 The results
 ^^^^^^^^^^^
@@ -416,7 +412,7 @@ spikes on the cell from all spike detectors on the cell and saves the times at w
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 109-113
+   :lines: 98-102
 
 A more interesting result of the simulation is perhaps the output of the voltage probe previously
 placed on the "custom_terminal" locset. The model saves the output of the probes as [time, value]
@@ -425,7 +421,7 @@ choose the any other library:
 
 .. literalinclude:: ../../python/example/single_cell_detailed.py
    :language: python
-   :lines: 5,6,114-
+   :lines: 5,6,104-
 
 The following plot is generated. The orange line is slightly delayed from the blue line, which is
 what we'd expect because branch 4 is longer than branch 3 of the morphology. We also see 3 spikes,
diff --git a/doc/tutorial/single_cell_detailed_recipe.rst b/doc/tutorial/single_cell_detailed_recipe.rst
index 451e28cda8a422dd318e668572d4a53aba897242..66f694ac31f7028211677d5dd85e3d17a5cd7f51 100644
--- a/doc/tutorial/single_cell_detailed_recipe.rst
+++ b/doc/tutorial/single_cell_detailed_recipe.rst
@@ -32,7 +32,7 @@ examine the recipe in detail: how to create one, and why it is needed.
 
 .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py
    :language: python
-   :lines: 84-120
+   :lines: 74-114
 
 Let's go through the recipe point by point.
 
@@ -88,26 +88,22 @@ Now we can instantiate a ``single_recipe`` object.
 
 .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py
    :language: python
-   :lines: 123-124
+   :lines: 113-114
 
 The simulation
 **************
 
 We have all we need to create a :class:`arbor.simulation` object.
 
-.. literalinclude:: ../../python/example/single_cell_detailed_recipe.py
-   :language: python
-   :lines: 126-127
-
 Before we run the simulation, however, we need to register what results we expect once execution is over.
 This was handled by the :class:`arbor.single_cell_model` object in the original example.
 
-We would like to get a list of the spikes on the cell during the runtime of the simulation, and we would like
-to plot the voltage registered by the probe on the "custom_terminal" locset.
-
 .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py
    :language: python
-   :lines: 129-133
+   :lines: 116-123
+
+We would like to get a list of the spikes on the cell during the runtime of the simulation, and we would like
+to plot the voltage registered by the probe on the "custom_terminal" locset.
 
 The lines handling probe sampling warrant a second look. First, we declared :term:`probe_id` to be a
 :class:`arbor.cell_member`, with :class:`arbor.cell_member.gid` = 0 and :class:`arbor.cell_member.index` = 0.
@@ -121,7 +117,7 @@ We can now run the simulation we just instantiated for a duration of 100 ms with
 
 .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py
    :language: python
-   :lines: 135-136
+   :lines: 125-126
 
 The results
 ***********
@@ -133,13 +129,13 @@ We can print the times of the spikes:
 
 .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py
    :language: python
-   :lines: 138-142
+   :lines: 128-132
 
 The probe results, again, warrant some more explanation:
 
 .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py
    :language: python
-   :lines: 144-148
+   :lines: 134-138
 
 ``sim.samples()`` takes a ``handle`` of the probe we wish to examine. It returns a list
 of ``(data, meta)`` terms: ``data`` being the time and value series of the probed quantity; and
@@ -152,7 +148,7 @@ We plot the results using pandas and seaborn as we did in the original example,
 
 .. literalinclude:: ../../python/example/single_cell_detailed_recipe.py
    :language: python
-   :lines: 150-
+   :lines: 140-
 
 The following plot is generated. Identical to the plot of the original example.
 
diff --git a/doc/tutorial/single_cell_recipe.rst b/doc/tutorial/single_cell_recipe.rst
index c9e77771bed071c5ae2577f86a97d31c004cbea6..40d3233578e6bb5da4c6a07bfeadb58443608de4 100644
--- a/doc/tutorial/single_cell_recipe.rst
+++ b/doc/tutorial/single_cell_recipe.rst
@@ -47,7 +47,7 @@ models without cells are quite boring!
 
 .. literalinclude:: ../../python/example/single_cell_recipe.py
    :language: python
-   :lines: 28-63
+   :lines: 31-61
 
 Step **(4)** describes the recipe that will reflect our single cell model.
 
@@ -77,7 +77,11 @@ Step **(4.6)** returns the properties that will be applied to all cells of that
 
 More methods can be overridden if your model requires that, see :class:`arbor.recipe` for options.
 
-Step **(5)** instantiates the recipe.
+Now we instantiate the recipe
+
+.. literalinclude:: ../../python/example/single_cell_recipe.py
+   :language: python
+   :lines: 64-67
 
 The simulation
 --------------
@@ -95,7 +99,7 @@ The details of manual hardware configuration will be left for another tutorial.
 
 .. literalinclude:: ../../python/example/single_cell_recipe.py
    :language: python
-   :lines: 65-75
+   :lines: 68-79
 
 Step **(6)** instantiates the simulation.
 
@@ -114,7 +118,7 @@ If we create the same analysis of the results we therefore expect the same resul
 
 .. literalinclude:: ../../python/example/single_cell_recipe.py
    :language: python
-   :lines: 77-
+   :lines: 80-
 
 Step **(8)** plots the measured potentials during the runtime of the simulation.
 Retrieving the sampled quantities is a little different, these have to be accessed
diff --git a/example/dryrun/branch_cell.hpp b/example/dryrun/branch_cell.hpp
index 9750c46c0170c85dfe18c7cfbc4564c0a4065ba5..cf208d6d2cd5aedb7e3a7e9204c3ceccd3064b35 100644
--- a/example/dryrun/branch_cell.hpp
+++ b/example/dryrun/branch_cell.hpp
@@ -104,34 +104,22 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
         dist_from_soma += l;
     }
 
-    arb::label_dict labels;
-
     using arb::reg::tagged;
+    arb::label_dict labels;
     labels.set("soma", tagged(stag));
     labels.set("dend", tagged(dtag));
 
-    arb::decor decor;
-
-    decor.paint("soma"_lab, arb::density("hh"));
-    decor.paint("dend"_lab, arb::density("pas"));
-
-    decor.set_default(arb::axial_resistivity{100}); // [Ω·cm]
-
-    // Add spike threshold detector at the soma.
-    decor.place(arb::mlocation{0,0}, arb::threshold_detector{10}, "detector");
-
-    // Add a synapse to the mid point of the first dendrite.
-    decor.place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "synapse");
+    auto decor = arb::decor()
+        .set_default(arb::axial_resistivity{100})                             // [Ω·cm]
+        .paint("soma"_lab, arb::density("hh"))                                // Add HH dynamics to soma.
+        .paint("dend"_lab, arb::density("pas"))                               // Leaky current everywhere else.
+        .place(arb::mlocation{0,0}, arb::threshold_detector{10}, "detector")  // Add spike threshold detector at the soma.
+        .place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "synapse")     // Add a synapse to the mid point of the first dendrite.
+        .set_default(arb::cv_policy_every_segment());                         // Make a CV between every sample in the sample tree.
 
     // Add additional synapses that will not be connected to anything.
     for (unsigned i=1u; i<params.synapses; ++i) {
         decor.place(arb::mlocation{1, 0.5}, arb::synapse("expsyn"), "dummy_synapses");
     }
-
-    // Make a CV between every sample in the sample tree.
-    decor.set_default(arb::cv_policy_every_segment());
-
-    arb::cable_cell cell(arb::morphology(tree), labels, decor);
-
-    return cell;
+    return arb::cable_cell{arb::morphology(tree), labels, decor};
 }
diff --git a/example/gap_junctions/gap_junctions.cpp b/example/gap_junctions/gap_junctions.cpp
index 10b124d1e37251552dce0b4b51a128216fad46bb..2887cb50c848bc94c803b21bab4987fbbbb071f4 100644
--- a/example/gap_junctions/gap_junctions.cpp
+++ b/example/gap_junctions/gap_junctions.cpp
@@ -275,37 +275,21 @@ arb::cable_cell gj_cell(cell_gid_type gid, unsigned ncell, double stim_duration)
     double dend_rad = 3./2; // μm
     tree.append(0, {0,0,2*soma_rad, dend_rad}, {0,0,2*soma_rad+300, dend_rad}, 3);  // dendrite
 
-    arb::decor decor;
-
-    decor.set_default(arb::axial_resistivity{100});       // [Ω·cm]
-    decor.set_default(arb::membrane_capacitance{0.018});  // [F/m²]
-
-    // Define the density channels and their parameters.
-    arb::mechanism_desc nax("nax");
-    nax["gbar"] = 0.04;
-    nax["sh"] = 10;
-
-    arb::mechanism_desc kdrmt("kdrmt");
-    kdrmt["gbar"] = 0.0001;
-
-    arb::mechanism_desc kamt("kamt");
-    kamt["gbar"] = 0.004;
-
-    arb::mechanism_desc pas("pas/e=-65.0");
-    pas["g"] =  1.0/12000.0;
-
-    // Paint density channels on all parts of the cell
-    decor.paint("(all)"_reg, arb::density{nax});
-    decor.paint("(all)"_reg, arb::density{kdrmt});
-    decor.paint("(all)"_reg, arb::density{kamt});
-    decor.paint("(all)"_reg, arb::density{pas});
-
-    // Add a spike detector to the soma.
-    decor.place(arb::mlocation{0,0}, arb::threshold_detector{10}, "detector");
-
-    // Add two gap junction sites.
-    decor.place(arb::mlocation{0, 1}, arb::junction{"gj"}, "local_1");
-    decor.place(arb::mlocation{0, 0}, arb::junction{"gj"}, "local_0");
+    auto decor = arb::decor{}
+        .set_default(arb::axial_resistivity{100})       // [Ω·cm]
+        .set_default(arb::membrane_capacitance{0.018})  // [F/m²]
+        // Paint density channels on all parts of the cell
+        .paint("(all)"_reg, arb::density{"nax", {{"gbar", 0.04}, {"sh", 10}}})
+        .paint("(all)"_reg, arb::density{"kdrmt", {{"gbar", 0.0001}}})
+        .paint("(all)"_reg, arb::density{"kamt", {{"gbar", 0.004}}})
+        .paint("(all)"_reg, arb::density{"pas/e=-65", {{"g", 1.0/12000.0}}})
+        // Add a spike detector to the soma.
+        .place(arb::mlocation{0,0}, arb::threshold_detector{10}, "detector")
+        // Add two gap junction sites.
+        .place(arb::mlocation{0, 1}, arb::junction{"gj"}, "local_1")
+        .place(arb::mlocation{0, 0}, arb::junction{"gj"}, "local_0")
+        // Add a synapse to the mid point of the first dendrite.
+        .place(arb::mlocation{0, 0.5}, arb::synapse{"expsyn"}, "syn");
 
     // Attach a stimulus to the first cell of the first group
     if (!gid) {
@@ -313,9 +297,6 @@ arb::cable_cell gj_cell(cell_gid_type gid, unsigned ncell, double stim_duration)
         decor.place(arb::mlocation{0, 0.5}, stim, "stim");
     }
 
-    // Add a synapse to the mid point of the first dendrite.
-    decor.place(arb::mlocation{0, 0.5}, arb::synapse{"expsyn"}, "syn");
-
     // Create the cell and set its electrical properties.
     return arb::cable_cell(tree, {}, decor);
 }
diff --git a/example/generators/generators.cpp b/example/generators/generators.cpp
index 9e72aff20af3da1ce9b854458f59d25efd57adc4..44c5f9c4fd122d9b4c4ce5432cfd19e3787eb1d0 100644
--- a/example/generators/generators.cpp
+++ b/example/generators/generators.cpp
@@ -59,13 +59,12 @@ public:
         arb::label_dict labels;
         labels.set("soma", arb::reg::tagged(1));
 
-        arb::decor decor;
-        decor.paint("soma"_lab, arb::density("pas"));
-
-        // Add one synapse at the soma.
-        // This synapse will be the target for all events, from both
-        // event_generators.
-        decor.place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "syn");
+        auto decor = arb::decor{}
+            .paint("soma"_lab, arb::density("pas"))
+            // Add one synapse at the soma.
+            // This synapse will be the target for all events, from both
+            // event_generators.
+        .place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "syn");
 
         return arb::cable_cell(tree, labels, decor);
     }
diff --git a/example/lfp/CMakeLists.txt b/example/lfp/CMakeLists.txt
index a3046c48121e81509f63aeb205eef364b559a3f9..a6dbabfd6e82bb188750bff2785ee177b93571cd 100644
--- a/example/lfp/CMakeLists.txt
+++ b/example/lfp/CMakeLists.txt
@@ -1,4 +1,4 @@
 add_executable(lfp EXCLUDE_FROM_ALL lfp.cpp)
 add_dependencies(examples lfp)
-target_link_libraries(lfp PRIVATE arbor ext-tinyopt)
+target_link_libraries(lfp PRIVATE arbor arborio ext-tinyopt)
 file(COPY plot-lfp.py DESTINATION "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
diff --git a/example/lfp/lfp.cpp b/example/lfp/lfp.cpp
index cb4f7ec26e223466b2f7267d00843276e5278714..95ead2c1529b1a9993eb79a311265810f3aa8dee 100644
--- a/example/lfp/lfp.cpp
+++ b/example/lfp/lfp.cpp
@@ -15,6 +15,8 @@
 #include <arbor/util/any_ptr.hpp>
 #include <arbor/util/unique_any.hpp>
 
+#include <arborio/label_parse.hpp>
+
 using std::any;
 using arb::util::any_cast;
 using arb::util::any_ptr;
@@ -22,6 +24,8 @@ using arb::util::unique_any;
 using arb::cell_gid_type;
 using arb::cell_member_type;
 
+using namespace arborio::literals;
+
 // Recipe represents one cable cell with one synapse, together with probes for total trans-membrane current, membrane voltage,
 // ionic current density, and synaptic conductance. A sequence of spikes are presented to the one synapse on the cell.
 
@@ -78,22 +82,18 @@ private:
         // Apical dendrite, length 490 μm, radius 1 μm, with SWC tag 4.
         tree.append(soma_apex, {0, 0, 10, 1},  {0, 0, 500, 1}, 4);
 
-        auto dec = arb::decor();
-        // Use NEURON defaults for reversal potentials, ion concentrations etc., but override ra, cm.
-        dec.set_default(axial_resistivity{100});     // [Ω·cm]
-        dec.set_default(membrane_capacitance{0.01}); // [F/m²]
-
-        // Twenty CVs per branch on the dendrites (tag 4).
-        dec.set_default(cv_policy_fixed_per_branch(20, arb::reg::tagged(4)));
-
-        // Add pas and hh mechanisms:
-        dec.paint(reg::tagged(1), density("hh")); // (default parameters)
-        dec.paint(reg::tagged(4), density("pas/e=-70.0"));
-
-        // Add exponential synapse at centre of soma.
-        synapse_location_ = ls::on_components(0.5, reg::tagged(1));
-        dec.place(synapse_location_, synapse("expsyn", {{"e", 0}, {"tau", 2}}), "syn");
-
+        synapse_location_ = "(on-components 0.5 (tag 1))"_ls;
+        auto dec = decor()
+            // Use NEURON defaults for reversal potentials, ion concentrations etc., but override ra, cm.
+            .set_default(axial_resistivity{100})     // [Ω·cm]
+            .set_default(membrane_capacitance{0.01}) // [F/m²]
+            // Twenty CVs per branch on the dendrites (tag 4).
+            .set_default(cv_policy_fixed_per_branch(20, arb::reg::tagged(4)))
+            // Add pas and hh mechanisms:
+            .paint("(tag 1)"_reg, density("hh")) // (default parameters)
+            .paint("(tag 4)"_reg, density("pas/e=-70.0"))
+            // Add exponential synapse at centre of soma.
+            .place(synapse_location_, synapse("expsyn", {{"e", 0}, {"tau", 2}}), "syn");
         cell_ = cable_cell(tree, {}, dec);
     }
 };
diff --git a/example/probe-demo/probe-demo.cpp b/example/probe-demo/probe-demo.cpp
index 0d27c111bd8b3a8b209717dc3dcbeff8904c42dc..16bcef344b83fa2b3910b7ec2887837e6fd66506 100644
--- a/example/probe-demo/probe-demo.cpp
+++ b/example/probe-demo/probe-demo.cpp
@@ -120,12 +120,11 @@ struct cable_recipe: public arb::recipe {
         arb::segment_tree tree;
         tree.append(arb::mnpos, {0, 0, 0, 0.5*diam}, {length, 0, 0, 0.5*diam}, 1);
 
-        arb::decor decor;
-        decor.paint(arb::reg::all(), arb::density("hh")); // HH mechanism over whole cell.
-        decor.place(arb::mlocation{0, 0.}, arb::i_clamp{1.}, "iclamp"); // Inject a 1 nA current indefinitely.
-        decor.place(arb::mlocation{0, 0.}, arb::synapse("expsyn"), "synapse1"); // a synapse
-        decor.place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "synapse2"); // another synapse
-
+        auto decor = arb::decor{}
+            .paint(arb::reg::all(), arb::density("hh"))                         // HH mechanism over whole cell.
+            .place(arb::mlocation{0, 0.}, arb::i_clamp{1.}, "iclamp")           // Inject a 1 nA current indefinitely.
+            .place(arb::mlocation{0, 0.}, arb::synapse("expsyn"), "synapse1")   // a synapse
+            .place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "synapse2"); // another synapse
         return arb::cable_cell(tree, {}, decor);
     }
 
diff --git a/example/ring/branch_cell.hpp b/example/ring/branch_cell.hpp
index 527f26012c4a47dac2b285d25c66347f18054e55..503827c0c1559b01a30d21f354fd7c04a7e1a681 100644
--- a/example/ring/branch_cell.hpp
+++ b/example/ring/branch_cell.hpp
@@ -110,21 +110,13 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
     labels.set("soma", tagged(stag));
     labels.set("dend", tagged(dtag));
 
-    arb::decor decor;
-
-    decor.paint("soma"_lab, arb::density("hh"));
-    decor.paint("dend"_lab, arb::density("pas"));
-
-    decor.set_default(arb::axial_resistivity{100}); // [Ω·cm]
-
-    // Add spike threshold detector at the soma.
-    decor.place(arb::mlocation{0,0}, arb::threshold_detector{10}, "detector");
-
-    // Add a synapse to the mid point of the first dendrite.
-    decor.place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "primary_syn");
-
+    auto decor = arb::decor{}
+        .paint("soma"_lab, arb::density("hh"))
+        .paint("dend"_lab, arb::density("pas"))
+        .set_default(arb::axial_resistivity{100}) // [Ω·cm]
+        .place(arb::mlocation{0,0}, arb::threshold_detector{10}, "detector")   // Add spike threshold detector at the soma.
+        .place(arb::mlocation{0, 0.5}, arb::synapse("expsyn"), "primary_syn"); // Add a synapse to the mid point of the first dendrite.
     // Add additional synapses that will not be connected to anything.
-
     if (params.synapses > 1) {
         decor.place(arb::ls::uniform("dend"_lab, 0, params.synapses - 2, gid), arb::synapse("expsyn"), "extra_syns");
     }
diff --git a/example/single/single.cpp b/example/single/single.cpp
index 4befabcdc35a3ead4698cf3e3d1a71df164841ce..26afc366ee16654d8743518b4619f4372f833f00 100644
--- a/example/single/single.cpp
+++ b/example/single/single.cpp
@@ -65,17 +65,12 @@ struct single_recipe: public arb::recipe {
         dict.set("soma", tagged(1));
         dict.set("dend", join(tagged(3), tagged(4), tagged(42)));
 
-        arb::decor decor;
-
-        // Add HH mechanism to soma, passive channels to dendrites.
-        decor.paint("soma"_lab, arb::density("hh"));
-        decor.paint("dend"_lab, arb::density("pas"));
-
-        // Add synapse to last branch.
-
-        arb::cell_lid_type last_branch = morpho.num_branches()-1;
-        arb::mlocation end_last_branch = { last_branch, 1. };
-        decor.place(end_last_branch, arb::synapse("exp2syn"), "synapse");
+        auto decor = arb::decor{}
+            // Add HH mechanism to soma, passive channels to dendrites.
+            .paint("soma"_lab, arb::density("hh"))
+            .paint("dend"_lab, arb::density("pas"))
+            // Add synapse to last branch.
+            .place(arb::mlocation{ morpho.num_branches()-1, 1. }, arb::synapse("exp2syn"), "synapse");
 
         return arb::cable_cell(morpho, dict, decor);
     }
diff --git a/python/cells.cpp b/python/cells.cpp
index 1020143d480613896d33e060e4e80c7fad092a21..ca4716f2ca22c1da7abf701e52c214753fcf19cd 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -657,6 +657,7 @@ void register_cells(pybind11::module& m) {
                 if (cm) d.set_default(arb::membrane_capacitance{*cm});
                 if (rL) d.set_default(arb::axial_resistivity{*rL});
                 if (tempK) d.set_default(arb::temperature_K{*tempK});
+                return d;
             },
             pybind11::arg_v("Vm",    pybind11::none(), "initial membrane voltage [mV]."),
             pybind11::arg_v("cm",    pybind11::none(), "membrane capacitance [F/m²]."),
@@ -674,9 +675,8 @@ void register_cells(pybind11::module& m) {
                 if (ext_con) d.set_default(arb::init_ext_concentration{ion, *ext_con});
                 if (rev_pot) d.set_default(arb::init_reversal_potential{ion, *rev_pot});
                 if (diff)    d.set_default(arb::ion_diffusivity{ion, *diff});
-                if (auto m = maybe_method(method)) {
-                    d.set_default(arb::ion_reversal_potential_method{ion, *m});
-                }
+                if (auto m = maybe_method(method)) d.set_default(arb::ion_reversal_potential_method{ion, *m});
+                return d;
             },
             pybind11::arg_v("ion", "name of the ion species."),
             pybind11::arg_v("int_con", pybind11::none(), "initial internal concentration [mM]."),
@@ -692,7 +692,7 @@ void register_cells(pybind11::module& m) {
         // Paint mechanisms.
         .def("paint",
             [](arb::decor& dec, const char* region, const arb::density& mechanism) {
-                dec.paint(arborio::parse_region_expression(region).unwrap(), mechanism);
+                return dec.paint(arborio::parse_region_expression(region).unwrap(), mechanism);
             },
             "region"_a, "mechanism"_a,
             "Associate a density mechanism with a region.")
@@ -705,7 +705,7 @@ void register_cells(pybind11::module& m) {
         // Paint membrane/static properties.
         .def("paint",
             [](arb::decor& dec,
-                const char* region,
+               const char* region,
                optional<double> Vm, optional<double> cm,
                optional<double> rL, optional<double> tempK)
             {
@@ -714,6 +714,7 @@ void register_cells(pybind11::module& m) {
                 if (cm) dec.paint(r, arb::membrane_capacitance{*cm});
                 if (rL) dec.paint(r, arb::axial_resistivity{*rL});
                 if (tempK) dec.paint(r, arb::temperature_K{*tempK});
+                return dec;
             },
             pybind11::arg_v("region", "the region label or description."),
             pybind11::arg_v("Vm",    pybind11::none(), "initial membrane voltage [mV]."),
@@ -731,6 +732,7 @@ void register_cells(pybind11::module& m) {
                 if (ext_con) dec.paint(r, arb::init_ext_concentration{name, *ext_con});
                 if (rev_pot) dec.paint(r, arb::init_reversal_potential{name, *rev_pot});
                 if (diff)    dec.paint(r, arb::ion_diffusivity{name, *diff});
+                return dec;
             },
             "region"_a, pybind11::kw_only(), "ion_name"_a,
             pybind11::arg_v("int_con", pybind11::none(), "Initial internal concentration [mM]"),
@@ -771,11 +773,11 @@ void register_cells(pybind11::module& m) {
             "Add a voltage spike detector at each location in locations."
             "The group of spike detectors has the label 'label', used for forming connections between cells.")
         .def("discretization",
-            [](arb::decor& dec, const arb::cv_policy& p) { dec.set_default(p); },
+            [](arb::decor& dec, const arb::cv_policy& p) { return dec.set_default(p); },
             pybind11::arg_v("policy", "A cv_policy used to discretise the cell into compartments for simulation"))
         .def("discretization",
             [](arb::decor& dec, const std::string& p) {
-                dec.set_default(arborio::parse_cv_policy_expression(p).unwrap());
+                return dec.set_default(arborio::parse_cv_policy_expression(p).unwrap());
             },
             pybind11::arg_v("policy", "An s-expression string representing a cv_policy used to discretise the "
                                       "cell into compartments for simulation"));
diff --git a/python/example/dynamic-catalogue.py b/python/example/dynamic-catalogue.py
index 67fc7725edcf8cfb342f6a8d4c979810b75666a4..64c74cc926e6009e310f464e7c2c3ffa31fd54cd 100644
--- a/python/example/dynamic-catalogue.py
+++ b/python/example/dynamic-catalogue.py
@@ -14,9 +14,7 @@ class recipe(arb.recipe):
         self.tree.append(arb.mnpos, (0, 0, 0, 10), (1, 0, 0, 10), 1)
         self.props = arb.neuron_cable_properties()
         self.props.catalogue = arb.load_catalogue(cat)
-        d = arb.decor()
-        d.paint("(all)", arb.density("dummy"))
-        d.set_property(Vm=0.0)
+        d = arb.decor().paint("(all)", "dummy").set_property(Vm=0.0)
         self.cell = arb.cable_cell(self.tree, arb.label_dict(), d)
 
     def global_properties(self, _):
diff --git a/python/example/gap_junctions.py b/python/example/gap_junctions.py
index 2541abd0098d811c733b6ed4afbf8c3ec302282a..2f93e22424a5e948bad345f241033368ded038bf 100644
--- a/python/example/gap_junctions.py
+++ b/python/example/gap_junctions.py
@@ -32,32 +32,30 @@ def make_cable_cell(gid):
     tree.append(s, arbor.mpoint(0, 0, 0, 2), arbor.mpoint(40, 0, 0, 2), tag=2)
 
     # Label dictionary for cell components
-    labels = arbor.label_dict().add_swc_tags()
-
-    # Mark location for synapse site at midpoint of dendrite (branch 0 = soma + dendrite)
-    labels["synapse_site"] = "(location 0 0.6)"
-
-    # Gap junction site at connection point of soma and dendrite
-    labels["gj_site"] = "(location 0 0.2)"
-
-    # Label root of the tree
-    labels["root"] = "(root)"
+    labels = arbor.label_dict(
+        {
+            # Mark location for synapse site at midpoint of dendrite (branch 0  soma + dendrite)
+            "synapse_site": "(location 0 0.6)",
+            # Gap junction site at connection point of soma and dendrite
+            "gj_site": "(location 0 0.2)",
+            # Label root of the tree
+            "root": "(root)",
+        }
+    ).add_swc_tags()
 
     # Paint dynamics onto the cell, hh on soma and passive properties on dendrite
-    decor = arbor.decor()
-    decor.paint('"soma"', arbor.density("hh"))
-    decor.paint('"dend"', arbor.density("pas"))
-
-    # Attach one synapse and gap junction each on their labeled sites
-    decor.place('"synapse_site"', arbor.synapse("expsyn"), "syn")
-    decor.place('"gj_site"', arbor.junction("gj"), "gj")
-
-    # Attach spike detector to cell root
-    decor.place('"root"', arbor.spike_detector(-10), "detector")
-
-    cell = arbor.cable_cell(tree, labels, decor)
+    decor = (
+        arbor.decor()
+        .paint('"soma"', arbor.density("hh"))
+        .paint('"dend"', arbor.density("pas"))
+        # Attach one synapse and gap junction each on their labeled sites
+        .place('"synapse_site"', arbor.synapse("expsyn"), "syn")
+        .place('"gj_site"', arbor.junction("gj"), "gj")
+        # Attach spike detector to cell root
+        .place('"root"', arbor.spike_detector(-10), "detector")
+    )
 
-    return cell
+    return arbor.cable_cell(tree, labels, decor)
 
 
 # Create a recipe that generates connected chains of cells
diff --git a/python/example/network_ring.py b/python/example/network_ring.py
index 692f44433cd719f5d9fc02afd40897b5d517d477..6013131001b254aa096ddf2e5d536d5bc4c99e92 100755
--- a/python/example/network_ring.py
+++ b/python/example/network_ring.py
@@ -46,31 +46,29 @@ def make_cable_cell(gid):
     )
 
     # Associate labels to tags
-    labels = arbor.label_dict()
-    labels["soma"] = "(tag 1)"
-    labels["dend"] = "(tag 3)"
-
-    # (2) Mark location for synapse at the midpoint of branch 1 (the first dendrite).
-    labels["synapse_site"] = "(location 1 0.5)"
-    # Mark the root of the tree.
-    labels["root"] = "(root)"
+    labels = arbor.label_dict(
+        {
+            "soma": "(tag 1)",
+            "dend": "(tag 3)",
+            # (2) Mark location for synapse at the midpoint of branch 1 (the first dendrite).
+            "synapse_site": "(location 1 0.5)",
+            # Mark the root of the tree.
+            "root": "(root)",
+        }
+    )
 
     # (3) Create a decor and a cable_cell
-    decor = arbor.decor()
-
-    # Put hh dynamics on soma, and passive properties on the dendrites.
-    decor.paint('"soma"', arbor.density("hh"))
-    decor.paint('"dend"', arbor.density("pas"))
-
-    # (4) Attach a single synapse.
-    decor.place('"synapse_site"', arbor.synapse("expsyn"), "syn")
-
-    # Attach a spike detector with threshold of -10 mV.
-    decor.place('"root"', arbor.spike_detector(-10), "detector")
-
-    cell = arbor.cable_cell(tree, labels, decor)
+    decor = (
+        arbor.decor()
+        # Put hh dynamics on soma, and passive properties on the dendrites.
+        .paint('"soma"', arbor.density("hh")).paint('"dend"', arbor.density("pas"))
+        # (4) Attach a single synapse.
+        .place('"synapse_site"', arbor.synapse("expsyn"), "syn")
+        # Attach a spike detector with threshold of -10 mV.
+        .place('"root"', arbor.spike_detector(-10), "detector")
+    )
 
-    return cell
+    return arbor.cable_cell(tree, labels, decor)
 
 
 # (5) Create a recipe that generates a network of connected cells.
diff --git a/python/example/network_ring_mpi.py b/python/example/network_ring_mpi.py
index 2ac4f014380f19339a35be803c6d029201264d04..6892a498eebd29dcee04a603d1a1a8869d90511c 100644
--- a/python/example/network_ring_mpi.py
+++ b/python/example/network_ring_mpi.py
@@ -48,31 +48,29 @@ def make_cable_cell(gid):
     )
 
     # Associate labels to tags
-    labels = arbor.label_dict()
-    labels["soma"] = "(tag 1)"
-    labels["dend"] = "(tag 3)"
-
-    # (2) Mark location for synapse at the midpoint of branch 1 (the first dendrite).
-    labels["synapse_site"] = "(location 1 0.5)"
-    # Mark the root of the tree.
-    labels["root"] = "(root)"
+    labels = arbor.label_dict(
+        {
+            "soma": "(tag 1)",
+            "dend": "(tag 3)",
+            # (2) Mark location for synapse at the midpoint of branch 1 (the first dendrite).
+            "synapse_site": "(location 1 0.5)",
+            # Mark the root of the tree.
+            "root": "(root)",
+        }
+    )
 
     # (3) Create a decor and a cable_cell
-    decor = arbor.decor()
-
-    # Put hh dynamics on soma, and passive properties on the dendrites.
-    decor.paint('"soma"', arbor.density("hh"))
-    decor.paint('"dend"', arbor.density("pas"))
-
-    # (4) Attach a single synapse.
-    decor.place('"synapse_site"', arbor.synapse("expsyn"), "syn")
-
-    # Attach a spike detector with threshold of -10 mV.
-    decor.place('"root"', arbor.spike_detector(-10), "detector")
-
-    cell = arbor.cable_cell(tree, labels, decor)
+    decor = (
+        arbor.decor()
+        # Put hh dynamics on soma, and passive properties on the dendrites.
+        .paint('"soma"', arbor.density("hh")).paint('"dend"', arbor.density("pas"))
+        # (4) Attach a single synapse.
+        .place('"synapse_site"', arbor.synapse("expsyn"), "syn")
+        # Attach a spike detector with threshold of -10 mV.
+        .place('"root"', arbor.spike_detector(-10), "detector")
+    )
 
-    return cell
+    return arbor.cable_cell(tree, labels, decor)
 
 
 # (5) Create a recipe that generates a network of connected cells.
diff --git a/python/example/network_two_cells_gap_junctions.py b/python/example/network_two_cells_gap_junctions.py
index 54fa53c0810a2a741afebc04a329e0a6be3d3dbf..95fd830ec4b4ac0ac55f42637fc6c4fc613e22ae 100755
--- a/python/example/network_two_cells_gap_junctions.py
+++ b/python/example/network_two_cells_gap_junctions.py
@@ -82,15 +82,15 @@ class TwoCellsWithGapJunction(arbor.recipe):
 
         labels = arbor.label_dict({"cell": "(tag 1)", "gj_site": "(location 0 0.5)"})
 
-        decor = arbor.decor()
-        decor.set_property(Vm=self.Vms[gid])
-        decor.set_property(cm=self.cm)
-        decor.set_property(rL=self.rL)
-
-        # add a gap junction mechanism at the "gj_site" location and label that specific mechanism on that location "gj_label"
-        junction_mech = arbor.junction("gj", {"g": self.gj_g})
-        decor.place('"gj_site"', junction_mech, "gj_label")
-        decor.paint('"cell"', arbor.density(f"pas/e={self.Vms[gid]}", {"g": self.g}))
+        decor = (
+            arbor.decor()
+            .set_property(Vm=self.Vms[gid])
+            .set_property(cm=self.cm)
+            .set_property(rL=self.rL)
+            # add a gap junction mechanism at the "gj_site" location and label that specific mechanism on that location "gj_label"
+            .place('"gj_site"', arbor.junction("gj", {"g": self.gj_g}), "gj_label")
+            .paint('"cell"', arbor.density(f"pas/e={self.Vms[gid]}", {"g": self.g}))
+        )
 
         if self.cv_policy_max_extent is not None:
             policy = arbor.cv_policy_max_extent(self.cv_policy_max_extent)
@@ -101,14 +101,15 @@ class TwoCellsWithGapJunction(arbor.recipe):
         return arbor.cable_cell(tree, labels, decor)
 
     def gap_junctions_on(self, gid):
-        assert gid in [0, 1]
-
         # create a bidirectional gap junction from cell 0 at label "gj_label" to cell 1 at label "gj_label" and back.
-        return [
-            arbor.gap_junction_connection(
-                (1 if gid == 0 else 0, "gj_label"), "gj_label", 1
-            )
-        ]
+
+        if gid == 0:
+            tgt = 1
+        elif gid == 1:
+            tgt = 0
+        else:
+            raise RuntimeError("Invalid GID for example.")
+        return [arbor.gap_junction_connection((tgt, "gj_label"), "gj_label", 1)]
 
 
 if __name__ == "__main__":
diff --git a/python/example/probe_lfpykit.py b/python/example/probe_lfpykit.py
index 9f99bcd7ce0fba18d9679094bb34e8af4ad381a6..10e769345a871c2276a5ff2d8ecc975ade1c932c 100644
--- a/python/example/probe_lfpykit.py
+++ b/python/example/probe_lfpykit.py
@@ -77,22 +77,20 @@ def make_cable_cell(morphology, clamp_location):
     labels = arbor.label_dict()
 
     # decor
-    decor = arbor.decor()
-
-    # set initial voltage, temperature, axial resistivity, membrane capacitance
-    decor.set_property(
-        Vm=-65,  # Initial membrane voltage (mV)
-        tempK=300,  # Temperature (Kelvin)
-        rL=10000,  # Axial resistivity (Ω cm)
-        cm=0.01,  # Membrane capacitance (F/m**2)
+    decor = (
+        arbor.decor()
+        # set initial voltage, temperature, axial resistivity, membrane capacitance
+        .set_property(
+            Vm=-65,  # Initial membrane voltage (mV)
+            tempK=300,  # Temperature (Kelvin)
+            rL=10000,  # Axial resistivity (Ω cm)
+            cm=0.01,  # Membrane capacitance (F/m**2)
+        )
+        # set passive mechanism all over
+        # passive mech w. leak reversal potential (mV)
+        .paint("(all)", arbor.density("pas/e=-65", {"g": 0.0001}))
     )
 
-    # set passive mechanism all over
-    # passive mech w. leak reversal potential (mV)
-    pas = arbor.mechanism("pas/e=-65")
-    pas.set("g", 0.0001)  # leak conductivity (S/cm2)
-    decor.paint("(all)", arbor.density(pas))
-
     # set number of CVs per branch
     policy = arbor.cv_policy_fixed_per_branch(cvs_per_branch)
     decor.discretization(policy)
diff --git a/python/example/single_cell_cable.py b/python/example/single_cell_cable.py
index adf2e4e7bbb775a3e63321d01fea7d23ffc7323c..8cb5d4cfdb3bb0e61e5eea34dd89ce0356f6b6d7 100755
--- a/python/example/single_cell_cable.py
+++ b/python/example/single_cell_cable.py
@@ -92,19 +92,17 @@ class Cable(arbor.recipe):
 
         labels = arbor.label_dict({"cable": "(tag 1)", "start": "(location 0 0)"})
 
-        decor = arbor.decor()
-        decor.set_property(Vm=self.Vm)
-        decor.set_property(cm=self.cm)
-        decor.set_property(rL=self.rL)
-
-        decor.paint('"cable"', arbor.density(f"pas/e={self.Vm}", {"g": self.g}))
-
-        decor.place(
-            '"start"',
-            arbor.iclamp(
-                self.stimulus_start, self.stimulus_duration, self.stimulus_amplitude
-            ),
-            "iclamp",
+        decor = (
+            arbor.decor()
+            .set_property(Vm=self.Vm, cm=self.cm, rL=self.rL)
+            .paint('"cable"', arbor.density(f"pas/e={self.Vm}", {"g": self.g}))
+            .place(
+                '"start"',
+                arbor.iclamp(
+                    self.stimulus_start, self.stimulus_duration, self.stimulus_amplitude
+                ),
+                "iclamp",
+            )
         )
 
         policy = arbor.cv_policy_max_extent(self.cv_policy_max_extent)
diff --git a/python/example/single_cell_detailed.py b/python/example/single_cell_detailed.py
index afff91736d4ec9c715d6c67bd8f1ca43d7526632..b9f85fd99850f4dd3702850a899fc4cc7c188613 100755
--- a/python/example/single_cell_detailed.py
+++ b/python/example/single_cell_detailed.py
@@ -21,60 +21,49 @@ morph = arbor.load_swc_arbor(filename)
 
 # (2) Create and populate the label dictionary.
 
-# Pre-defined SWC regions
-labels = arbor.label_dict().add_swc_tags()
 
-# Regions:
-
-# Add a label for a region that includes the whole morphology
-labels["all"] = "(all)"
-# Add a label for the parts of the morphology with radius greater than 1.5 μm.
-labels["gt_1.5"] = '(radius-ge (region "all") 1.5)'
-# Join regions "apic" and "gt_1.5"
-labels["custom"] = '(join (region "apic") (region "gt_1.5"))'
-
-# Locsets:
-
-# Add a labels for the root of the morphology and all the terminal points
-labels["root"] = "(root)"
-labels["terminal"] = "(terminal)"
-# Add a label for the terminal locations in the "custom" region:
-labels["custom_terminal"] = '(restrict (locset "terminal") (region "custom"))'
-# Add a label for the terminal locations in the "axon" region:
-labels["axon_terminal"] = '(restrict (locset "terminal") (region "axon"))'
+labels = arbor.label_dict(
+    {
+        # Regions:
+        # Add a label for a region that includes the whole morphology
+        "all": "(all)",
+        # Add a label for the parts of the morphology with radius greater than 1.5 μm.
+        "gt_1.5": '(radius-ge (region "all") 1.5)',
+        # Join regions "apic" and "gt_1.5"
+        "custom": '(join (region "apic") (region "gt_1.5"))',
+        # Locsets:
+        # Add a labels for the root of the morphology and all the terminal points
+        "root": "(root)",
+        "terminal": "(terminal)",
+        # Add a label for the terminal locations in the "custom" region:
+        "custom_terminal": '(restrict (locset "terminal") (region "custom"))',
+        # Add a label for the terminal locations in the "axon" region:
+        "axon_terminal": '(restrict (locset "terminal") (region "axon"))',
+    }
+).add_swc_tags()
 
 # (3) Create and populate the decor.
+# NB. This can be written more compactly using method chaining
 
 decor = arbor.decor()
-
 # Set the default properties of the cell (this overrides the model defaults).
 decor.set_property(Vm=-55)
 decor.set_ion("na", int_con=10, ext_con=140, rev_pot=50, method="nernst/na")
 decor.set_ion("k", int_con=54.4, ext_con=2.5, rev_pot=-77)
-
 # Override the cell defaults.
 decor.paint('"custom"', tempK=270)
 decor.paint('"soma"', Vm=-50)
-
 # Paint density mechanisms.
 decor.paint('"all"', density("pas"))
 decor.paint('"custom"', density("hh"))
 decor.paint('"dend"', density("Ih", {"gbar": 0.001}))
-
 # Place stimuli and spike detectors.
 decor.place('"root"', arbor.iclamp(10, 1, current=2), "iclamp0")
 decor.place('"root"', arbor.iclamp(30, 1, current=2), "iclamp1")
 decor.place('"root"', arbor.iclamp(50, 1, current=2), "iclamp2")
 decor.place('"axon_terminal"', arbor.spike_detector(-10), "detector")
-
-# Single CV for the "soma" region
-soma_policy = arbor.cv_policy_single('"soma"')
-# Single CV for the "soma" region
-dflt_policy = arbor.cv_policy_max_extent(1.0)
-# default policy everywhere except the soma
-policy = dflt_policy | soma_policy
-# Set cv_policy
-decor.discretization(policy)
+# Set discretisation: Soma as one CV, 1um everywhere else
+decor.discretization('(replace (single (region "soma")) (max-extent 1.0))')
 
 # (4) Create the cell.
 
diff --git a/python/example/single_cell_detailed_recipe.py b/python/example/single_cell_detailed_recipe.py
index 86aa9ce49f59c0ecf7b86c7a927e3f6d5fa9a065..e13f8a85b38e12abe264203d7f66cfb1587bfd09 100644
--- a/python/example/single_cell_detailed_recipe.py
+++ b/python/example/single_cell_detailed_recipe.py
@@ -21,60 +21,50 @@ morph = arbor.load_swc_arbor(filename)
 
 # (2) Create and populate the label dictionary.
 
-# Label dict, with Pre-defined labels soma, axon, dend, and apic
-labels = arbor.label_dict().add_swc_tags()
-
-# Regions:
-
-# Add a label for a region that includes the whole morphology
-labels["all"] = "(all)"
-# Add a label for the parts of the morphology with radius greater than 1.5 μm.
-labels["gt_1.5"] = '(radius-ge (region "all") 1.5)'
-# Join regions "apic" and "gt_1.5"
-labels["custom"] = '(join (region "apic") (region "gt_1.5"))'
-
-# Locsets:
-
-# Add a labels for the root of the morphology and all the terminal points
-labels["root"] = "(root)"
-labels["terminal"] = "(terminal)"
-# Add a label for the terminal locations in the "custom" region:
-labels["custom_terminal"] = '(restrict (locset "terminal") (region "custom"))'
-# Add a label for the terminal locations in the "axon" region:
-labels["axon_terminal"] = '(restrict (locset "terminal") (region "axon"))'
+labels = arbor.label_dict(
+    {
+        # Regions:
+        # Add a label for a region that includes the whole morphology
+        "all": "(all)",
+        # Add a label for the parts of the morphology with radius greater than 1.5 μm.
+        "gt_1.5": '(radius-ge (region "all") 1.5)',
+        # Join regions "apic" and "gt_1.5"
+        "custom": '(join (region "apic") (region "gt_1.5"))',
+        # Locsets:
+        # Add a labels for the root of the morphology and all the terminal points
+        "root": "(root)",
+        "terminal": "(terminal)",
+        # Add a label for the terminal locations in the "custom" region:
+        "custom_terminal": '(restrict (locset "terminal") (region "custom"))',
+        # Add a label for the terminal locations in the "axon" region:
+        "axon_terminal": '(restrict (locset "terminal") (region "axon"))',
+    }
+).add_swc_tags()  # Add SWC pre-defined regions
 
 # (3) Create and populate the decor.
 
-decor = arbor.decor()
-
-# Set the default properties of the cell (this overrides the model defaults).
-decor.set_property(Vm=-55)
-decor.set_ion("na", int_con=10, ext_con=140, rev_pot=50, method="nernst/na")
-decor.set_ion("k", int_con=54.4, ext_con=2.5, rev_pot=-77)
-
-# Override the cell defaults.
-decor.paint('"custom"', tempK=270)
-decor.paint('"soma"', Vm=-50)
-
-# Paint density mechanisms.
-decor.paint('"all"', density("pas"))
-decor.paint('"custom"', density("hh"))
-decor.paint('"dend"', density("Ih", {"gbar": 0.001}))
-
-# Place stimuli and spike detectors.
-decor.place('"root"', arbor.iclamp(10, 1, current=2), "iclamp0")
-decor.place('"root"', arbor.iclamp(30, 1, current=2), "iclamp1")
-decor.place('"root"', arbor.iclamp(50, 1, current=2), "iclamp2")
-decor.place('"axon_terminal"', arbor.spike_detector(-10), "detector")
-
-# Single CV for the "soma" region
-soma_policy = arbor.cv_policy_single('"soma"')
-# Single CV for the "soma" region
-dflt_policy = arbor.cv_policy_max_extent(1.0)
-# default policy everywhere except the soma
-policy = dflt_policy | soma_policy
-# Set cv_policy
-decor.discretization(policy)
+decor = (
+    arbor.decor()
+    # Set the default properties of the cell (this overrides the model defaults).
+    .set_property(Vm=-55)
+    .set_ion("na", int_con=10, ext_con=140, rev_pot=50, method="nernst/na")
+    .set_ion("k", int_con=54.4, ext_con=2.5, rev_pot=-77)
+    # Override the cell defaults.
+    .paint('"custom"', tempK=270)
+    .paint('"soma"', Vm=-50)
+    # Paint density mechanisms.
+    .paint('"all"', density("pas"))
+    .paint('"custom"', density("hh"))
+    .paint('"dend"', density("Ih", {"gbar": 0.001}))
+    # Place stimuli and spike detectors.
+    .place('"root"', arbor.iclamp(10, 1, current=2), "iclamp0")
+    .place('"root"', arbor.iclamp(30, 1, current=2), "iclamp1")
+    .place('"root"', arbor.iclamp(50, 1, current=2), "iclamp2")
+    .place('"axon_terminal"', arbor.spike_detector(-10), "detector")
+    # Set discretisation: Soma as one CV, 1um everywhere else
+    .discretization('(replace (single (region "soma")) (max-extent 1.0))')
+)
+
 
 # (4) Create the cell.
 
diff --git a/python/example/single_cell_model.py b/python/example/single_cell_model.py
index 9510178248812b67b955e1214ace4052e9fc8487..28d52b1c423b75e69605889de8c9b21b691a4371 100755
--- a/python/example/single_cell_model.py
+++ b/python/example/single_cell_model.py
@@ -13,11 +13,14 @@ tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), ta
 labels = arbor.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"})
 
 # (3) Create and set up a decor object
-decor = arbor.decor()
-decor.set_property(Vm=-40)
-decor.paint('"soma"', arbor.density("hh"))
-decor.place('"midpoint"', arbor.iclamp(10, 2, 0.8), "iclamp")
-decor.place('"midpoint"', arbor.spike_detector(-10), "detector")
+
+decor = (
+    arbor.decor()
+    .set_property(Vm=-40)
+    .paint('"soma"', arbor.density("hh"))
+    .place('"midpoint"', arbor.iclamp(10, 2, 0.8), "iclamp")
+    .place('"midpoint"', arbor.spike_detector(-10), "detector")
+)
 
 # (4) Create cell and the single cell model based on it
 cell = arbor.cable_cell(tree, labels, decor)
diff --git a/python/example/single_cell_nml.py b/python/example/single_cell_nml.py
index 7082cd6a293108e173ebcb99c65b80892d7703bd..7e0f63fec817b36b2f78ff92c252553a3bab8da5 100755
--- a/python/example/single_cell_nml.py
+++ b/python/example/single_cell_nml.py
@@ -27,53 +27,46 @@ morpho_segments = morpho_data.segments()
 morpho_named = morpho_data.named_segments()
 morpho_groups = morpho_data.groups()
 
-# Create new label dict add to it all the NeuroML dictionaries.
-labels = arbor.label_dict()
+# Create new label dict with some locsets.
+labels = arbor.label_dict(
+    {
+        "stim_site": "(location 1 0.5)",  # site for the stimulus, in the middle of branch 1.
+        "axon_end": '(restrict (terminal) (region "axon"))',  # end of the axon.
+        "root": "(root)",  # the start of the soma in this morphology is at the root of the cell.
+    }
+)
+# Add to it all the NeuroML dictionaries.
 labels.append(morpho_segments)
 labels.append(morpho_named)
 labels.append(morpho_groups)
 
-# Add locsets to the label dictionary.
-labels[
-    "stim_site"
-] = "(location 1 0.5)"  # site for the stimulus, in the middle of branch 1.
-labels["axon_end"] = '(restrict (terminal) (region "axon"))'  # end of the axon.
-labels[
-    "root"
-] = "(root)"  # the start of the soma in this morphology is at the root of the cell.
-
 # Optional: print out the regions and locsets available in the label dictionary.
 print("Label dictionary regions: ", labels.regions, "\n")
 print("Label dictionary locsets: ", labels.locsets, "\n")
 
-decor = arbor.decor()
-
-# Set initial membrane potential to -55 mV
-decor.set_property(Vm=-55)
-# Use Nernst to calculate reversal potential for calcium.
-decor.set_ion("ca", method=mech("nernst/x=ca"))
-# decor.set_ion('ca', method='nernst/x=ca')
-# hh mechanism on the soma and axon.
-decor.paint('"soma"', arbor.density("hh"))
-decor.paint('"axon"', arbor.density("hh"))
-# pas mechanism the dendrites.
-decor.paint('"dend"', arbor.density("pas"))
-# Increase resistivity on dendrites.
-decor.paint('"dend"', rL=500)
-# Attach stimuli that inject 4 nA current for 1 ms, starting at 3 and 8 ms.
-decor.place('"root"', arbor.iclamp(10, 1, current=5), "iclamp0")
-decor.place('"stim_site"', arbor.iclamp(3, 1, current=0.5), "iclamp1")
-decor.place('"stim_site"', arbor.iclamp(10, 1, current=0.5), "iclamp2")
-decor.place('"stim_site"', arbor.iclamp(8, 1, current=4), "iclamp3")
-# Detect spikes at the soma with a voltage threshold of -10 mV.
-decor.place('"axon_end"', arbor.spike_detector(-10), "detector")
-
-# Create the policy used to discretise the cell into CVs.
-# Use a single CV for the soma, and CVs of maximum length 1 μm elsewhere.
-soma_policy = arbor.cv_policy_single('"soma"')
-dflt_policy = arbor.cv_policy_max_extent(1.0)
-policy = dflt_policy | soma_policy
-decor.discretization(policy)
+decor = (
+    arbor.decor()
+    # Set initial membrane potential to -55 mV
+    .set_property(Vm=-55)
+    # Use Nernst to calculate reversal potential for calcium.
+    .set_ion("ca", method=mech("nernst/x=ca"))
+    # hh mechanism on the soma and axon.
+    .paint('"soma"', arbor.density("hh"))
+    .paint('"axon"', arbor.density("hh"))
+    # pas mechanism the dendrites.
+    .paint('"dend"', arbor.density("pas"))
+    # Increase resistivity on dendrites.
+    .paint('"dend"', rL=500)
+    # Attach stimuli that inject 4 nA current for 1 ms, starting at 3 and 8 ms.
+    .place('"root"', arbor.iclamp(10, 1, current=5), "iclamp0")
+    .place('"stim_site"', arbor.iclamp(3, 1, current=0.5), "iclamp1")
+    .place('"stim_site"', arbor.iclamp(10, 1, current=0.5), "iclamp2")
+    .place('"stim_site"', arbor.iclamp(8, 1, current=4), "iclamp3")
+    # Detect spikes at the soma with a voltage threshold of -10 mV.
+    .place('"axon_end"', arbor.spike_detector(-10), "detector")
+    # Set discretisation: Soma as one CV, 1um everywhere else
+    .discretization('(replace (single (region "soma")) (max-extent 1.0))')
+)
 
 # Combine morphology with region and locset definitions to make a cable cell.
 cell = arbor.cable_cell(morpho, labels, decor)
diff --git a/python/example/single_cell_recipe.py b/python/example/single_cell_recipe.py
index 1a2f0334b370270e038b45f250050e91ae022182..6e12fd546da5f99c28dee13a6dac92d8e5cf498d 100644
--- a/python/example/single_cell_recipe.py
+++ b/python/example/single_cell_recipe.py
@@ -18,11 +18,14 @@ labels = arbor.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"})
 
 # (3) Create cell and set properties
 
-decor = arbor.decor()
-decor.set_property(Vm=-40)
-decor.paint('"soma"', arbor.density("hh"))
-decor.place('"midpoint"', arbor.iclamp(10, 2, 0.8), "iclamp")
-decor.place('"midpoint"', arbor.spike_detector(-10), "detector")
+decor = (
+    arbor.decor()
+    .set_property(Vm=-40)
+    .paint('"soma"', arbor.density("hh"))
+    .place('"midpoint"', arbor.iclamp(10, 2, 0.8), "iclamp")
+    .place('"midpoint"', arbor.spike_detector(-10), "detector")
+)
+
 cell = arbor.cable_cell(tree, labels, decor)
 
 # (4) Define a recipe for a single cell and set of probes upon it.
@@ -31,30 +34,30 @@ cell = arbor.cable_cell(tree, labels, decor)
 
 
 class single_recipe(arbor.recipe):
+    # (4.1) The base class constructor must be called first, to ensure that
+    # all memory in the wrapped C++ class is initialized correctly.
     def __init__(self):
-        # (4.1) 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.the_props = arbor.neuron_cable_properties()
 
+    # (4.2) Override the num_cells method
     def num_cells(self):
-        # (4.2) Override the num_cells method
         return 1
 
+    # (4.3) Override the cell_kind method
     def cell_kind(self, gid):
-        # (4.3) Override the cell_kind method
         return arbor.cell_kind.cable
 
+    # (4.4) Override the cell_description method
     def cell_description(self, gid):
-        # (4.4) Override the cell_description method
         return cell
 
+    # (4.5) Override the probes method with a voltage probe located on "midpoint"
     def probes(self, gid):
-        # (4.5) Override the probes method with a voltage probe located on "midpoint"
         return [arbor.cable_probe_membrane_voltage('"midpoint"')]
 
+    # (4.6) Override the global_properties method
     def global_properties(self, kind):
-        # (4.6) Override the global_properties method
         return self.the_props
 
 
diff --git a/python/example/single_cell_stdp.py b/python/example/single_cell_stdp.py
index 8c2d443c86730def84b92c52473ec1f693cb7aa4..4330b9c269bec0659b0cc37423a4d040b14bdfb6 100755
--- a/python/example/single_cell_stdp.py
+++ b/python/example/single_cell_stdp.py
@@ -1,9 +1,9 @@
 #!/usr/bin/env python3
 
 import arbor
-import numpy
-import pandas
-import seaborn  # You may have to pip install these.
+import numpy as np
+import pandas as pd
+import seaborn as sns  # You may have to pip install these.
 
 
 class single_recipe(arbor.recipe):
@@ -28,27 +28,25 @@ class single_recipe(arbor.recipe):
 
         labels = arbor.label_dict({"soma": "(tag 1)", "center": "(location 0 0.5)"})
 
-        decor = arbor.decor()
-        decor.set_property(Vm=-40)
-        decor.paint("(all)", arbor.density("hh"))
-
-        decor.place('"center"', arbor.spike_detector(-10), "detector")
-        decor.place('"center"', arbor.synapse("expsyn"), "synapse")
-
-        mech = arbor.mechanism("expsyn_stdp")
-        mech.set("max_weight", 1.0)
-        syn = arbor.synapse(mech)
-
-        decor.place('"center"', syn, "stpd_synapse")
-
-        cell = arbor.cable_cell(tree, labels, decor)
+        decor = (
+            arbor.decor()
+            .set_property(Vm=-40)
+            .paint("(all)", arbor.density("hh"))
+            .place('"center"', arbor.spike_detector(-10), "detector")
+            .place('"center"', arbor.synapse("expsyn"), "synapse")
+            .place(
+                '"center"',
+                arbor.synapse("expsyn_stdp", {"max_weight": 1.0}),
+                "stpd_synapse",
+            )
+        )
 
-        return cell
+        return arbor.cable_cell(tree, labels, decor)
 
     def event_generators(self, gid):
         """two stimuli: one that makes the cell spike, the other to monitor STDP"""
 
-        stimulus_times = numpy.linspace(50, 500, self.n_pairs)
+        stimulus_times = np.linspace(50, 500, self.n_pairs)
 
         # strong enough stimulus
         spike = arbor.event_generator(
@@ -63,12 +61,15 @@ class single_recipe(arbor.recipe):
         return [spike, stdp]
 
     def probes(self, gid):
+        def mk(w):
+            return arbor.cable_probe_point_state(1, "expsyn_stdp", w)
+
         return [
             arbor.cable_probe_membrane_voltage('"center"'),
-            arbor.cable_probe_point_state(1, "expsyn_stdp", "g"),
-            arbor.cable_probe_point_state(1, "expsyn_stdp", "apost"),
-            arbor.cable_probe_point_state(1, "expsyn_stdp", "apre"),
-            arbor.cable_probe_point_state(1, "expsyn_stdp", "weight_plastic"),
+            mk("g"),
+            mk("apost"),
+            mk("apre"),
+            mk("weight_plastic"),
         ]
 
     def global_properties(self, kind):
@@ -83,40 +84,33 @@ def run(dT, n_pairs=1, do_plots=False):
     sim.record(arbor.spike_recording.all)
 
     reg_sched = arbor.regular_schedule(0.1)
-    handle_mem = sim.sample((0, 0), reg_sched)
-    handle_g = sim.sample((0, 1), reg_sched)
-    handle_apost = sim.sample((0, 2), reg_sched)
-    handle_apre = sim.sample((0, 3), reg_sched)
-    handle_weight_plastic = sim.sample((0, 4), reg_sched)
+    handles = {
+        "U": sim.sample((0, 0), reg_sched),
+        "g": sim.sample((0, 1), reg_sched),
+        "apost": sim.sample((0, 2), reg_sched),
+        "apre": sim.sample((0, 3), reg_sched),
+        "weight_plastic": sim.sample((0, 4), reg_sched),
+    }
 
     sim.run(tfinal=600)
 
     if do_plots:
         print("Plotting detailed results ...")
-
-        for (handle, var) in [
-            (handle_mem, "U"),
-            (handle_g, "g"),
-            (handle_apost, "apost"),
-            (handle_apre, "apre"),
-            (handle_weight_plastic, "weight_plastic"),
-        ]:
-
+        for var, handle in handles.items():
             data, meta = sim.samples(handle)[0]
 
-            df = pandas.DataFrame({"t/ms": data[:, 0], var: data[:, 1]})
-            seaborn.relplot(data=df, kind="line", x="t/ms", y=var, ci=None).savefig(
-                "single_cell_stdp_result_{}.svg".format(var)
+            df = pd.DataFrame({"t/ms": data[:, 0], var: data[:, 1]})
+            sns.relplot(data=df, kind="line", x="t/ms", y=var, ci=None).savefig(
+                f"single_cell_stdp_result_{var}.svg"
             )
 
-    weight_plastic, meta = sim.samples(handle_weight_plastic)[0]
-
+    weight_plastic, _ = sim.samples(handles["weight_plastic"])[0]
     return weight_plastic[:, 1][-1]
 
 
-data = numpy.array([(dT, run(dT)) for dT in numpy.arange(-20, 20, 0.5)])
-df = pandas.DataFrame({"t/ms": data[:, 0], "dw": data[:, 1]})
+data = np.array([(dT, run(dT)) for dT in np.arange(-20, 20, 0.5)])
+df = pd.DataFrame({"t/ms": data[:, 0], "dw": data[:, 1]})
 print("Plotting results ...")
-seaborn.relplot(data=df, x="t/ms", y="dw", kind="line", ci=None).savefig(
+sns.relplot(data=df, x="t/ms", y="dw", kind="line", ci=None).savefig(
     "single_cell_stdp.svg"
 )
diff --git a/python/example/single_cell_swc.py b/python/example/single_cell_swc.py
index 2a229399396ec79ef948ed8b33ddda8938dcb87b..aa0e2df74f56681ddfaca75843747db2f6dfd76a 100755
--- a/python/example/single_cell_swc.py
+++ b/python/example/single_cell_swc.py
@@ -11,7 +11,6 @@
 #     attached to the soma.
 
 import arbor
-from arbor import mechanism as mech
 import pandas
 import seaborn
 import sys
@@ -30,38 +29,34 @@ labels = arbor.label_dict(
     {
         "root": "(root)",  # the start of the soma in this morphology is at the root of the cell.
         "stim_site": "(location 0 0.5)",  # site for the stimulus, in the middle of branch 0.
-        "axon_end": '(restrict (terminal) (region "axon"))',  # end of the axon. NB. 'axon' will be added below
-    }
+        "axon_end": '(restrict (terminal) (region "axon"))',
+    }  # end of the axon.
 ).add_swc_tags()  # Finally, add the SWC default labels.
 
-decor = arbor.decor()
-
-# Set initial membrane potential to -55 mV
-decor.set_property(Vm=-55)
-# Use Nernst to calculate reversal potential for calcium.
-decor.set_ion("ca", method=mech("nernst/x=ca"))
-# decor.set_ion('ca', method='nernst/x=ca')
-# hh mechanism on the soma and axon.
-decor.paint('"soma"', arbor.density("hh"))
-decor.paint('"axon"', arbor.density("hh"))
-# pas mechanism the dendrites.
-decor.paint('"dend"', arbor.density("pas"))
-# Increase resistivity on dendrites.
-decor.paint('"dend"', rL=500)
-# Attach stimuli that inject 4 nA current for 1 ms, starting at 3 and 8 ms.
-decor.place('"root"', arbor.iclamp(10, 1, current=5), "iclamp0")
-decor.place('"stim_site"', arbor.iclamp(3, 1, current=0.5), "iclamp1")
-decor.place('"stim_site"', arbor.iclamp(10, 1, current=0.5), "iclamp2")
-decor.place('"stim_site"', arbor.iclamp(8, 1, current=4), "iclamp3")
-# Detect spikes at the soma with a voltage threshold of -10 mV.
-decor.place('"axon_end"', arbor.spike_detector(-10), "detector")
-
-# Create the policy used to discretise the cell into CVs.
-# Use a single CV for the soma, and CVs of maximum length 1 μm elsewhere.
-soma_policy = arbor.cv_policy_single('"soma"')
-dflt_policy = arbor.cv_policy_max_extent(1.0)
-policy = dflt_policy | soma_policy
-decor.discretization(policy)
+decor = (
+    arbor.decor()
+    # Set initial membrane potential to -55 mV
+    .set_property(Vm=-55)
+    # Use Nernst to calculate reversal potential for calcium.
+    .set_ion("ca", method=arbor.mechanism("nernst/x=ca"))
+    # hh mechanism on the soma and axon.
+    .paint('"soma"', arbor.density("hh"))
+    .paint('"axon"', arbor.density("hh"))
+    # pas mechanism the dendrites.
+    .paint('"dend"', arbor.density("pas"))
+    # Increase resistivity on dendrites.
+    .paint('"dend"', rL=500)
+    # Attach stimuli that inject 4 nA current for 1 ms, starting at 3 and 8 ms.
+    .place('"root"', arbor.iclamp(10, 1, current=5), "iclamp0")
+    .place('"stim_site"', arbor.iclamp(3, 1, current=0.5), "iclamp1")
+    .place('"stim_site"', arbor.iclamp(10, 1, current=0.5), "iclamp2")
+    .place('"stim_site"', arbor.iclamp(8, 1, current=4), "iclamp3")
+    # Detect spikes at the soma with a voltage threshold of -10 mV.
+    .place('"axon_end"', arbor.spike_detector(-10), "detector")
+    # Create the policy used to discretise the cell into CVs.
+    # Use a single CV for the soma, and CVs of maximum length 1 μm elsewhere.
+    .discretization('(replace (single (region "soma")) (max-extent 1.0))')
+)
 
 # Combine morphology with region and locset definitions to make a cable cell.
 cell = arbor.cable_cell(morpho, labels, decor)
diff --git a/scripts/mk-include.py b/scripts/mk-include.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb798e678c739525d0e2367196e10350fde4f241
--- /dev/null
+++ b/scripts/mk-include.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+
+# Use this script for convenience when making tutorials
+# for readthedocs/sphinx.
+#
+# Run on a tutorial python file like this
+#
+# ./mk-include tutorial.py prefix
+#
+# the script will extract all comments from the
+# tutorial script starting with
+#
+# # (N)
+#
+# where N is whole number. For each such comment
+# it will print out a literal include block for use
+# in a sphinx tutorial .rst file, eg
+#
+# .. literalinclude:: ../../python/example/single_cell_detailed.py
+#   :language: python
+#   :lines: 98-102
+#
+# The line numbers are chosen such they start at the
+# comment '# (N)' and end just before the next such
+# comment (or the end of file).
+#
+# The prefix argument is added to the basename like this
+#
+# ./mk-include path/to/tutorial.py prefix/of/docs
+#
+# gives blocks like this
+#
+# .. literalinclude:: prefix/of/docs/tutorial.py
+
+import sys
+import re
+from pathlib import Path
+
+fn, pf = map(Path, sys.argv[1:])
+
+hd, tl, bl, em = 0, 0, None, None
+with open(fn) as fd:
+    for ln in fd:
+        tl += 1
+        m = re.match(r"\s*#\s*\(([0-9]+)\).*", ln)
+        if m:
+            if bl:
+                print(
+                    f"""## Block {bl}
+
+.. literalinclude:: {pf}/{fn.name}
+   :language: python
+   :lines: {hd}-{em}
+"""
+                )
+            hd = tl
+            bl = m.group(1)
+        if ln.strip():
+            em = tl
+
+if bl:
+    print(
+        f"""## Block {bl}
+
+.. literalinclude:: {pf}/{fn.name}
+   :language: python
+   :line: {hd}-{tl}
+"""
+    )