diff --git a/python/dune/gdt/CMakeLists.txt b/python/dune/gdt/CMakeLists.txt
index 30b7a4f39bea8ac2961c687993c045e20327bb5a..4fc20402684de1ea2040cfe86e2f2fcf43b5d108 100644
--- a/python/dune/gdt/CMakeLists.txt
+++ b/python/dune/gdt/CMakeLists.txt
@@ -13,7 +13,13 @@
 
 file(GLOB_RECURSE header "*.hh")
 
-dune_pybindxi_add_module(_discretefunction_discretefunction EXCLUDE_FROM_ALL ${header}
+dune_pybindxi_add_module(_discretefunction_bochner
+                         EXCLUDE_FROM_ALL
+                         ${header}
+                         discretefunction/bochner.cc)
+dune_pybindxi_add_module(_discretefunction_discretefunction
+                         EXCLUDE_FROM_ALL
+                         ${header}
                          discretefunction/discretefunction.cc)
 dune_pybindxi_add_module(_discretefunction_dof_vector EXCLUDE_FROM_ALL ${header} discretefunction/dof-vector.cc)
 dune_pybindxi_add_module(_functionals_interfaces_common EXCLUDE_FROM_ALL ${header} functionals/interfaces_common.cc)
diff --git a/python/dune/gdt/__init__.py b/python/dune/gdt/__init__.py
index 0258c3fd4c9017287359f50715c761a691925233..31c5c16e873633418223a1bc4870e0e67e8c6838 100644
--- a/python/dune/gdt/__init__.py
+++ b/python/dune/gdt/__init__.py
@@ -16,6 +16,7 @@ from dune.xt import guarded_import
 from dune.xt.common.vtk.plot import plot
 
 for mod_name in (     # order should not matter!
+        '_discretefunction_bochner',
         '_discretefunction_discretefunction',
         '_discretefunction_dof_vector',
         '_functionals_interfaces_common',
diff --git a/python/dune/gdt/discretefunction/bochner.cc b/python/dune/gdt/discretefunction/bochner.cc
new file mode 100644
index 0000000000000000000000000000000000000000..f17b2021047f2ac511e02ae7bf7c2398cb854b42
--- /dev/null
+++ b/python/dune/gdt/discretefunction/bochner.cc
@@ -0,0 +1,239 @@
+// This file is part of the dune-gdt project:
+//   https://github.com/dune-community/dune-gdt
+// Copyright 2010-2018 dune-gdt developers and contributors. All rights reserved.
+// License: Dual licensed as BSD 2-Clause License (http://opensource.org/licenses/BSD-2-Clause)
+//      or  GPL-2.0+ (http://opensource.org/licenses/gpl-license)
+//          with "runtime exception" (http://www.dune-project.org/license.html)
+// Authors:
+//   Felix Schindler (2021)
+
+#include "config.h"
+
+#include <dune/pybindxi/pybind11.h>
+
+#include <dune/xt/grid/grids.hh>
+#include <python/dune/xt/la/traits.hh>
+#include <python/dune/xt/grid/grids.bindings.hh>
+
+#include <dune/xt/common/python.hh>
+#include <dune/xt/common/string.hh>
+#include <dune/xt/functions/interfaces/grid-function.hh>
+#include <dune/xt/grid/type_traits.hh>
+#include <dune/xt/la/container.hh>
+#include <python/dune/xt/common/exceptions.bindings.hh>
+#include <python/dune/xt/functions/interfaces/grid-function.hh>
+#include <python/dune/xt/grid/grids.bindings.hh>
+#include <python/dune/xt/grid/traits.hh>
+#include <python/dune/xt/la/traits.hh>
+#include <python/dune/xt/la/container.bindings.hh>
+
+#include <dune/gdt/discretefunction/bochner.hh>
+
+namespace Dune {
+namespace GDT {
+namespace bindings {
+
+
+template <class V, class MatchingVectorTag, class GV, size_t r = 1, size_t rC = 1, class R = double>
+class DiscreteBochnerFunction
+{
+  using type = GDT::DiscreteBochnerFunction<V, GV, r, rC, R>;
+  using G = typename GV::Grid;
+  using BS = BochnerSpace<GV, r, rC, R>;
+
+public:
+  using bound_type = pybind11::class_<type>;
+
+  static V& get_vec_ref(pybind11::handle list_element)
+  {
+    try {
+      return list_element.cast<V&>();
+    } catch (...) {
+    }
+    // if we came that far the above did not work, try
+    try {
+      return list_element.attr("impl").cast<V&>();
+    } catch (...) {
+    }
+    // if we came that far the above did not work, give up
+    DUNE_THROW(XT::Common::Exceptions::wrong_input_given,
+               "Could convert Python object of type [1] to C++ type [2] when trying to convert list_of_vectors:"
+                   << "\n\n"
+                   << "  - [1]: " << pybind11::str(list_element.get_type()).cast<std::string>() << "\n"
+                   << "  - [2]: " << XT::Common::Typename<V>::value() << "&");
+  } // ... get_vec_ref(...)
+
+  static bound_type bind(pybind11::module& m,
+                         const std::string& layer_id = "",
+                         const std::string& grid_id = XT::Grid::bindings::grid_name<G>::value(),
+                         const std::string& class_id = "discrete_bochner_function")
+  {
+    namespace py = pybind11;
+    using namespace pybind11::literals;
+
+    std::string class_name = class_id + "_" + grid_id;
+    if (!layer_id.empty())
+      class_name += "_" + layer_id;
+    if (r > 1)
+      class_name += "_to_" + XT::Common::to_string(r) + "d";
+    if (rC > 1)
+      class_name += "x" + XT::Common::to_string(rC) + "d";
+    class_name += "_" + XT::LA::bindings::container_name<V>::value();
+    const auto ClassName = XT::Common::to_camel_case(class_name);
+    const std::string default_name = "dune.gdt.discretebochnerfunction";
+    bound_type c(m, ClassName.c_str(), ClassName.c_str());
+
+    c.def_property_readonly("space", &type::space);
+    c.def_property_readonly("name", &type::name);
+
+    c.def("evaluate", &type::evaluate, "time"_a);
+    c.def(
+        "visualize",
+        [](type& self, const std::string& filename_prefix) {
+          return self.visualize(filename_prefix, VTK::appendedraw);
+        },
+        "filename_prefix"_a);
+
+    m.def(
+        XT::Common::to_camel_case(class_id).c_str(),
+        [](const BS& bochner_space, py::list list_of_vectors, const MatchingVectorTag&, const std::string& name) {
+          // try to interprete list_of_vectors as a ListVectorArray of correct length
+          DUNE_THROW_IF(list_of_vectors.size() != bochner_space.temporal_space().mapper().size(),
+                        XT::Common::Exceptions::wrong_input_given,
+                        "list_of_vectors does not match bochner_space:"
+                            << "\n"
+                            << "  bochner_space.spatial_space().mapper().size() = "
+                            << bochner_space.spatial_space().mapper().size() << "\n"
+                            << "  list_of_vectors.size() = " << list_of_vectors.size());
+          auto vector_array =
+              std::make_unique<XT::LA::ListVectorArray<V>>(/*dim=*/bochner_space.spatial_space().mapper().size(),
+                                                           /*length=*/0,
+                                                           /*reserve=*/bochner_space.temporal_space().mapper().size());
+          size_t counter = 0;
+          auto time_points = bochner_space.time_points();
+          for (py::handle list_element : list_of_vectors) {
+            auto& vec = get_vec_ref(list_element);
+            vector_array->append(vec, {{"_t", {time_points[counter]}}});
+          }
+          return new type(bochner_space, vector_array.release(), name);
+        },
+        "space"_a,
+        "list_of_vectors"_a,
+        "vector_type"_a,
+        "name"_a = default_name,
+        py::keep_alive<0, 1>(),
+        py::keep_alive<0, 2>());
+    if (std::is_same<MatchingVectorTag, XT::LA::bindings::Istl>::value)
+      m.def(
+          XT::Common::to_camel_case(class_id).c_str(),
+          [](const BS& bochner_space, py::list list_of_vectors, const std::string& name, const MatchingVectorTag&) {
+            // try to interprete list_of_vectors as a ListVectorArray of correct length
+            DUNE_THROW_IF(list_of_vectors.size() != bochner_space.temporal_space().mapper().size(),
+                          XT::Common::Exceptions::wrong_input_given,
+                          "list_of_vectors does not match bochner_space:"
+                              << "\n"
+                              << "  bochner_space.spatial_space().mapper().size() = "
+                              << bochner_space.spatial_space().mapper().size() << "\n"
+                              << "  list_of_vectors.size() = " << list_of_vectors.size());
+            auto vector_array = std::make_unique<XT::LA::ListVectorArray<V>>(
+                /*dim=*/bochner_space.spatial_space().mapper().size(),
+                /*length=*/0,
+                /*reserve=*/bochner_space.temporal_space().mapper().size());
+            size_t counter = 0;
+            auto time_points = bochner_space.time_points();
+            for (py::handle list_element : list_of_vectors) {
+              auto& vec = get_vec_ref(list_element);
+              vector_array->append(vec, {{"_t", {time_points[counter]}}});
+            }
+            return new type(bochner_space, vector_array.release(), name);
+          },
+          "space"_a,
+          "list_of_vectors"_a,
+          "name"_a = default_name,
+          "vector_type"_a = XT::LA::bindings::Istl(),
+          py::keep_alive<0, 1>(),
+          py::keep_alive<0, 2>());
+
+    m.def(
+        XT::Common::to_camel_case(class_id).c_str(),
+        [](const BS& space, const MatchingVectorTag&, const std::string& name) {
+          return make_discrete_bochner_function<V>(space, name);
+        },
+        "space"_a,
+        "vector_type"_a,
+        "name"_a = default_name,
+        py::keep_alive<0, 1>());
+    if (std::is_same<MatchingVectorTag, XT::LA::bindings::Istl>::value)
+      m.def(
+          XT::Common::to_camel_case(class_id).c_str(),
+          [](const BS& space, const std::string& name, const MatchingVectorTag&) {
+            return make_discrete_bochner_function<V>(space, name);
+          },
+          "space"_a,
+          "name"_a = default_name,
+          "vector_type"_a = XT::LA::bindings::Istl(),
+          py::keep_alive<0, 1>());
+    return c;
+  } // ... bind(...)
+}; // class DiscreteBochnerFunction
+
+
+} // namespace bindings
+} // namespace GDT
+} // namespace Dune
+
+
+template <class V, class VT, class GridTypes = Dune::XT::Grid::bindings::AvailableGridTypes>
+struct DiscreteBochnerFunction_for_all_grids
+{
+  using G = Dune::XT::Common::tuple_head_t<GridTypes>;
+  using GV = typename G::LeafGridView;
+  static const constexpr size_t d = G::dimension;
+
+  static void bind(pybind11::module& m)
+  {
+    using Dune::GDT::bindings::DiscreteBochnerFunction;
+
+    DiscreteBochnerFunction<V, VT, GV>::bind(m);
+    if (d > 1)
+      DiscreteBochnerFunction<V, VT, GV, d>::bind(m);
+    // add your extra dimensions here
+    // ...
+    DiscreteBochnerFunction_for_all_grids<V, VT, Dune::XT::Common::tuple_tail_t<GridTypes>>::bind(m);
+  }
+};
+
+template <class V, class VT>
+struct DiscreteBochnerFunction_for_all_grids<V, VT, Dune::XT::Common::tuple_null_type>
+{
+  static void bind(pybind11::module& /*m*/) {}
+};
+
+
+PYBIND11_MODULE(_discretefunction_bochner, m)
+{
+  namespace py = pybind11;
+  using namespace Dune;
+  using namespace Dune::XT;
+  using namespace Dune::GDT;
+
+  py::module::import("dune.xt.common");
+  py::module::import("dune.xt.la");
+  py::module::import("dune.xt.grid");
+  py::module::import("dune.xt.functions");
+
+  py::module::import("dune.gdt._discretefunction_discretefunction");
+  py::module::import("dune.gdt._spaces_bochner");
+
+  DiscreteBochnerFunction_for_all_grids<LA::CommonDenseVector<double>,
+                                        LA::bindings::Common,
+                                        XT::Grid::bindings::AvailableGridTypes>::bind(m);
+#if HAVE_EIGEN
+  DiscreteBochnerFunction_for_all_grids<LA::EigenDenseVector<double>,
+                                        LA::bindings::Eigen,
+                                        XT::Grid::bindings::AvailableGridTypes>::bind(m);
+#endif
+  DiscreteBochnerFunction_for_all_grids<LA::IstlDenseVector<double>,
+                                        LA::bindings::Istl,
+                                        XT::Grid::bindings::AvailableGridTypes>::bind(m);
+}