diff --git a/include/aidge/utils/DynamicAttributes.hpp b/include/aidge/utils/DynamicAttributes.hpp
index 4acc96cf1e291cceb1932ee97af35b8f1b56202b..ee881c94f7af75b6edb6017299115297ccb29185 100644
--- a/include/aidge/utils/DynamicAttributes.hpp
+++ b/include/aidge/utils/DynamicAttributes.hpp
@@ -48,27 +48,7 @@ public:
      */
     template<class T> const T& getAttr(const std::string& name) const
     {
-        const auto dot = name.find('.');
-        if (dot == name.npos) {
-#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>())));
-                }
-            }
-#endif
-
-            return future_std::any_cast<const T&>(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)).getAttr<T>(nsName);
-        }
+        return future_std::any_cast<const T&>(get(name));
     }
 
     template<class T> T& getAttr(const std::string& name) {
@@ -83,26 +63,7 @@ public:
     ///\param value Attribute value
     template<class T> void addAttr(const std::string& name, const T& value)
     {
-        const auto dot = name.find('.');
-        if (dot == name.npos) {
-            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 everytime
-                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);
-            const auto nsName = name.substr(dot + 1);
-            const auto& res = mAttrs.emplace(std::make_pair(ns, future_std::any(DynamicAttributes())));
-            future_std::any_cast<DynamicAttributes&>(res.first->second).addAttr(nsName, value);
-        }
+        add(name, future_std::any(value));
     }
 
     ///\brief Set an Attribute value, identified by its name. If it already exists, its value (and type, if different) is changed.
@@ -111,28 +72,7 @@ public:
     ///\param value Attribute value
     template<class T> void setAttr(const std::string& name, const T& value)
     {
-        const auto dot = name.find('.');
-        if (dot == name.npos) {
-            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 everytime
-                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);
-            const auto nsName = name.substr(dot + 1);
-            auto res = mAttrs.emplace(std::make_pair(ns, future_std::any(DynamicAttributes())));
-            future_std::any_cast<DynamicAttributes&>(res.first->second).setAttr<T>(nsName, value);
-        }
+        set(name, future_std::any(value));
     }
 
     void delAttr(const std::string& name) {
@@ -328,9 +268,84 @@ public:
     };
 #endif
 
+    const future_std::any& get(const std::string& name) const
+    {
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+#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>())));
+                }
+            }
+#endif
+
+            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)).get(nsName);
+        }
+    }
+
+    void add(const std::string& name, const future_std::any& value)
+    {
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            const auto& res = mAttrs.emplace(std::make_pair(name, 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 everytime
+                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);
+            const auto nsName = name.substr(dot + 1);
+            const auto& res = mAttrs.emplace(std::make_pair(ns, future_std::any(DynamicAttributes())));
+            future_std::any_cast<DynamicAttributes&>(res.first->second).add(nsName, value);
+        }
+    }
+
+    void set(const std::string& name, const future_std::any& value)
+    {
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            auto res = mAttrs.emplace(std::make_pair(name, 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 everytime
+                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);
+            const auto nsName = name.substr(dot + 1);
+            auto res = mAttrs.emplace(std::make_pair(ns, future_std::any(DynamicAttributes())));
+            future_std::any_cast<DynamicAttributes&>(res.first->second).set(nsName, value);
+        }
+    }
+
     virtual ~DynamicAttributes() {}
 
-   friend bool operator<(const DynamicAttributes& lhs, const DynamicAttributes& rhs);
+    friend bool operator<(const DynamicAttributes& lhs, const DynamicAttributes& rhs);
 
 private:
 #ifdef PYBIND
