diff --git a/include/aidge/backend/OperatorImpl.hpp b/include/aidge/backend/OperatorImpl.hpp
index 68e2a57b498551f6600d6b5720919d03b9bf037c..649898dd130d5811f65f65af87bc117d3502647c 100644
--- a/include/aidge/backend/OperatorImpl.hpp
+++ b/include/aidge/backend/OperatorImpl.hpp
@@ -14,73 +14,177 @@
 
 #include <string>
 #include <vector>
+#include <functional>
 
 #include "aidge/utils/Types.h"
+#include "aidge/utils/DynamicAttributes.hpp"
+#include "aidge/data/Data.hpp"
 #include "aidge/data/Elts.hpp"
+#include "aidge/scheduler/ProdConso.hpp"
 
 namespace Aidge {
+class Node;
 class Operator;
 
+/**
+ * @brief ImplSpec stores the requirements or the specifications of an implementation.
+ *
+ */
+struct ImplSpec {
+    struct IOSpec {
+        IOSpec(DataType type_, DataFormat format_ = DataFormat::Any, const std::vector<std::pair<int, int>>& dims_ = {}):
+            type(type_),
+            format(format_),
+            dims(dims_)
+        {}
+
+        DataType type;
+        DataFormat format;
+        std::vector<std::pair<int, int>> dims;
+    };
+
+    ImplSpec(const DynamicAttributes& attrs_ = DynamicAttributes());
+    ImplSpec(const IOSpec& io, const DynamicAttributes& attrs_ = DynamicAttributes());
+    ImplSpec(const IOSpec& i, const IOSpec& o, const DynamicAttributes& attrs_ = DynamicAttributes());
+    ImplSpec(const std::vector<IOSpec>& i, const std::vector<IOSpec>& o, const DynamicAttributes& attrs_ = DynamicAttributes());
+    ImplSpec(const Aidge::ImplSpec&);
+    ~ImplSpec() noexcept;
+
+    std::vector<IOSpec> inputs;
+    std::vector<IOSpec> outputs;
+    DynamicAttributes attrs;
+};
+
+inline bool operator==(const ImplSpec::IOSpec& lhs, const ImplSpec::IOSpec& rhs) {
+    return (lhs.type == rhs.type)
+        && (lhs.format == rhs.format)
+        && (lhs.dims == rhs.dims);
+}
+
+inline bool operator<(const ImplSpec::IOSpec& lhs, const ImplSpec::IOSpec& rhs) {
+    return (lhs.type < rhs.type)
+        || (lhs.type == rhs.type && lhs.format < rhs.format)
+        || (lhs.type == rhs.type && lhs.format == rhs.format && lhs.dims < rhs.dims);
+}
+
+inline bool operator<(const ImplSpec& lhs, const ImplSpec& rhs) {
+    return (lhs.inputs < rhs.inputs)
+        || (lhs.inputs == rhs.inputs && lhs.outputs < rhs.outputs)
+        || (lhs.inputs == rhs.inputs && lhs.outputs == rhs.outputs && lhs.attrs < rhs.attrs);
+}
+
+
+inline bool operator==(const ImplSpec& lhs, const ImplSpec& rhs) {
+    return !(lhs < rhs) && !(rhs < lhs);
+}
+
+/**
+ * @brief Impl stores the details of a specific implementation.
+ * It is associated to a ImplSpec in a registry.
+ *
+ */
+template <class FwdFunc, class BwdFunc>
+struct Impl {
+    Impl(std::function<std::unique_ptr<ProdConso>(const Operator&)> prodConso_,
+      std::function<FwdFunc> forward_,
+      std::function<BwdFunc> backward_ = nullptr):
+        prodConso(prodConso_), forward(forward_), backward(backward_) {}
+
+    std::function<std::unique_ptr<ProdConso>(const Operator&)> prodConso;
+    std::function<FwdFunc> forward;
+    std::function<BwdFunc> backward;
+};
+
 class OperatorImpl {
 public:
     OperatorImpl(const Operator& op, const std::string& backend = "");
     virtual void forward();
     virtual void backward();
+    virtual std::shared_ptr<ProdConso> prodConso();
 
     const std::string& backend() const noexcept {
         return mBackend;
     }
-    /**
-     * @brief Minimum amount of data from a specific input required by the
-     * implementation to be run.
-     *
-     * @param inputIdx Index of the input analyzed.
-     * @return std::size_t
-     */
-    virtual Elts_t getNbRequiredData(const IOIndex_t inputIdx) const;
 
-    // Amount of input data that cannot be overwritten during the execution.
-    virtual Elts_t getNbRequiredProtected(const IOIndex_t inputIdx) const;
-
-    // Memory required at an output for a given input size.
-    virtual Elts_t getRequiredMemory(const IOIndex_t outputIdx, const std::vector<DimSize_t> &inputsSize) const;
+    const Operator& getOperator() const noexcept {
+        return mOp;
+    }
 
     /**
-     * @brief Total amount of consumed data from a specific input.
+     * @brief Get the operator required implementation specification, according
+     * to the current operator configuration.
      *
-     * @param inputIdx Index of the input analyzed.
-     * @return DimSize_t
      */
-    virtual Elts_t getNbConsumedData(const IOIndex_t inputIdx) const;
+    ImplSpec getRequiredSpec() const;
 
     /**
-     * @brief Total amount of produced data ready to be used on a specific output.
+     * @brief Get the best implementation that matches \p requiredSpecs.
+     * If no implementation matches \p requiredSpecs, \p requiredSpecs is
+     * returned.
      *
-     * @param outputIdx Index of the output analyzed.
-     * @return DimSize_t
      */
-    virtual Elts_t getNbProducedData(const IOIndex_t outputIdx) const;
+    ImplSpec getBestMatch(const ImplSpec& requiredSpecs) const;
 
     /**
-     * @brief Update the Consumer Producer system by simulating the consumption and production of i/o
+     * @brief Get an adapted meta operator corresponding to the required
+     * specifications \p requiredSpecs from the implementation specifications
+     * \p spec.
      *
+     * @param spec Implementation specification
+     * @param requiredSpecs Required specifications
+     * @return std::shared_ptr<Node> Adapted meta op or nullptr
      */
-    virtual void updateConsummerProducer();
+    std::shared_ptr<Node> getAdaptation(const ImplSpec& spec, const ImplSpec& requiredSpecs) const;
 
     /**
-     * @brief Reset the Consumer Producer system.
+     * @brief Get the best adapted meta operator corresponding to the required
+     * specifications \p requiredSpecs.
+     * The best adaptation is the one with the lowest overhead cost.
+     * Currently, it is the one requiring the least number of additionnal
+     * operators to match the available implementations.
      *
+     * @param requiredSpecs Required specifications
+     * @return std::shared_ptr<Node> Adapted meta op or nullptr
      */
-    virtual void resetConsummerProducer();
+    std::shared_ptr<Node> getBestAdaptation(const ImplSpec& requiredSpecs) const;
 
     virtual ~OperatorImpl() = default;
 
 protected:
+    virtual std::shared_ptr<ProdConso> getProdConso() const;
+    virtual std::vector<ImplSpec> getAvailableImplSpecs() const;
+    bool checkIOSpec(const ImplSpec::IOSpec& required, const ImplSpec::IOSpec& spec) const;
+
     const Operator &mOp;
     const std::string mBackend;
-    std::vector<Elts_t> mNbConsumedData;
-    std::vector<Elts_t> mNbProducedData;
+    std::shared_ptr<ProdConso> mProdConso;
 };
 } // namespace Aidge
 
