diff --git a/arbor/cable_cell.cpp b/arbor/cable_cell.cpp
index f90e5bffdb92a28106aa21f279e98ec9662f5e19..462c5602bebc1e42ab44ccfe4a71e2adfb302be0 100644
--- a/arbor/cable_cell.cpp
+++ b/arbor/cable_cell.cpp
@@ -103,6 +103,10 @@ struct cable_cell_impl {
             }
         }
     }
+
+    mlocation_list locations(const locset& l) const {
+        return thingify(l, provider);
+    }
 };
 
 using impl_ptr = std::unique_ptr<cable_cell_impl, void (*)(cable_cell_impl*)>;
@@ -133,6 +137,10 @@ const mprovider& cable_cell::provider() const {
     return impl_->provider;
 }
 
+mlocation_list cable_cell::locations(const locset& l) const {
+    return impl_->locations(l);
+}
+
 const cable_cell_location_map& cable_cell::location_assignments() const {
     return impl_->location_map;
 }
diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp
index dc742b5a2ea7d36b7445afa36ad66208d8a96938..e268062a58d0d358e47cc2906228ec5c59e102e5 100644
--- a/arbor/include/arbor/cable_cell.hpp
+++ b/arbor/include/arbor/cable_cell.hpp
@@ -178,6 +178,9 @@ public:
         return location_assignments().get<i_clamp>();
     }
 
+    // Access to a concrete list of locations for a locset.
+    mlocation_list locations(const locset&) const;
+
     // Generic access to painted and placed items.
     const cable_cell_region_map& region_assignments() const;
     const cable_cell_location_map& location_assignments() const;
diff --git a/python/cells.cpp b/python/cells.cpp
index f9807142e457c19e57b862cf82fdd381514f4f8a..10d98efedbf369ae07d774f87f1370a2a825e22a 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -525,6 +525,10 @@ void register_cells(pybind11::module& m) {
             },
             "locations"_a, "detector"_a,
             "Add a voltage spike detector at each location in locations.")
+        // Get locations associated with a locset label.
+        .def("locations",
+            [](arb::cable_cell& c, const char* label) {return c.locations(label);},
+            "label"_a, "The locations of the cell morphology for a locset label.")
         // Discretization control.
         .def("compartments_on_samples",
             [](arb::cable_cell& c) {c.default_parameters.discretization = arb::cv_policy_every_sample{};},
diff --git a/python/example/ring.py b/python/example/ring.py
index 94136094a93e0acc72a124383ce20cfe75439462..ee58e1e9c64e42d2a28e2454278a7eb27c2e843f 100644
--- a/python/example/ring.py
+++ b/python/example/ring.py
@@ -92,7 +92,7 @@ class ring_recipe (arbor.recipe):
         loc = arbor.location(0, 0) # at the soma
         return arbor.cable_probe('voltage', id, loc)
 
-context = arbor.context(threads=4, gpu_id=None)
+context = arbor.context(threads=12, gpu_id=None)
 print(context)
 
 meters = arbor.meter_manager()
@@ -128,7 +128,7 @@ spike_recorder = arbor.attach_spike_recorder(sim)
 # Sample rate of 10 sample every ms.
 samplers = [arbor.attach_sampler(sim, 0.1, arbor.cell_member(gid,0)) for gid in range(ncells)]
 
-tfinal=1
+tfinal=100
 sim.run(tfinal)
 print(f'{sim} finished')
 
@@ -147,7 +147,7 @@ fig, ax = plt.subplots()
 for gid in range(ncells):
     times = [s.time  for s in samplers[gid].samples(arbor.cell_member(gid,0))]
     volts = [s.value for s in samplers[gid].samples(arbor.cell_member(gid,0))]
-    ax.plot(times, volts, '.')
+    ax.plot(times, volts)
 
 legends = ['cell {}'.format(gid) for gid in range(ncells)]
 ax.legend(legends)
diff --git a/python/example/single_cell_builder.py b/python/example/single_cell_builder.py
index 7c7cd7a2d03ec79d87cba51870a5f5b62076af67..486b328a248e1a593b2117ab30f0325f2bd2f3b8 100644
--- a/python/example/single_cell_builder.py
+++ b/python/example/single_cell_builder.py
@@ -43,6 +43,8 @@ b.add_label('dend', '(join (region "dendn") (region "dendx"))')
 b.add_label('stim_site', '(location 2 0.5)')
 # The root of the tree (equivalent to '(location 0 0)')
 b.add_label('root', '(root)')
+# The tips of the dendrites (3 locations at b4, b3, b5).
+b.add_label('dtips', '(terminal)')
 
 # Extract the cable cell from the builder.
 cell = b.build()
@@ -63,13 +65,12 @@ cell.place('stim_site', arbor.iclamp( 80, 2, 0.8))
 # Add a spike detector with threshold of -10 mV.
 cell.place('root', arbor.spike_detector(-10))
 
-# make single cell model
+# Make single cell model.
 m = arbor.single_cell_model(cell)
 
 # Attach voltage probes, sampling at 10 kHz.
-m.probe('voltage', loc(0,0), 10000) # at the soma
-m.probe('voltage', loc(3,1), 10000) # at the end of branch 3
-m.probe('voltage', loc(4,1), 10000) # at the end of branch 4
+m.probe('voltage', loc(0,0), 10000) # at the soma.
+m.probe('voltage', 'dtips',   10000) # at the tips of the dendrites.
 
 # Run simulation for 100 ms of simulated activity.
 tfinal=100
diff --git a/python/example/single_cell_swc.py b/python/example/single_cell_swc.py
index 951f502f311a90bcd5f9db3f35a79277c69770db..4cdbb9c4e967cf94326517f7dc4d76d7cc464a7e 100644
--- a/python/example/single_cell_swc.py
+++ b/python/example/single_cell_swc.py
@@ -42,7 +42,7 @@ cell.compartments_on_samples()
 m = arbor.single_cell_model(cell)
 
 # Attach voltage probes that sample at 50 kHz.
-m.probe('voltage', where=loc(0,0),  frequency=50000)
+m.probe('voltage', where='root',  frequency=50000)
 m.probe('voltage', where=loc(2,1),  frequency=50000)
 m.probe('voltage', where=loc(4,1),  frequency=50000)
 m.probe('voltage', where=loc(30,1), frequency=50000)
diff --git a/python/morphology.cpp b/python/morphology.cpp
index 0e503bf52701f225b0530d2ec1312b1a2f7d0a84..6f99190189d3756e588e03796e5e82f916bbd41b 100644
--- a/python/morphology.cpp
+++ b/python/morphology.cpp
@@ -41,13 +41,9 @@ void register_morphology(pybind11::module& m) {
         .def_readonly("position", &arb::mlocation::pos,
             "The relative position on the branch (∈ [0.,1.], where 0. means proximal and 1. distal).")
         .def("__str__",
-            [](arb::mlocation l) {
-                return util::pprintf("(location {} {})", l.branch, l.pos);
-            })
+            [](arb::mlocation l) { return util::pprintf("(location {} {})", l.branch, l.pos); })
         .def("__repr__",
-            [](arb::mlocation l) {
-                return util::pprintf("<arbor.location: branch {}, position {}>", l.branch, l.pos);
-            });
+            [](arb::mlocation l) { return util::pprintf("(location {} {})", l.branch, l.pos); });
 
     // arb::mpoint
     pybind11::class_<arb::mpoint> mpoint(m, "mpoint");
