diff --git a/Applications/CLI/CMakeLists.txt b/Applications/CLI/CMakeLists.txt
index f4e4a94213a15deeb51a0f63eb7f6b1a620a471d..74c05b7a45e9a407333d00bd035eefbc70145034 100644
--- a/Applications/CLI/CMakeLists.txt
+++ b/Applications/CLI/CMakeLists.txt
@@ -39,7 +39,7 @@ if(OGS_USE_PYTHON)
     target_link_libraries(ogs_embedded_python PUBLIC pybind11::embed)
     target_compile_definitions(ogs_embedded_python PUBLIC OGS_USE_PYTHON)
     target_link_libraries(ogs_embedded_python PRIVATE
-        ProcessLibBoundaryConditionPythonModule)
+        ProcessLibBoundaryConditionPythonModule ProcessLibSourceTermPythonModule)
 
     target_link_libraries(ogs PRIVATE ogs_embedded_python)
 
diff --git a/ProcessLib/CMakeLists.txt b/ProcessLib/CMakeLists.txt
index 61f2ee6746a5e216943913aa368515cdf45e6308..162ba4aacc5185ee79e8b4ad4f3c706984316932 100644
--- a/ProcessLib/CMakeLists.txt
+++ b/ProcessLib/CMakeLists.txt
@@ -32,6 +32,10 @@ if(OGS_USE_PYTHON)
     add_subdirectory(BoundaryCondition/Python)
     target_link_libraries(ProcessLib
         PUBLIC ProcessLibBoundaryConditionPython)
+
+    add_subdirectory(SourceTerms/Python)
+    target_link_libraries(ProcessLib
+        PUBLIC ProcessLibSourceTermPython)
 endif()
 
 if(OGS_INSITU)
diff --git a/ProcessLib/SourceTerms/CreateSourceTerm.cpp b/ProcessLib/SourceTerms/CreateSourceTerm.cpp
index 4e50e55f62343ea6eed0a69cb418af8ae9893279..820dff5831b76540cf2ac1b54d0d8a35001fcb9e 100644
--- a/ProcessLib/SourceTerms/CreateSourceTerm.cpp
+++ b/ProcessLib/SourceTerms/CreateSourceTerm.cpp
@@ -11,6 +11,9 @@
 
 #include "CreateNodalSourceTerm.h"
 #include "CreateVolumetricSourceTerm.h"
+#ifdef OGS_USE_PYTHON
+#include "Python/CreatePythonSourceTerm.h"
+#endif
 #include "SourceTerm.h"
 #include "SourceTermConfig.h"
 
@@ -41,6 +44,18 @@ std::unique_ptr<SourceTerm> createSourceTerm(
             *config.component_id);
     }
 
+    if (type == "Python")
+    {
+#ifdef OGS_USE_PYTHON
+        return ProcessLib::createPythonSourceTerm(
+            config.config, config.mesh, dof_table, mesh.getID(), variable_id,
+            *config.component_id, integration_order, shapefunction_order,
+            mesh.getDimension());
+#else
+        OGS_FATAL("OpenGeoSys has not been built with Python support.");
+#endif
+    }
+
     OGS_FATAL("Unknown source term type: `%s'.", type.c_str());
 }
 }  // namespace ProcessLib