+template<>
+struct fmt::formatter<Aidge::ImplSpec::IOSpec> {
+    template<typename ParseContext>
+    inline constexpr auto parse(ParseContext& ctx) {
+        return ctx.begin();
+    }
+
+    template<typename FormatContext>
+    inline auto format(Aidge::ImplSpec::IOSpec const& ioSpec, FormatContext& ctx) const {
+        return fmt::format_to(ctx.out(), "{}, {}, {}", ioSpec.type, ioSpec.format, ioSpec.dims);
+    }
+};
+
+template<>
+struct fmt::formatter<Aidge::ImplSpec> {
+    template<typename ParseContext>
+    inline constexpr auto parse(ParseContext& ctx) {
+        return ctx.begin();
+    }
+
+    template<typename FormatContext>
+    inline auto format(Aidge::ImplSpec const& implSpec, FormatContext& ctx) const {
+        return fmt::format_to(ctx.out(), "{}, {}", implSpec.inputs, implSpec.outputs);
+    }
+};
+
 #endif /* AIDGE_BACKEND_OPERATORIMPL_H_ */
diff --git a/include/aidge/operator/Identity.hpp b/include/aidge/operator/Identity.hpp
index 17eca02261704e98341adca81636b594d92c2318..24476f231806bf38ae48b9e2d5ec405e072afdb2 100644
--- a/include/aidge/operator/Identity.hpp
+++ b/include/aidge/operator/Identity.hpp
@@ -26,6 +26,11 @@
 #include "aidge/utils/ErrorHandling.hpp"
 
 namespace Aidge {
+class Identity_OpImpl : public OperatorImpl {
+public:
+    Identity_OpImpl(const Operator& op, const std::string& backend = ""): OperatorImpl(op, backend) {}
+    void forward() override;
+};
 
 /**
  * @brief Indentity_Op is an helper operator made to ease the declaration of MetaNodes.
@@ -35,7 +40,7 @@ namespace Aidge {
  *
  */
 class Identity_Op : public OperatorTensor,
-    public Registrable<Identity_Op, std::string, std::unique_ptr<OperatorImpl>(const Identity_Op&)> {
+    public Registrable<Identity_Op, std::string, std::function<std::unique_ptr<OperatorImpl>(const Identity_Op&)>> {
 public:
     static const std::string Type;
 
@@ -54,29 +59,8 @@ public:
      */
     std::shared_ptr<Operator> clone() const override;
 
-    // bool forwardDims(bool /*allowDataDependency*/ = false) override final { return true; } // Do nothing
-
-    /**
-     * @brief Check if output dimensions have been computed.
-     * @note Since Identity has no output Tensor, this function checks if its
-     * only input's dimensions have been computed.
-     *
-     * @return true Input has dimensions.
-     * @return false Input has no dimensions or is a nullptr.
-     */
-    bool dimsForwarded() const override final;
-
-
-    void forward() override final;
-
-    void backward() override final { }
-
-    void setBackend(const std::string& /*name*/, DeviceIdx_t /*device*/ = 0) override final {
-        // setBackend do nothing, Identity node has no backend it just pass the same Tensor
-    }
-    void setDataType(const DataType& /*dataType*/) const override final {
-        // setDatatype do nothing, Identity node has no backend it just pass the same Tensor
-    }
+    void setBackend(const std::string& name, DeviceIdx_t device = 0) override final;
+    std::set<std::string> getAvailableBackends() const override;
 
     static const std::vector<std::string> getInputsName(){
         return {"data_input"};
diff --git a/include/aidge/utils/Attributes.hpp b/include/aidge/utils/Attributes.hpp
index 20f93398df0e1453bad95be22479a37451665ee7..fd29bf4ce57ac94e0860172d2d1c15dc40f15ae0 100644
--- a/include/aidge/utils/Attributes.hpp
+++ b/include/aidge/utils/Attributes.hpp
@@ -14,6 +14,9 @@
 
 #include <string>
 #include <set>
+#include <map>
+
+#include "aidge/utils/future_std/any.hpp"
 
 #ifdef PYBIND
 #include <pybind11/pybind11.h>
@@ -63,15 +66,15 @@ public:
     */
     virtual std::set<std::string> getAttrsName() const = 0;
 
-#ifdef PYBIND
-    virtual bool hasAttrPy(const std::string& name) const = 0;
+    virtual std::map<std::string, future_std::any> getAttrs() const = 0;
 
-    /* Bindable get function, does not require any templating.
+#ifdef PYBIND
+    /* Bindable get function, does not recquire any templating.
     *  This is thanks to py::object which allow the function to
     *  be agnostic from its return type.
     */
     virtual py::object getAttrPy(const std::string& name) const  = 0;
-    /* Bindable set function, does not require any templating.
+    /* Bindable set function, does not recquire any templating.
     *  This is thanks to py::object which allow the function to
     *  be agnostic from ``value`` type.
     */
@@ -84,6 +87,7 @@ public:
     virtual py::dict dict() const = 0;
 
 #endif
+
     virtual ~Attributes() {}
 };
 }
diff --git a/include/aidge/utils/DynamicAttributes.hpp b/include/aidge/utils/DynamicAttributes.hpp
index 8f6f5c7dea1d099e8061644d5ae034309ba4185a..1b55d7afbf8263a77cf70752fc92f72ef5027904 100644
--- a/include/aidge/utils/DynamicAttributes.hpp
+++ b/include/aidge/utils/DynamicAttributes.hpp
@@ -18,6 +18,7 @@
 #include <typeinfo>
 #include <cassert>
 #include <string>
+#include <typeindex>
 
 #include "aidge/utils/future_std/any.hpp"
 #include "aidge/utils/Attributes.hpp"
@@ -38,31 +39,34 @@ namespace Aidge {
 ///\todo managing complex types or excluding non-trivial, non-aggregate types
 class DynamicAttributes : public Attributes {
 public:
+    DynamicAttributes() = default;
+    DynamicAttributes(const std::map<std::string, future_std::any>& attrs): mAttrs(attrs) {}
+
     /**
      * \brief Returning an Attribute identified by its name
      * \tparam T expected Attribute type
      * \param name Attribute name
      * \details assert if T is not the actual Attribute type or if the Attribute does not
      *  exist
-     * \note at() throws if the Attribute does not exist, using find to test for Attribute existence
+     * \note at() throws if the Attribute does not exist, using find to test for Attribute existance
      */
-    template<class T> const T& getAttr(const std::string& name) const
+    template<class T> T getAttr(const std::string& name) const
     {
         const auto dot = name.find('.');
         if (dot == name.npos) {
+            mAnyUtils.emplace(typeid(T), std::unique_ptr<AnyUtils<T>>(new AnyUtils<T>()));
+
+            const auto& attr = mAttrs.at(name);
 #ifdef PYBIND
-            // If attribute does not exist in C++, it might have been created or modified in Python
-            auto it = mAttrs.find(name);
-            if (it == mAttrs.end()) {
-                auto itPy = mAttrsPy.find(name);
-                if (itPy != mAttrsPy.end()) {
-                    // Insert the attribute back in C++
-                    mAttrs.emplace(std::make_pair(name, future_std::any(itPy->second.cast<T>())));
-                }
+            if (attr.type() == typeid(py::object)) {
+                // Note: because of cast<T>(), this function cannot return a const reference!
+                return future_std::any_cast<const py::object&>(attr).cast<T>();
             }
+            else
 #endif
-
-            return future_std::any_cast<const T&>(mAttrs.at(name));
+            {
+                return future_std::any_cast<const T&>(attr);
+            }
         }
         else {
             const auto ns = name.substr(0, dot);
@@ -72,9 +76,21 @@ public:
     }
 
     template<class T> T& getAttr(const std::string& name) {
-        // Scott Meyers' solution to avoid code duplication
-        return const_cast<T&>(
-            static_cast<const DynamicAttributes&>(*this).getAttr<T>(name));
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            mAnyUtils.emplace(typeid(T), std::unique_ptr<AnyUtils<T>>(new AnyUtils<T>()));
+
+            auto& attr = mAttrs.at(name);
+#ifdef PYBIND
+            AIDGE_ASSERT(attr.type() != typeid(py::object), "getAttr(): cannot return a reference to a Python-defined attribute.");
+#endif
+            return future_std::any_cast<T&>(attr);
+        }
+        else {
+            const auto ns = name.substr(0, dot);
+            const auto nsName = name.substr(dot + 1);
+            return future_std::any_cast<DynamicAttributes&>(mAttrs.at(ns)).getAttr<T>(nsName);
+        }
     }
 
     ///\brief Add a new Attribute, identified by its name. If it already exists, asserts.
@@ -85,17 +101,10 @@ public:
     {
         const auto dot = name.find('.');
         if (dot == name.npos) {
+            mAnyUtils.emplace(typeid(T), std::unique_ptr<AnyUtils<T>>(new AnyUtils<T>()));
+
             const auto& res = mAttrs.emplace(std::make_pair(name, future_std::any(value)));
             AIDGE_ASSERT(res.second, "addAttr(): attribute \"{}\" already exists. Use setAttr() if this is expected.", name);
-
-#ifdef PYBIND
-            // We cannot handle Python object if the Python interpreter is not running
-            if (Py_IsInitialized()) {
-                // Keep a copy of the attribute in py::object that is updated every time
-                const auto& resPy = mAttrsPy.emplace(std::make_pair(name, py::cast(value)));
-                AIDGE_ASSERT(resPy.second, "addAttr(): attribute \"{}\" already exists (added in Python). Use setAttr() if this is expected.", name);
-            }
-#endif
         }
         else {
             const auto ns = name.substr(0, dot);
@@ -113,19 +122,11 @@ public:
     {
         const auto dot = name.find('.');
         if (dot == name.npos) {
+            mAnyUtils.emplace(typeid(T), std::unique_ptr<AnyUtils<T>>(new AnyUtils<T>()));
+
             auto res = mAttrs.emplace(std::make_pair(name, future_std::any(value)));
             if (!res.second)
                 res.first->second = future_std::any(value);
-
-#ifdef PYBIND
-            // We cannot handle Python object if the Python interpreter is not running
-            if (Py_IsInitialized()) {
-                // Keep a copy of the attribute in py::object that is updated every time
-                auto resPy = mAttrsPy.emplace(std::make_pair(name, py::cast(value)));
-                if (!resPy.second)
-                    resPy.first->second = std::move(py::cast(value));
-            }
-#endif
         }
         else {
             const auto ns = name.substr(0, dot);
@@ -139,9 +140,6 @@ public:
         const auto dot = name.find('.');
         if (dot == name.npos) {
             mAttrs.erase(name);
-#ifdef PYBIND
-            mAttrsPy.erase(name);
-#endif
         }
         else {
             const auto ns = name.substr(0, dot);
@@ -153,41 +151,12 @@ public:
 #ifdef PYBIND
     void addAttrPy(const std::string& name, py::object&& value)
     {
-        const auto dot = name.find('.');
-        if (dot == name.npos) {
-            auto it = mAttrs.find(name);
-            AIDGE_ASSERT(it == mAttrs.end(), "add_attr(): attribute \"{}\" already exists (added in C++). Use set_attr() if this is expected.", name);
-
-            const auto& res = mAttrsPy.emplace(std::make_pair(name, value));
-            AIDGE_ASSERT(res.second, "add_attr(): attribute \"{}\" already exists. Use set_attr() if this is expected.", name);
-        }
-        else {
-            const auto ns = name.substr(0, dot);
-            const auto nsName = name.substr(dot + 1);
-            const auto& res = mAttrs.emplace(std::make_pair(ns, DynamicAttributes()));
-
-            future_std::any_cast<DynamicAttributes&>(res.first->second).addAttrPy(nsName, std::move(value));
-        }
+        addAttr(name, std::move(value));
     }
 
     void setAttrPy(const std::string& name, py::object&& value) override final
     {
-        const auto dot = name.find('.');
-        if (dot == name.npos) {
-            auto resPy = mAttrsPy.emplace(std::make_pair(name, value));
-            if (!resPy.second)
-                resPy.first->second = std::move(value);
-
-            // Force getAttr() to take attribute value from mAttrsPy and update mAttrs
-            mAttrs.erase(name);
-        }
-        else {
-            const auto ns = name.substr(0, dot);
-            const auto nsName = name.substr(dot + 1);
-            const auto& res = mAttrs.emplace(std::make_pair(ns, DynamicAttributes()));
-
-            future_std::any_cast<DynamicAttributes&>(res.first->second).setAttrPy(nsName, std::move(value));
-        }
+        setAttr(name, std::move(value));
     }
 
     py::dict dict() const override {
@@ -196,9 +165,16 @@ public:
             if (elt.second.type() == typeid(DynamicAttributes)) {
                 attributes[elt.first.c_str()] = future_std::any_cast<const DynamicAttributes&>(elt.second).dict();
             }
-        }
-        for (const auto& elt : mAttrsPy) {
-            attributes[elt.first.c_str()] = elt.second;
+            else {
+                // At this point, not every attribute may be known to mAnyUtils
+                const auto anyUtilsIt = mAnyUtils.find(elt.second.type());
+                if (anyUtilsIt != mAnyUtils.end()) {
+                    attributes[elt.first.c_str()] = anyUtilsIt->second->cast(elt.second);
+                }
+                else {
+                    attributes[elt.first.c_str()] = "???";
+                }
+            }
         }
         return attributes;
     }
@@ -221,12 +197,7 @@ public:
     bool hasAttr(const std::string& name) const override final {
         const auto dot = name.find('.');
         if (dot == name.npos) {
-#ifdef PYBIND
-            return (mAttrs.find(name) != mAttrs.cend() || mAttrsPy.find(name) != mAttrsPy.cend());
-
-#else
             return (mAttrs.find(name) != mAttrs.cend());
-#endif
         }
         else {
             const auto ns = name.substr(0, dot);
@@ -241,45 +212,22 @@ public:
         }
     }
 
-#ifdef PYBIND
-    bool hasAttrPy(const std::string& name) const override final {
-        const auto dot = name.find('.');
-        if (dot == name.npos) {
-            // Attributes might have been created in Python, the second condition is necessary.
-            return (mAttrs.find(name) != mAttrs.cend() || mAttrsPy.find(name) != mAttrsPy.cend());
-        }
-        else {
-            const auto ns = name.substr(0, dot);
-            const auto it = mAttrs.find(ns);
-            if (it != mAttrs.cend()) {
-                const auto nsName = name.substr(dot + 1);
-                return future_std::any_cast<const DynamicAttributes&>(it->second).hasAttrPy(nsName);
-            }
-            else {
-                return false;
-            }
-        }
-    }
-#endif
-
     std::string getAttrType(const std::string& name) const override final {
         // In order to remain consistent between C++ and Python, with or without PyBind, the name of the type is:
         // - C-style for C++ created attributes
         // - Python-style for Python created attributes
         const auto dot = name.find('.');
         if (dot == name.npos) {
+            const auto& attr = mAttrs.at(name);
 #ifdef PYBIND
-            // If attribute does not exist in C++, it might have been created in Python
-            auto it = mAttrs.find(name);
-            if (it == mAttrs.end()) {
-                auto itPy = mAttrsPy.find(name);
-                if (itPy != mAttrsPy.end()) {
-                    return std::string(Py_TYPE(itPy->second.ptr())->tp_name);
-                }
+            if (attr.type() == typeid(py::object)) {
+                return std::string(Py_TYPE(future_std::any_cast<const py::object&>(attr).ptr())->tp_name);
             }
+            else
 #endif
-
-            return mAttrs.at(name).type().name();
+            {
+                return attr.type().name();
+            }
         }
         else {
             const auto ns = name.substr(0, dot);
@@ -292,33 +240,20 @@ public:
         std::set<std::string> attrsName;
         for(auto const& it: mAttrs)
             attrsName.insert(it.first);
-#ifdef PYBIND
-        // Attributes might have been created in Python
-        for(auto const& it: mAttrsPy)
-            attrsName.insert(it.first);
-#endif
         return attrsName;
     }
 
 #ifdef PYBIND
     /**
      * @detail See https://github.com/pybind/pybind11/issues/1590 as to why a
-     * generic type caster for std::any is not feasible.
-     * The strategy here is to keep a copy of each attribute in py::object that is updated every time.
+     * generic type caster for std::any is not feasable.
+     * The strategy here is to store a cast() function for each attribute type ever used.
     */
     inline py::object getAttrPy(const std::string& name) const override final {
         const auto dot = name.find('.');
         if (dot == name.npos) {
-            auto itPy = mAttrsPy.find(name);
-            if (itPy == mAttrsPy.end()) {
-                // Attribute may be a namespace
-                auto it = mAttrs.find(name);
-                AIDGE_ASSERT(it != mAttrs.end() && it->second.type() == typeid(DynamicAttributes), "get_attr(): attribute \"{}\" not found", name);
-                return py::cast(future_std::any_cast<const DynamicAttributes&>(it->second));
-            }
-            else {
-                return itPy->second;
-            }
+            const auto& attr = mAttrs.at(name);
+            return mAnyUtils.at(attr.type())->cast(attr);
         }
         else {
             const auto ns = name.substr(0, dot);
@@ -328,25 +263,150 @@ public:
     };
 #endif
 
-    virtual ~DynamicAttributes() {}
+    future_std::any getAny(const std::string& name) const
+    {
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            return mAttrs.at(name);
+        }
+        else {
+            const auto ns = name.substr(0, dot);
+            const auto nsName = name.substr(dot + 1);
+            return future_std::any_cast<const DynamicAttributes&>(mAttrs.at(ns)).getAny(nsName);
+        }
+    }
 
-private:
+    std::map<std::string, future_std::any> getAttrs() const override {
+        return mAttrs;
+    }
+
+    virtual ~DynamicAttributes() {
 #ifdef PYBIND
-    // Stores C++ attributes (copy) and Python-only attributes
-    // Code should be compiled with -fvisibility=hidden
-    // See https://pybind11.readthedocs.io/en/stable/faq.html:
-    // “‘SomeClass’ declared with greater visibility than the type of its
-    // field ‘SomeClass::member’ [-Wattributes]”
-    // This map will only be populated if Python interpreter is running
-    std::map<std::string, py::object> mAttrsPy;
-    // Stores C++ attributes only
-    // mutable because it may be updated in getAttr() from Python
-    mutable std::map<std::string, future_std::any> mAttrs;
-#else
+        if (!Py_IsInitialized()) {
+            // Resets the internal pointer of py::object to nullptr without decreasing the object's reference count.
+            // At this point, the Python interpreter may have exited (it is the case if the current DynamicAttribute being destroyed is static),
+            // in which case py:object has already being destroyed despite the reference counting being > 0.
+            // See https://github.com/pybind/pybind11/issues/1598
+            for (auto& attr : mAttrs) {
+                if (attr.second.type() == typeid(py::object)) {
+                    future_std::any_cast<py::object&>(attr.second).release();
+                }
+            }
+        }
+#endif
+    }
+
+    friend bool operator<(const DynamicAttributes& lhs, const DynamicAttributes& rhs);
+    friend struct std::hash<DynamicAttributes>;
+
+private:
     std::map<std::string, future_std::any> mAttrs;
+
+public:
+    struct AnyUtils_ {
+#ifdef PYBIND
+        virtual py::object cast(const future_std::any& attr) const = 0;
+#endif
+        virtual bool compare(const future_std::any&, const future_std::any&) const = 0;
+        virtual size_t hash(const future_std::any&) const = 0;
+        virtual ~AnyUtils_() = default;
+    };
+
+    template <class T>
+    struct AnyUtils : public AnyUtils_ {
+#ifdef PYBIND
+        py::object cast(const future_std::any& attr) const override final {
+            return py::cast(future_std::any_cast<const T&>(attr));
+        }
 #endif
+
+        bool compare(const future_std::any& lhs, const future_std::any& rhs) const override final {
+#ifdef PYBIND
+            if (lhs.type() == typeid(py::object) && rhs.type() != typeid(py::object)) {
+                return (future_std::any_cast<py::object>(lhs).cast<T>() < future_std::any_cast<T>(rhs));
+            }
+            else if (lhs.type() != typeid(py::object) && rhs.type() == typeid(py::object)) {
+                return (future_std::any_cast<T>(lhs) < future_std::any_cast<py::object>(rhs).cast<T>());
+            }
+            else
+#endif
+            {
+                return (future_std::any_cast<T>(lhs) < future_std::any_cast<T>(rhs));
+            }
+        }
+
+        size_t hash(const future_std::any& attr) const override final {
+            return std::hash<T>()(future_std::any_cast<T>(attr));
+        }
+    };
+
+    // Stores typed utils functions for each attribute type ever used
+    static std::map<std::type_index, std::unique_ptr<AnyUtils_>> mAnyUtils;
 };
 
+template<> void DynamicAttributes::setAttr<future_std::any>(const std::string& name, const future_std::any& value);
+
+#ifdef PYBIND
+template <>
+struct DynamicAttributes::AnyUtils<py::object> : public DynamicAttributes::AnyUtils_ {
+    py::object cast(const future_std::any& attr) const override {
+        return future_std::any_cast<const py::object&>(attr);
+    }
+
+    bool compare(const future_std::any& lhs, const future_std::any& rhs) const override {
+        return (future_std::any_cast<py::object>(lhs) < future_std::any_cast<py::object>(rhs));
+    }
+
+    size_t hash(const future_std::any& attr) const override final {
+        // Here we are mixing Python and C++ hashes... if both are
+        // well implemented, this should not increase the collision 
+        // probability for the same number of stored hashes.
+        return py::hash(future_std::any_cast<py::object>(attr));
+    }
+};
+#endif
+
+inline bool operator<(const DynamicAttributes& lhs, const DynamicAttributes& rhs) {
+    return (lhs.mAttrs < rhs.mAttrs);
+}
+
+// Combine the hashes (boost-like hash combining, see boost::hash_combine())
+inline void hash_combine(std::size_t& seed, const std::size_t& value) {
+    seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+}
+}
+
+namespace std {
+    // Make DynamicAttributes hashable so that is can be stored in hash-based containers.
+    // This is particularly useful in Python since set() and dict() are hash-based.
+    template <>
+    struct hash<Aidge::DynamicAttributes> {
+        size_t operator()(const Aidge::DynamicAttributes& attrs) const {
+            std::size_t seed = 0;
+            for (const auto& pair : attrs.mAttrs) {
+                Aidge::hash_combine(seed, std::hash<std::string>()(pair.first));
+                Aidge::hash_combine(seed, Aidge::DynamicAttributes::mAnyUtils.at(pair.second.type())->hash(pair.second));
+            }
+            return seed;
+        }
+    };
+
+    // General specialization of std::hash for any container that has iterators (e.g., std::vector, std::list, std::set)
+    template <template <typename...> class Container, typename T, typename... Args>
+    struct hash<Container<T, Args...>> {
+        std::size_t operator()(const Container<T, Args...>& iterable) const {
+            std::size_t seed = 0;
+            for (const auto& v : iterable) {
+                // Recursively hash the value pointed by the iterator
+                Aidge::hash_combine(seed, std::hash<T>()(v));
+            }
+            return seed;
+        }
+    };
+}
+
+namespace future_std {
+bool operator<(const future_std::any& lhs, const future_std::any& rhs);
 }
 
 #endif /* AIDGE_CORE_UTILS_DYNAMICATTRIBUTES_H_ */
diff --git a/src/graph/GraphView.cpp b/src/graph/GraphView.cpp
index 48597e2b6fa95ff3195ed2eea6b8c39dcef86771..e1f25e86827e81b26436876dce1b98fe0cda80b8 100644
--- a/src/graph/GraphView.cpp
+++ b/src/graph/GraphView.cpp
@@ -103,7 +103,7 @@ void Aidge::GraphView::save(const std::string& path, bool verbose, bool showProd
         std::string givenName =
             (node_ptr->name().empty())
                 ? "<em>" + node_ptr->type() + "#" + namePtrTable.at(node_ptr) + "</em>"
-                : "\"" + node_ptr->name() + "\\n<sub><em>(" + node_ptr->type() + "#" + namePtrTable.at(node_ptr) + ")</em></sub>\"";
+                : "\"" + node_ptr->name() + "<br/><sub><em>(" + node_ptr->type() + "#" + namePtrTable.at(node_ptr) + ")</em></sub>\"";
 
         std::string nodeCls = "";
         if (node_ptr->type() == "Producer") {
@@ -144,27 +144,31 @@ void Aidge::GraphView::save(const std::string& path, bool verbose, bool showProd
       }
       IOIndex_t outputIdx = 0;
       for (const auto& childs : node_ptr->getOrderedChildren()) {
-        for (const auto& child : childs) {
+        // Keep only unique childs in order to avoid duplicating connections
+        const auto uniqueChilds = std::set<NodePtr>(childs.begin(), childs.end());
+        for (const auto& child : uniqueChilds) {
           if (child != nullptr) {
             IOIndex_t inputIdx = 0;
             for (auto parent : child->inputs()) {
               if (parent.first == node_ptr && parent.second == outputIdx) {
                 // Add-on to display the operator's output dimensions
                 std::string dims = "";
+                std::string dtype = "";
                 const auto op = std::dynamic_pointer_cast<OperatorTensor>(node_ptr->getOperator());
                 if (op && !op->getOutput(outputIdx)->undefined()) {
                   dims += " " + fmt::format("{}", op->getOutput(outputIdx)->dims());
+                  dtype += "\n" + fmt::format("{}", op->getOutput(outputIdx)->dataType());
                 }
 
                 if (mNodes.find(child) != mNodes.end()) {
-                  fmt::print(fp.get(), "{}_{}-->|\"{}{}&rarr;{}\"|{}_{}\n", node_ptr->type(), namePtrTable.at(node_ptr),
-                              outputIdx, dims, inputIdx, child->type(), namePtrTable.at(child));
+                  fmt::print(fp.get(), "{}_{}-->|\"{}{}{}&rarr;{}\"|{}_{}\n", node_ptr->type(), namePtrTable.at(node_ptr),
+                              outputIdx, dims, dtype, inputIdx, child->type(), namePtrTable.at(child));
                 }
                 else if (verbose) {
-                  fmt::print(fp.get(), "{}_{}-->|\"{}{}&rarr;{}\"|{}:::externalCls\n", node_ptr->type(), namePtrTable.at(node_ptr),
-                              outputIdx, dims, inputIdx, static_cast<void*>(child.get()));
+                  fmt::print(fp.get(), "{}_{}-->|\"{}{}{}&rarr;{}\"|{}:::externalCls\n", node_ptr->type(), namePtrTable.at(node_ptr),
+                              outputIdx, dims, dtype, inputIdx, static_cast<void*>(child.get()));
                 }
-                break;
+                // Do no break here because the same child can be connected to several inputs
               }
               ++inputIdx;
             }
@@ -270,7 +274,10 @@ void Aidge::GraphView::setRootNode(NodePtr node) {
 std::set<std::shared_ptr<Aidge::Node>> Aidge::GraphView::inputNodes() const {
     std::set<std::shared_ptr<Aidge::Node>> nodes;
     for (const auto& node : mInputNodes) {
-        nodes.insert(node.first);
+        // Do not include dummy inputs
+        if (node.first) {
+            nodes.insert(node.first);
+        }
     }
     return nodes;
 }
@@ -278,7 +285,10 @@ std::set<std::shared_ptr<Aidge::Node>> Aidge::GraphView::inputNodes() const {
 std::set<std::shared_ptr<Aidge::Node>> Aidge::GraphView::outputNodes() const {
     std::set<std::shared_ptr<Aidge::Node>> nodes;
     for (const auto& node : mOutputNodes) {
-        nodes.insert(node.first);
+        // Do not include dummy outputs
+        if (node.first) {
+            nodes.insert(node.first);
+        }
     }
     return nodes;
 }
@@ -341,7 +351,7 @@ Aidge::IOIndex_t Aidge::GraphView::getNbDataInputs() const {
   for (const std::shared_ptr<Node> &inNode : inputNodes()) {
     // We cannot simply add inNode->nbDataInputs(), as input nodes may already
     // have some inputs connected within the GraphView, which would therefore not
-    // constitute inputs (from outside) for the GraphView!
+    // constitue inputs (from outside) for the GraphView!
     const std::vector<std::pair<std::shared_ptr<Node>, IOIndex_t>> inputNodeinputs =
         inNode->dataInputs();
 
@@ -423,7 +433,7 @@ bool Aidge::GraphView::forwardDims(const std::vector<std::vector<Aidge::DimSize_
     // remove current Data connections and use dummy inputs to propagate dimensions
     // setInputs
     // Link every tensor to the right pointer
-    // following parent - children information
+    // following parent - children informations
     if (!dims.empty()){
       Log::debug("forwardDims(): setting graph input dims ({} dims provided).", dims.size());
 
@@ -522,7 +532,10 @@ bool Aidge::GraphView::forwardDims(const std::vector<std::vector<Aidge::DimSize_
                 }
 
                 if (parentsForwarded && op->forwardDims(allowDataDependency)) {
-                    // Recompute every time, even if it was already computed in a
+                    Log::debug("Dimensions forwarded for node {} (of type {})",
+                        nodePtr->name(), nodePtr->type());
+
+                    // Recompute everytime, even if it was already computed in a
                     // previous call of forwardDims(), as the graph may have changed!
                     dimsForwarded.insert(nodePtr);
                     for (const auto& child : nodePtr->getChildren()) {
@@ -532,7 +545,9 @@ bool Aidge::GraphView::forwardDims(const std::vector<std::vector<Aidge::DimSize_
                     }
                 }
                 else {
-                    Log::debug("Unable to forward dimensions for node {} (of type {}) yet", nodePtr->name(), nodePtr->type());
+                    if (parentsForwarded) {
+                        Log::debug("Unable to forward dimensions for node {} (of type {})", nodePtr->name(), nodePtr->type());
+                    }
                     nextList.insert(nodePtr);
                 }
             }
@@ -614,7 +629,7 @@ void Aidge::GraphView::setInputId(Aidge::IOIndex_t /*inID*/,
 }
 
 void Aidge::GraphView::add(std::shared_ptr<Node> node, bool includeLearnableParam) {
-  AIDGE_ASSERT(node != nullptr, "Trying to add non-existent node!");
+  AIDGE_ASSERT(node != nullptr, "Trying to add non-existant node!");
 
   // first node to be added to the graph is the root node by default
   if (mRootNode == nullptr) {
@@ -685,6 +700,61 @@ std::pair<std::vector<Aidge::NodePtr>, size_t> Aidge::GraphView::getRankedNodes(
   return std::make_pair(rankedNodes, orderUnicityLimit);
 }
 
+std::vector<Aidge::NodePtr> Aidge::GraphView::getOrderedNodes(bool reversed) const {
+    // We compute the order from a post-dfs walk on the reverse graph starting from
+    // ordered output nodes.
+    // Also, we walk the graph upward left to right in order
+    // to get a topological left-right order when possible.
+    // For the case where reversed is true, we walk the graph upward right to left
+    // and reverse the final order to get a post-dfs left-right order when possible.
+    std::vector<std::pair<NodePtr,std::pair<size_t, std::vector<NodePtr>>>> stack;
+    std::vector<NodePtr> reversePostDfs;
+    std::set<NodePtr> visited;
+    std::vector<NodePtr> outNodes(mNodes.size());
+    auto reverse_if_dfs = [reversed](auto &parents) {
+        if (reversed) std::reverse(parents.begin(), parents.end());
+    };
+    for (const auto& output : mOutputNodes) {
+            outNodes.push_back(output.first);
+    }
+    reverse_if_dfs(outNodes);
+    stack.push_back(std::make_pair(nullptr, std::make_pair(0, std::move(outNodes))));
+    while (!stack.empty()) {
+        auto node = stack.back().first;
+        auto& parentIdx = stack.back().second.first;
+        auto& parents = stack.back().second.second;
+        if (parentIdx == parents.size()) {
+            stack.pop_back();
+            if (node) {
+                reversePostDfs.push_back(node);
+            }
+        } else {
+            auto backEdgeIdx = reversed ? parents.size() - 1 - parentIdx: parentIdx;
+            auto isBackEdge = node != nullptr ? node->parentIsBackEdge(backEdgeIdx): false;
+            auto parent = parents[parentIdx++];
+            if (parent != nullptr && inView(parent) &&
+                visited.find(parent) == visited.end()) {
+                if (isBackEdge) {
+                    stack[0].second.second.push_back(parent);
+                } else {
+                    visited.insert(parent);
+                    auto next_parents = parent->getParents();
+                    reverse_if_dfs(next_parents);
+                    stack.push_back(std::make_pair(parent, std::make_pair(0, std::move(next_parents))));
+                }
+            }
+        }
+    }
+
+    if (reversePostDfs.size() != mNodes.size()) {
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+                             "Could not enumerate all nodes, set output nodes such that all graph nodes are connected.");
+    }
+
+    reverse_if_dfs(reversePostDfs);
+    return reversePostDfs;
+}
+
 std::map<Aidge::NodePtr, std::string> Aidge::GraphView::getRankedNodesName(const std::string& format, bool markNonUnicity) const {
   const auto rankedNodes = getRankedNodes();
   std::map<NodePtr, std::string> rankedNodesName;
@@ -751,7 +821,7 @@ bool Aidge::GraphView::add(std::set<std::shared_ptr<Node>> otherNodes, bool incl
     mRootNode = *noParentNodes.begin();
 
     if (noParentNodes.size() > 1) {
-      // If there is more than one, order unicity cannot be guaranteed!
+      // If there is more than one, order unicity cannot be garanteed!
       orderUnicity = false;
     }
 
@@ -854,7 +924,7 @@ void Aidge::GraphView::addChild(
   // assert input node is valid
   if (!toNode.first) {
     assert(toOtherView->inputNodes().size() == 1U &&
-           "If no input node is provided, the other graph should have only "
+           "If no intput node is provided, the other graph should have only "
            "one to make the choice explicit.");
     toNode.first = *(toOtherView->inputNodes().begin());
   } else {
@@ -975,7 +1045,7 @@ void Aidge::GraphView::remove(std::shared_ptr<Node> nodePtr, bool includeLearnab
 
 
 bool Aidge::GraphView::swap(Node & /*node*/, Node & /*otherNode*/) {
-  fmt::print("Swap() not implemented yet. Return false.\n");
+  fmt::print("Swap() not implementated yet. Return false.\n");
   return false;
 }
 
@@ -1406,7 +1476,7 @@ void Aidge::GraphView::updateInputsOutputsDelete(std::shared_ptr<Node> deletedNo
 std::shared_ptr<Aidge::GraphView> Aidge::GraphView::cloneCallback(NodePtr(*cloneNode)(NodePtr)) const {
   std::shared_ptr<GraphView> newGraph = std::make_shared<GraphView>(mName);
 
-  // Map for old node -> new node correspondence
+  // Map for old node -> new node correspondance
   std::map<NodePtr, NodePtr> oldToNewNodes;
 
   for (const std::shared_ptr<Node> &node_ptr : mNodes) {