diff --git a/python/single_cell_model.cpp b/python/single_cell_model.cpp
index 6d5084c47c558ee5e266eaeb9ad269dd0a408c51..29d5df5d8b9e2b29796c26902aace1b8c7c4342c 100644
--- a/python/single_cell_model.cpp
+++ b/python/single_cell_model.cpp
@@ -157,7 +157,10 @@ public:
 
     // example use:
     //      m.probe('voltage', arbor.location(2,0.5))
-    void probe(const std::string& what, const arb::mlocation& where, double frequency) {
+    //      m.probe('voltage', '(location 2 0.5)')
+    //      m.probe('voltage', 'term')
+
+    void probe(const std::string& what, const arb::locset& where, double frequency) {
         if (what != "voltage") {
             throw pyarb_error(
                 util::pprintf("{} does not name a valid variable to trace (currently only 'voltage' is supported)", what));
@@ -166,11 +169,9 @@ public:
             throw pyarb_error(
                 util::pprintf("sampling frequency is not greater than zero", what));
         }
-        if (where.branch>=cell_.morphology().num_branches()) {
-            throw pyarb_error(
-                util::pprintf("invalid location", what));
+        for (auto& l: cell_.locations(where)) {
+            probes_.push_back({l, frequency});
         }
-        probes_.push_back({where, frequency});
     }
 
     void add_ion(const std::string& ion, double valence, double int_con, double ext_con, double rev_pot) {
@@ -238,7 +239,17 @@ void register_single_cell(pybind11::module& m) {
         .def(pybind11::init<arb::cable_cell>(),
             "cell"_a, "Initialise a single cell model for a cable cell.")
         .def("run", &single_cell_model::run, "tfinal"_a, "Run model from t=0 to t=tfinal ms.")
-        .def("probe", &single_cell_model::probe,
+        .def("probe",
+            [](single_cell_model& m, const char* what, const char* where, double frequency) {
+                m.probe(what, where, frequency);},
+            "what"_a, "where"_a, "frequency"_a,
+            "Sample a variable on the cell.\n"
+            " what:      Name of the variable to record (currently only 'voltage').\n"
+            " where:     Location on cell morphology at which to sample the variable.\n"
+            " frequency: The target frequency at which to sample [Hz].")
+        .def("probe",
+            [](single_cell_model& m, const char* what, const arb::mlocation& where, double frequency) {
+                m.probe(what, where, frequency);},
             "what"_a, "where"_a, "frequency"_a,
             "Sample a variable on the cell.\n"
             " what:      Name of the variable to record (currently only 'voltage').\n"