diff --git a/ProcessLib/SourceTerms/Python/CMakeLists.txt b/ProcessLib/SourceTerms/Python/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8ecddaea2e59a5715ef26188d8e27f4795415512
--- /dev/null
+++ b/ProcessLib/SourceTerms/Python/CMakeLists.txt
@@ -0,0 +1,32 @@
+add_library(ProcessLibSourceTermPython
+    CreatePythonSourceTerm.cpp
+    CreatePythonSourceTerm.h
+    PythonSourceTerm.cpp
+    PythonSourceTerm.h
+    PythonSourceTermLocalAssembler.h
+    PythonSourceTermPythonSideInterface.h)
+if(BUILD_SHARED_LIBS)
+    install(TARGETS ProcessLibSourceTermPython
+        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
+endif()
+
+target_compile_definitions(ProcessLibSourceTermPython
+    PUBLIC OGS_USE_PYTHON)
+
+target_link_libraries(ProcessLibSourceTermPython
+    PUBLIC BaseLib MathLib MeshLib NumLib logog
+    PRIVATE pybind11::pybind11)
+
+# For the embedded Python module
+add_library(ProcessLibSourceTermPythonModule
+    PythonSourceTermModule.cpp
+    PythonSourceTermModule.h)
+if(BUILD_SHARED_LIBS)
+    install(TARGETS ProcessLibSourceTermPythonModule
+        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
+endif()
+
+target_link_libraries(ProcessLibSourceTermPythonModule
+    PUBLIC
+    ProcessLibSourceTermPython
+    pybind11::pybind11)
diff --git a/ProcessLib/SourceTerms/Python/CreatePythonSourceTerm.cpp b/ProcessLib/SourceTerms/Python/CreatePythonSourceTerm.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..491107abba3fc8a1e1e49f7dcd3ef9e62724b896
--- /dev/null
+++ b/ProcessLib/SourceTerms/Python/CreatePythonSourceTerm.cpp
@@ -0,0 +1,87 @@
+/**
+ * \copyright
+ * Copyright (c) 2012-2018, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#include "CreatePythonSourceTerm.h"
+
+#include <pybind11/pybind11.h>
+
+#include "BaseLib/ConfigTree.h"
+#include "MeshLib/Mesh.h"
+#include "NumLib/DOF/LocalToGlobalIndexMap.h"
+#include "ProcessLib/SourceTerms/SourceTerm.h"
+#include "PythonSourceTerm.h"
+
+namespace ProcessLib
+{
+std::unique_ptr<SourceTerm> createPythonSourceTerm(
+    BaseLib::ConfigTree const& config, MeshLib::Mesh const& source_term_mesh,
+    NumLib::LocalToGlobalIndexMap const& dof_table,
+    std::size_t const bulk_mesh_id, int const variable_id,
+    int const component_id, unsigned const integration_order,
+    unsigned const shapefunction_order, unsigned const global_dim)
+{
+    DBUG("Constructing PythonSourceTerm from config.");
+    //! \ogs_file_param{prj__process_variables__process_variable__source_term__source_term__type}
+    config.checkConfigParameter("type", "Python");
+
+    auto const source_term_object =
+        //! \ogs_file_param{prj__process_variables__process_variable__source_term__source_term__Python__source_term_object}
+        config.getConfigParameter<std::string>("source_term_object");
+    //! \ogs_file_param{prj__process_variables__process_variable__source_term__source_term__Python__flush_stdout}
+    auto const flush_stdout = config.getConfigParameter("flush_stdout", false);
+
+    // Evaluate Python code in scope of main module
+    pybind11::object scope =
+        pybind11::module::import("__main__").attr("__dict__");
+
+    if (!scope.contains(source_term_object))
+        OGS_FATAL(
+            "Function `%s' is not defined in the python script file, or there "
+            "was no python script file specified.",
+            source_term_object.c_str());
+
+    auto* source_term = scope[source_term_object.c_str()]
+                            .cast<ProcessLib::SourceTerms::Python::
+                                      PythonSourceTermPythonSideInterface*>();
+
+    if (variable_id >= static_cast<int>(dof_table.getNumberOfVariables()) ||
+        component_id >= dof_table.getNumberOfVariableComponents(variable_id))
+    {
+        OGS_FATAL(
+            "Variable id or component id too high. Actual values: (%d, %d), "
+            "maximum values: (%d, %d).",
+            variable_id, component_id, dof_table.getNumberOfVariables(),
+            dof_table.getNumberOfVariableComponents(variable_id));
+    }
+
+    // In case of partitioned mesh the source_term could be empty, i.e. there is
+    // no source_term condition.
+#ifdef USE_PETSC
+    // This can be extracted to createSourceTerm() but then the config
+    // parameters are not read and will cause an error.
+    // TODO (naumov): Add a function to ConfigTree for skipping the tags of the
+    // subtree and move the code up in createSourceTerm().
+    if (source_term_mesh.getDimension() == 0 &&
+        source_term_mesh.getNumberOfNodes() == 0 &&
+        source_term_mesh.getNumberOfElements() == 0)
+    {
+        return nullptr;
+    }
+#endif  // USE_PETSC
+
+    return std::make_unique<ProcessLib::SourceTerms::Python::PythonSourceTerm>(
+        dof_table,
+        ProcessLib::SourceTerms::Python::PythonSourceTermData{
+            source_term, dof_table, bulk_mesh_id,
+            dof_table.getGlobalComponent(variable_id, component_id),
+            source_term_mesh},
+        integration_order, shapefunction_order, global_dim, flush_stdout);
+}
+
+}  // namespace ProcessLib
diff --git a/ProcessLib/SourceTerms/Python/CreatePythonSourceTerm.h b/ProcessLib/SourceTerms/Python/CreatePythonSourceTerm.h
new file mode 100644
index 0000000000000000000000000000000000000000..b7b2d6a4b7b328ba235a38ef7f6e12714b428f1a
--- /dev/null
+++ b/ProcessLib/SourceTerms/Python/CreatePythonSourceTerm.h
@@ -0,0 +1,38 @@
+/**
+ * \copyright
+ * Copyright (c) 2012-2018, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#include <memory>
+
+namespace BaseLib
+{
+class ConfigTree;
+}
+
+namespace MeshLib
+{
+class Mesh;
+}
+
+namespace NumLib
+{
+class LocalToGlobalIndexMap;
+}
+
+namespace ProcessLib
+{
+class SourceTerm;
+
+std::unique_ptr<SourceTerm> createPythonSourceTerm(
+    BaseLib::ConfigTree const& config, MeshLib::Mesh const& source_term_mesh,
+    NumLib::LocalToGlobalIndexMap const& dof_table,
+    std::size_t const bulk_mesh_id, int const variable_id,
+    int const component_id, unsigned const integration_order,
+    unsigned const shapefunction_order, unsigned const global_dim);
+
+}  // namespace ProcessLib
diff --git a/ProcessLib/SourceTerms/Python/PythonSourceTerm.cpp b/ProcessLib/SourceTerms/Python/PythonSourceTerm.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1497c70e53117f1f5a4595d4334b3bd5654ec730
--- /dev/null
+++ b/ProcessLib/SourceTerms/Python/PythonSourceTerm.cpp
@@ -0,0 +1,98 @@
+/**
+ * \copyright
+ * Copyright (c) 2012-2018, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#include "PythonSourceTerm.h"
+
+#include <pybind11/pybind11.h>
+#include <iostream>
+
+#include "MeshLib/MeshSearch/NodeSearch.h"
+#include "ProcessLib/Utils/CreateLocalAssemblers.h"
+#include "ProcessLib/Utils/ProcessUtils.h"
+#include "PythonSourceTermLocalAssembler.h"
+
+namespace
+{
+//! Optionally flushes the standard output upon creation and destruction.
+//! Can be used to improve the debug output readability when printing debug
+//! messages both from OGS and from Python.
+class FlushStdoutGuard final
+{
+public:
+    //! Optionally flushes C++ stdout before running Python code.
+    explicit FlushStdoutGuard(bool const flush) : _flush(flush)
+    {
+        if (!flush)
+            return;
+
+        LOGOG_COUT << std::flush;
+    }
+
+    //! Optionally flushes Python's stdout after running Python code.
+    ~FlushStdoutGuard()
+    {
+        if (!_flush)
+            return;
+
+        using namespace pybind11::literals;
+        pybind11::print("end"_a = "", "flush"_a = true);
+    }
+
+private:
+    //! To flush or not to flush.
+    const bool _flush;
+};
+}  // anonymous namespace
+
+namespace ProcessLib
+{
+namespace SourceTerms
+{
+namespace Python
+{
+PythonSourceTerm::PythonSourceTerm(
+    NumLib::LocalToGlobalIndexMap const& source_term_dof_table,
+    PythonSourceTermData&& source_term_data, unsigned const integration_order,
+    unsigned const shapefunction_order, unsigned const global_dim,
+    bool const flush_stdout)
+    : SourceTerm(source_term_dof_table),
+      _source_term_data(std::move(source_term_data)),
+      _flush_stdout(flush_stdout)
+{
+    std::vector<MeshLib::Node*> const& source_term_nodes =
+        _source_term_data.source_term_mesh.getNodes();
+    MeshLib::MeshSubset source_term_mesh_subset(
+        _source_term_data.source_term_mesh, source_term_nodes);
+
+    // Create local DOF table from the source term mesh subset for the given
+    // variable and component id.
+    _dof_table_source_term =
+        _source_term_data.dof_table_bulk.deriveBoundaryConstrainedMap(
+            std::move(source_term_mesh_subset));
+
+    createLocalAssemblers<PythonSourceTermLocalAssembler>(
+        global_dim, _source_term_data.source_term_mesh.getElements(),
+        *_dof_table_source_term, shapefunction_order, _local_assemblers,
+        _source_term_data.source_term_mesh.isAxiallySymmetric(),
+        integration_order, _source_term_data);
+}
+
+void PythonSourceTerm::integrate(const double t, const GlobalVector& x,
+                                 GlobalVector& b, GlobalMatrix* Jac) const
+{
+    FlushStdoutGuard guard(_flush_stdout);
+
+    GlobalExecutor::executeMemberOnDereferenced(
+        &PythonSourceTermLocalAssemblerInterface::assemble, _local_assemblers,
+        *_dof_table_source_term, t, x, b, Jac);
+}
+
+}  // namespace Python
+}  // namespace SourceTerms
+}  // namespace ProcessLib
diff --git a/ProcessLib/SourceTerms/Python/PythonSourceTerm.h b/ProcessLib/SourceTerms/Python/PythonSourceTerm.h
new file mode 100644
index 0000000000000000000000000000000000000000..512c260d93f6ad7f2cbc75bd7a8723436787c110
--- /dev/null
+++ b/ProcessLib/SourceTerms/Python/PythonSourceTerm.h
@@ -0,0 +1,77 @@
+/**
+ * \copyright
+ * Copyright (c) 2012-2018, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#pragma once
+
+#include "NumLib/DOF/LocalToGlobalIndexMap.h"
+#include "NumLib/IndexValueVector.h"
+#include "ProcessLib/SourceTerms/SourceTerm.h"
+
+#include "PythonSourceTermLocalAssemblerInterface.h"
+#include "PythonSourceTermPythonSideInterface.h"
+
+namespace ProcessLib
+{
+namespace SourceTerms
+{
+namespace Python
+{
+//! Groups data used by source terms, in particular by the local assemblers.
+struct PythonSourceTermData final
+{
+    //! Python object computing source term values.
+    PythonSourceTermPythonSideInterface* source_term_object;
+
+    //! DOF table of the entire domain.
+    NumLib::LocalToGlobalIndexMap const& dof_table_bulk;
+
+    //! Mesh ID of the entire domain.
+    std::size_t const bulk_mesh_id;
+
+    //! Global component ID of the (variable, component) to which this source
+    //! term is applied.
+    int const global_component_id;
+
+    //! The source term mesh, i.e., the (sub-) domain of this source term.
+    const MeshLib::Mesh& source_term_mesh;
+};
+
+//! A source term whose values are computed by a Python script.
+class PythonSourceTerm final : public ProcessLib::SourceTerm
+{
+public:
+    explicit PythonSourceTerm(
+        NumLib::LocalToGlobalIndexMap const& source_term_dof_table,
+        PythonSourceTermData&& source_term_data,
+        unsigned const integration_order, unsigned const shapefunction_order,
+        unsigned const global_dim, bool const flush_stdout);
+
+    void integrate(const double t, GlobalVector const& x, GlobalVector& b,
+                   GlobalMatrix* jac) const override;
+
+private:
+    //! Auxiliary data.
+    PythonSourceTermData _source_term_data;
+
+    //! Local dof table for the boundary mesh.
+    std::unique_ptr<NumLib::LocalToGlobalIndexMap> _dof_table_source_term;
+
+    //! Local assemblers for all elements of the source term mesh.
+    std::vector<std::unique_ptr<PythonSourceTermLocalAssemblerInterface>>
+        _local_assemblers;
+
+    //! Whether or not to flush standard output before and after each call to
+    //! Python code. Ensures right order of output messages and therefore
+    //! simplifies debugging.
+    bool const _flush_stdout;
+};
+
+}  // namespace Python
+}  // namespace SourceTerms
+}  // namespace ProcessLib
diff --git a/ProcessLib/SourceTerms/Python/PythonSourceTermLocalAssembler.h b/ProcessLib/SourceTerms/Python/PythonSourceTermLocalAssembler.h
new file mode 100644
index 0000000000000000000000000000000000000000..9a8039ad6ce99ca501a78ba53069e4eb54b05da3
--- /dev/null
+++ b/ProcessLib/SourceTerms/Python/PythonSourceTermLocalAssembler.h
@@ -0,0 +1,234 @@
+/**
+ * \copyright
+ * Copyright (c) 2012-2018, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#pragma once
+
+#include "PythonSourceTerm.h"
+
+#include "MathLib/LinAlg/Eigen/EigenMapTools.h"
+#include "NumLib/DOF/DOFTableUtil.h"
+#include "NumLib/Fem/FiniteElement/TemplateIsoparametric.h"
+#include "ProcessLib/LocalAssemblerTraits.h"
+#include "ProcessLib/Utils/InitShapeMatrices.h"
+#include "PythonSourceTermLocalAssemblerInterface.h"
+#include "PythonSourceTermPythonSideInterface.h"
+
+namespace ProcessLib
+{
+namespace SourceTerms
+{
+namespace Python
+{
+//! Can be thrown to indicate that a member function is not overridden in a
+//! derived class (in particular, if a Python class inherits from a C++ class).
+struct MethodNotOverriddenInDerivedClassException
+{
+};
+
+template <typename NodalRowVectorType>
+struct IntegrationPointData final
+{
+    IntegrationPointData(NodalRowVectorType const& N_,
+                         double const& integration_weight_)
+        : N(N_), integration_weight(integration_weight_)
+    {
+    }
+
+    NodalRowVectorType const N;
+    double const integration_weight;
+
+    EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
+};
+
+template <typename ShapeFunction, typename IntegrationMethod,
+          unsigned GlobalDim>
+class PythonSourceTermLocalAssembler final
+    : public PythonSourceTermLocalAssemblerInterface
+{
+public:
+    using ShapeMatricesType = ShapeMatrixPolicyType<ShapeFunction, GlobalDim>;
+    using NodalRowVectorType = typename ShapeMatricesType::NodalRowVectorType;
+    using NodalMatrixType = typename ShapeMatricesType::NodalMatrixType;
+
+    explicit PythonSourceTermLocalAssembler(
+        MeshLib::Element const& e,
+        std::size_t const /*local_matrix_size*/,
+        bool is_axially_symmetric,
+        unsigned const integration_order,
+        PythonSourceTermData const& data)
+        : _data(data),
+          _element(e),
+          _is_axially_symmetric(is_axially_symmetric),
+          _integration_method(integration_order)
+    {
+        unsigned const n_integration_points =
+            _integration_method.getNumberOfPoints();
+
+        auto const shape_matrices =
+            initShapeMatrices<ShapeFunction, ShapeMatricesType,
+                              IntegrationMethod, GlobalDim>(
+                _element, _is_axially_symmetric, _integration_method);
+
+        for (unsigned ip = 0; ip < n_integration_points; ip++)
+        {
+            _ip_data.emplace_back(
+                shape_matrices[ip].N,
+                _integration_method.getWeightedPoint(ip).getWeight() *
+                    shape_matrices[ip].integralMeasure *
+                    shape_matrices[ip].detJ);
+        }
+    }
+
+    void assemble(std::size_t const source_term_element_id,
+                  NumLib::LocalToGlobalIndexMap const& dof_table_source_term,
+                  double const t, const GlobalVector& x, GlobalVector& b,
+                  GlobalMatrix* Jac) override
+    {
+        using ShapeMatricesType =
+            ShapeMatrixPolicyType<ShapeFunction, GlobalDim>;
+        using FemType =
+            NumLib::TemplateIsoparametric<ShapeFunction, ShapeMatricesType>;
+        FemType fe(*static_cast<const typename ShapeFunction::MeshElement*>(
+            &_element));
+
+        unsigned const num_integration_points =
+            _integration_method.getNumberOfPoints();
+        auto const num_var = _data.dof_table_bulk.getNumberOfVariables();
+        auto const num_nodes = ShapeFunction::NPOINTS;
+        auto const num_comp_total =
+            _data.dof_table_bulk.getNumberOfComponents();
+
+        auto const& bulk_node_ids_map =
+            *_data.source_term_mesh.getProperties()
+                 .template getPropertyVector<std::size_t>("bulk_node_ids");
+
+        // gather primary variables
+        typename ShapeMatricesType::template MatrixType<ShapeFunction::NPOINTS,
+                                                       Eigen::Dynamic>
+            primary_variables_mat(num_nodes, num_comp_total);
+        for (int var = 0; var < num_var; ++var)
+        {
+            auto const num_comp =
+                _data.dof_table_bulk.getNumberOfVariableComponents(var);
+            for (int comp = 0; comp < num_comp; ++comp)
+            {
+                auto const global_component =
+                    dof_table_source_term.getGlobalComponent(var, comp);
+
+                for (unsigned element_node_id = 0; element_node_id < num_nodes;
+                     ++element_node_id)
+                {
+                    auto const* const node = _element.getNode(element_node_id);
+                    auto const boundary_node_id = node->getID();
+                    auto const bulk_node_id =
+                        bulk_node_ids_map[boundary_node_id];
+                    MeshLib::Location loc{_data.bulk_mesh_id,
+                                          MeshLib::MeshItemType::Node,
+                                          bulk_node_id};
+                    auto const dof_idx =
+                        _data.dof_table_bulk.getGlobalIndex(loc, var, comp);
+                    if (dof_idx == NumLib::MeshComponentMap::nop)
+                    {
+                        // TODO extend Python BC to mixed FEM ansatz functions
+                        OGS_FATAL(
+                            "No d.o.f. found for (node=%d, var=%d, comp=%d).  "
+                            "That might be due to the use of mixed FEM ansatz "
+                            "functions, which is currently not supported by "
+                            "the implementation of Python BCs. That excludes, "
+                            "e.g., the HM process.",
+                            bulk_node_id, var, comp);
+                    }
+                    primary_variables_mat(element_node_id, global_component) =
+                        x[dof_idx];
+                }
+            }
+        }
+
+        NodalRowVectorType local_rhs = Eigen::VectorXd::Zero(num_nodes);
+        NodalMatrixType local_Jac =
+            Eigen::MatrixXd::Zero(num_nodes, num_nodes * num_comp_total);
+        std::vector<double> prim_vars_data(num_comp_total);
+        auto prim_vars = MathLib::toVector(prim_vars_data);
+
+        for (unsigned ip = 0; ip < num_integration_points; ip++)
+        {
+            auto const& ip_data = _ip_data[ip];
+            auto const& N = ip_data.N;
+            auto const& coords = fe.interpolateCoordinates(N);
+            // Assumption: all primary variables have same shape functions.
+            prim_vars = N * primary_variables_mat;
+
+            auto const& w = ip_data.integration_weight;
+            auto const flux_dflux =
+                _data.source_term_object->getFlux(t, coords, prim_vars_data);
+            auto const& flux = flux_dflux.first;
+            auto const& dflux = flux_dflux.second;
+            local_rhs.noalias() += N * (flux * w);
+
+            if (static_cast<int>(dflux.size()) != num_comp_total)
+            {
+                // This strict check is technically mandatory only if a
+                // Jacobian is assembled. However, it is done as a
+                // consistency check also for cases without Jacobian
+                // assembly.
+                OGS_FATAL(
+                    "The Python source term must return the derivative of "
+                    "the flux w.r.t. each primary variable. %d components "
+                    "expected. %d components returned from Python.",
+                    num_comp_total, dflux.size());
+            }
+
+            if (Jac)
+            {
+                for (int comp = 0; comp < num_comp_total; ++comp)
+                {
+                    auto const top = 0;
+                    auto const left = comp * num_nodes;
+                    auto const width = num_nodes;
+                    auto const height = num_nodes;
+                    // The assignement -= takes into account the sign convention
+                    // of 1st-order in time ODE systems in OpenGeoSys.
+                    local_Jac.block(top, left, width, height).noalias() -=
+                        ip_data.N.transpose() * (dflux[comp] * w) * N;
+                }
+            }
+        }
+
+        auto const& indices_specific_component =
+            dof_table_source_term(source_term_element_id,
+                                  _data.global_component_id)
+                .rows;
+        b.add(indices_specific_component, local_rhs);
+
+        if (Jac)
+        {
+            // only assemble a block of the Jacobian, not the whole local matrix
+            auto const indices_all_components = NumLib::getIndices(
+                source_term_element_id, dof_table_source_term);
+            MathLib::RowColumnIndices<GlobalIndexType> rci{
+                indices_specific_component, indices_all_components};
+            Jac->add(rci, local_Jac);
+        }
+    }
+
+private:
+    PythonSourceTermData const& _data;
+    MeshLib::Element const& _element;
+    bool _is_axially_symmetric;
+
+    IntegrationMethod const _integration_method;
+    std::vector<
+        IntegrationPointData<NodalRowVectorType>,
+        Eigen::aligned_allocator<IntegrationPointData<NodalRowVectorType>>>
+        _ip_data;
+};
+
+}  // namespace Python
+}  // namespace SourceTerms
+}  // namespace ProcessLib
diff --git a/ProcessLib/SourceTerms/Python/PythonSourceTermLocalAssemblerInterface.h b/ProcessLib/SourceTerms/Python/PythonSourceTermLocalAssemblerInterface.h
new file mode 100644
index 0000000000000000000000000000000000000000..c006156afab1df39c8bf05df39a5d3ebed8b9151
--- /dev/null
+++ b/ProcessLib/SourceTerms/Python/PythonSourceTermLocalAssemblerInterface.h
@@ -0,0 +1,33 @@
+
+/**
+ * \copyright
+ * Copyright (c) 2012-2018, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#pragma once
+
+namespace ProcessLib
+{
+namespace SourceTerms
+{
+namespace Python
+{
+class PythonSourceTermLocalAssemblerInterface
+{
+public:
+    virtual void assemble(
+        std::size_t const source_term_element_id,
+        NumLib::LocalToGlobalIndexMap const& source_term_dof_table,
+        double const t, const GlobalVector& x, GlobalVector& b,
+        GlobalMatrix* Jac) = 0;
+
+    virtual ~PythonSourceTermLocalAssemblerInterface() = default;
+};
+
+}  // namespace Python
+}  // namespace SourceTerms
+}  // namespace ProcessLib
diff --git a/ProcessLib/SourceTerms/Python/PythonSourceTermModule.cpp b/ProcessLib/SourceTerms/Python/PythonSourceTermModule.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d018837ad6978644cbec25fe0b3e0971b3479236
--- /dev/null
+++ b/ProcessLib/SourceTerms/Python/PythonSourceTermModule.cpp
@@ -0,0 +1,57 @@
+/**
+ * \copyright
+ * Copyright (c) 2012-2018, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#include "PythonSourceTermModule.h"
+
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include "PythonSourceTermPythonSideInterface.h"
+
+namespace ProcessLib
+{
+namespace SourceTerms
+{
+namespace Python
+{
+//! Trampoline class allowing methods of class
+//! PythonSourceTermPythonSideInterface to be overridden on the Python
+//! side. Cf. https://pybind11.readthedocs.io/en/stable/advanced/classes.html
+class PythonSourceTermPythonSideInterfaceTrampoline
+    : public PythonSourceTermPythonSideInterface
+{
+public:
+    using PythonSourceTermPythonSideInterface::
+        PythonSourceTermPythonSideInterface;
+
+    std::pair<double, std::array<double, 3>> getFlux(
+        double t, std::array<double, 3> x,
+        std::vector<double> const& primary_variables) const override
+    {
+        using Ret = std::pair<double, std::array<double, 3>>;
+        PYBIND11_OVERLOAD(Ret, PythonSourceTermPythonSideInterface, getFlux, t,
+                          x, primary_variables);
+    }
+};
+
+void pythonBindSourceTerm(pybind11::module& m)
+{
+    namespace py = pybind11;
+
+    py::class_<PythonSourceTermPythonSideInterface,
+               PythonSourceTermPythonSideInterfaceTrampoline>
+        pybc(m, "SourceTerm");
+
+    pybc.def(py::init());
+
+    pybc.def("getFlux", &PythonSourceTermPythonSideInterface::getFlux);
+}
+
+}  // namespace Python
+}  // namespace SourceTerms
+}  // namespace ProcessLib
diff --git a/ProcessLib/SourceTerms/Python/PythonSourceTermModule.h b/ProcessLib/SourceTerms/Python/PythonSourceTermModule.h
new file mode 100644
index 0000000000000000000000000000000000000000..47b279fc0a34f28c3a400d44c488a6372d36b932
--- /dev/null
+++ b/ProcessLib/SourceTerms/Python/PythonSourceTermModule.h
@@ -0,0 +1,25 @@
+/**
+ * \copyright
+ * Copyright (c) 2012-2018, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#pragma once
+
+#include <pybind11/pybind11.h>
+#include "BaseLib/ExportSymbol.h"
+
+namespace ProcessLib
+{
+namespace SourceTerms
+{
+namespace Python
+{
+//! Creates Python bindings for the Python source term class.
+OGS_EXPORT_SYMBOL void pythonBindSourceTerm(pybind11::module& m);
+}  // namespace Python
+}  // namespace SourceTerms
+}  // namespace ProcessLib
diff --git a/ProcessLib/SourceTerms/Python/PythonSourceTermPythonSideInterface.h b/ProcessLib/SourceTerms/Python/PythonSourceTermPythonSideInterface.h
new file mode 100644
index 0000000000000000000000000000000000000000..e2bf933785b2091b84eb4406b0d42c64bcb8b769
--- /dev/null
+++ b/ProcessLib/SourceTerms/Python/PythonSourceTermPythonSideInterface.h
@@ -0,0 +1,51 @@
+/**
+ * \copyright
+ * Copyright (c) 2012-2018, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#pragma once
+
+namespace ProcessLib
+{
+namespace SourceTerms
+{
+namespace Python
+{
+//! Base class for source terms.
+//! This class will get Python bindings and is intended to be to be derived in
+//! Python.
+class PythonSourceTermPythonSideInterface
+{
+public:
+    /*!
+     * Computes the flux for the provided arguments (time, position of the node,
+     * primary variables at the node).
+     *
+     * \return flux Flux of the source term at that node and derivative of the
+     * flux w.r.t. all primary variables.
+     */
+    virtual std::pair<double, std::array<double, 3>> getFlux(
+        double /*t*/, std::array<double, 3> /*x*/,
+        std::vector<double> const& /*primary_variables*/) const
+    {
+        return {std::numeric_limits<double>::quiet_NaN(), {}};
+    }
+
+    //! Tells if getFlux() has been overridden in the derived class in Python.
+    //!
+    //! \pre getFlux() must already have been called once.
+    bool isOverriddenGetFlux() const { return _overridden_get_flux; }
+
+    virtual ~PythonSourceTermPythonSideInterface() = default;
+
+private:
+    //! Tells if getFlux() has been overridden in the derived class in Python.
+    mutable bool _overridden_get_flux = true;
+};
+}  // namespace Python
+}  // namespace SourceTerms
+}  // namespace ProcessLib