diff --git a/src/backend/OperatorImpl.cpp b/src/backend/OperatorImpl.cpp
index c13a3b70167d9e33469ebdedfaa2809f51246cf3..a04952c4b29a8f55ad3f7c624a2b196f604f4b6a 100644
--- a/src/backend/OperatorImpl.cpp
+++ b/src/backend/OperatorImpl.cpp
@@ -62,7 +62,10 @@ Aidge::ImplSpec Aidge::OperatorImpl::getRequiredSpec() const {
     }
     // Attributes
     if (!mOp.isAtomic()) {
-        requiredSpec.attrs.setAttr("subtype", mOp.type());
+        requiredSpec.attrs.setAttr("type:!", mOp.type()); // :! mandatory qualifier
+    }
+    else {
+        requiredSpec.attrs.setAttr("type", mOp.type());
     }
     return requiredSpec;
 }
@@ -71,11 +74,12 @@ Aidge::ImplSpec Aidge::OperatorImpl::getBestMatch(ImplSpec requiredSpecs) const
     Log::debug("getBestMatch() for requirements: {}", requiredSpecs);
 
     const auto availableSpecs = getAvailableImplSpecs();
-    std::vector<bool> matchingSpecs(availableSpecs.size(), false);
+    std::vector<int> matchingSpecs(availableSpecs.size(), -1);
 
     for (size_t s = 0; s < availableSpecs.size(); ++s) {
         auto spec = availableSpecs[s];
-        bool match = true;
+        int match = true;
+        int priority = 0;
 
         // Check inputs
         for (size_t i = 0; i < requiredSpecs.inputs.size(); ++i) {
@@ -111,19 +115,57 @@ Aidge::ImplSpec Aidge::OperatorImpl::getBestMatch(ImplSpec requiredSpecs) const
         // TODO
 
         // Check attributes
-        // TODO
+        for (const auto& attrName : requiredSpecs.attrs.getAttrsName()) {
+            std::string name = attrName;
+            std::string qualifier;
+            const auto qualifierPos = std::find_if(attrName.begin(), attrName.end(),
+                [](char c) { return c == ':'; });
+            if (qualifierPos != attrName.begin()) {
+                name = attrName.substr(0, qualifierPos - attrName.begin());
+                qualifier = attrName.substr(qualifierPos - attrName.begin());
+            }
 
-        matchingSpecs[s] = match;
+            const bool mandatory = (qualifier == "!");
+            if (mandatory) {
+                // Required attribute:
+                if (!spec.attrs.hasAttr(name)) {
+                    // Missing attribute
+                    match = false;
+                    break;
+                }
+                else if (requiredSpecs.attrs.get(attrName) < spec.attrs.get(name)
+                    || spec.attrs.get(name) < requiredSpecs.attrs.get(attrName))
+                {
+                    // Attribute value mismatch
+                    match = false;
+                    break;
+                }
+            }
+            else {
+                const int attrPriority = (!qualifier.empty()) ? std::stoi(qualifier) : 0;
+
+                if (spec.attrs.hasAttr(name)
+                    && !(requiredSpecs.attrs.get(attrName) < spec.attrs.get(name))
+                    && !(spec.attrs.get(name) < requiredSpecs.attrs.get(attrName)))
+                {
+                    // Attribute value match
+                    priority = std::max(priority, attrPriority);
+                }
+            }
+        }
 
-        Log::debug("  {} - {}", (match) ? "MATCH" : "MISMATCH", spec);
+        if (match) {
+            matchingSpecs[s] = priority;
+        }
+
+        Log::debug("  {}:{} - {}", (match) ? "MATCH" : "MISMATCH", priority, spec);
     }
 
     // Return best match
-    // TODO: for now, returns the **first** match
-    for (size_t s = 0; s < availableSpecs.size(); ++s) {
-        if (matchingSpecs[s]) {
-            return availableSpecs[s];
-        }
+    const auto bestMatch = std::max_element(matchingSpecs.begin(), matchingSpecs.end());
+    if (*bestMatch >= 0) {
+        const auto bestSpecIdx = bestMatch - matchingSpecs.begin();
+        return availableSpecs[bestSpecIdx];
     }
 
     // If there is no match, return the required specs for the registrar, which