diff --git a/include/aidge/utils/DynamicAttributes.hpp b/include/aidge/utils/DynamicAttributes.hpp
index c5054eb2fd2e8bfa5e7fca898f343ce630643dbd..9c4c197e99406b0d4b4e9afdc5e4fb0c5479da62 100644
--- a/include/aidge/utils/DynamicAttributes.hpp
+++ b/include/aidge/utils/DynamicAttributes.hpp
@@ -46,40 +46,48 @@ public:
      *  exist
      * \note at() throws if the Attribute does not exist, using find to test for Attribute existance
      */
-    template<class T> T& getAttr(const std::string& name)
+    template<class T> const T& getAttr(const std::string& name) const
     {
-        AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes.");
-#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(pascalToSnake(name));
-            if (itPy != mAttrsPy.end()) {
-                // Insert the attribute back in C++
-                mAttrs.emplace(std::make_pair(name, future_std::any(itPy->second.cast<T>())));
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes for \"{}\".", 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(pascalToSnake(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<T&>(mAttrs.at(name));
-    }
+            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);
 
-    template<class T> const T& getAttr(const std::string& name) const
-    {
-        AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes.");
-#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(pascalToSnake(name));
-            if (itPy != mAttrsPy.end()) {
-                // Insert the attribute back in C++
-                mAttrs.emplace(std::make_pair(name, future_std::any(itPy->second.cast<T>())));
+#ifdef PYBIND
+            // If attribute does not exist in C++, it might have been created or modified in Python
+            auto it = mAttrs.find(ns);
+            if (it == mAttrs.end()) {
+                auto itPy = mAttrsPy.find(ns);
+                if (itPy != mAttrsPy.end()) {
+                    // Insert the attribute back in C++
+                    mAttrs.emplace(std::make_pair(ns, future_std::any(itPy->second.cast<DynamicAttributes>())));
+                }
             }
-        }
 #endif
+            return future_std::any_cast<const DynamicAttributes&>(mAttrs.at(ns)).getAttr<T>(nsName);
+        }
+    }
 
-        return future_std::any_cast<const T&>(mAttrs.at(name));
+    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));
     }
 
     ///\brief Add a new Attribute, identified by its name. If it already exists, asserts.
@@ -88,17 +96,35 @@ public:
     ///\param value Attribute value
     template<class T> void addAttr(const std::string& name, const T& value)
     {
-        AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes.");
-        const auto& res = mAttrs.emplace(std::make_pair(name, future_std::any(value)));
-        AIDGE_ASSERT(res.second, "attribute already exists");
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes for \"{}\".", name);
+            const auto& res = mAttrs.emplace(std::make_pair(name, future_std::any(value)));
+            AIDGE_ASSERT(res.second, "attribute \"{}\" already exists", 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
-            mAttrsPy.emplace(std::make_pair(pascalToSnake(name), py::cast(value)));
+            // 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
+                mAttrsPy.emplace(std::make_pair(pascalToSnake(name), py::cast(value)));
+            }
+#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())));
+
+#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
+                mAttrsPy.emplace(std::make_pair(ns, py::cast(DynamicAttributes())));
+            }
 #endif
+
+            future_std::any_cast<DynamicAttributes&>(res.first->second).addAttr(nsName, value);
+        }
     }
 
     ///\brief Set an Attribute value, identified by its name. If it already exists, its value (and type, if different) is changed.
@@ -107,49 +133,98 @@ public:
     ///\param value Attribute value
     template<class T> void setAttr(const std::string& name, const T& value)
     {
-        auto res = mAttrs.emplace(std::make_pair(name, future_std::any(value)));
-        if (!res.second)
-            res.first->second = future_std::any(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));
+            // 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())));
+
+#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(ns, py::cast(DynamicAttributes())));
+            }
 #endif
+
+            future_std::any_cast<DynamicAttributes&>(res.first->second).setAttr<T>(nsName, value);
+        }
     }
 
     void delAttr(const std::string& name) {
-        mAttrs.erase(name);
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            mAttrs.erase(name);
 #ifdef PYBIND
-        mAttrsPy.erase(name);
+            mAttrsPy.erase(name);
 #endif
+        }
+        else {
+            const auto ns = name.substr(0, dot);
+            const auto nsName = name.substr(dot + 1);
+            future_std::any_cast<DynamicAttributes&>(mAttrs.at(ns)).delAttr(nsName);
+#ifdef PYBIND
+            mAttrsPy.erase(name);
+#endif
+        }
     }
 
 #ifdef PYBIND
     void addAttrPy(const std::string& name, py::object&& value)
     {
-        AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python.");
-        auto it = mAttrs.find(snakeToPascal(name));
-        AIDGE_ASSERT(it == mAttrs.end(), "attribute already exists");
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python for \"{}\".", name);
+            auto it = mAttrs.find(snakeToPascal(name));
+            AIDGE_ASSERT(it == mAttrs.end(), "attribute \"{}\" already exists", name);
+
+            const auto& res = mAttrsPy.emplace(std::make_pair(name, value));
+            AIDGE_ASSERT(res.second, "attribute \"{}\" already exists", 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, future_std::any(DynamicAttributes())));
 
-        const auto& res = mAttrsPy.emplace(std::make_pair(name, value));
-        AIDGE_ASSERT(res.second, "attribute already exists");
+            future_std::any_cast<DynamicAttributes&>(res.first->second).addAttrPy(nsName, std::move(value));
+        }
     }
 
     void setAttrPy(const std::string& name, py::object&& value) override final
     {
-        AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python.");
-        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
-        const std::string pascalName = snakeToPascal(name);
-        mAttrs.erase(pascalName);
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python for \"{}\".", name);
+            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
+            const std::string pascalName = snakeToPascal(name);
+            mAttrs.erase(pascalName);
+        }
+        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).setAttrPy(nsName, std::move(value));
+        }
     }
 
     py::dict dict() const override {
@@ -177,15 +252,49 @@ public:
     ///     Generic Attributes API
     //////////////////////////////////////
     bool hasAttr(const std::string& name) const override final {
-        AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes.");
-        return (mAttrs.find(name) != mAttrs.cend());
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes for \"{}\".", name);
+            return (mAttrs.find(name) != mAttrs.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).hasAttr(nsName);
+            }
+            else {
+                return false;
+            }
+        }
     }
 
 #ifdef PYBIND
     bool hasAttrPy(const std::string& name) const override final {
-        AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python.");
-        // Attributes might have been created in Python, the second condition is necessary.
-        return (mAttrs.find(snakeToPascal(name)) != mAttrs.cend() || mAttrsPy.find(name) != mAttrsPy.cend());
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python for \"{}\".", name);
+            // Attributes might have been created in Python, the second condition is necessary.
+            return (mAttrs.find(snakeToPascal(name)) != mAttrs.cend() || mAttrsPy.find(name) != mAttrsPy.cend());
+        }
+        else {
+            const auto ns = name.substr(0, dot);
+            const auto nsName = name.substr(dot + 1);
+            const auto it = mAttrs.find(ns);
+            if (it != mAttrs.cend()) {
+                return future_std::any_cast<const DynamicAttributes&>(it->second).hasAttrPy(nsName);
+            }
+            else {
+                const auto itPy = mAttrsPy.find(ns);
+                if (itPy != mAttrsPy.cend()) {
+                    return itPy->second.cast<DynamicAttributes>().hasAttrPy(nsName);
+                }
+                else {
+                    return false;
+                }
+            }
+        }
     }
 #endif
 
@@ -193,18 +302,38 @@ public:
         // 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) {
 #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 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);
+                }
             }
+#endif
+
+            return mAttrs.at(name).type().name();
         }
+        else {
+            const auto ns = name.substr(0, dot);
+            const auto nsName = name.substr(dot + 1);
+
+#ifdef PYBIND
+            // If attribute does not exist in C++, it might have been created in Python
+            auto it = mAttrs.find(ns);
+            if (it == mAttrs.end()) {
+                auto itPy = mAttrsPy.find(ns);
+                if (itPy != mAttrsPy.end()) {
+                    return itPy->second.cast<DynamicAttributes>().getAttrType(nsName);
+                }
+            }
 #endif
 
-        return mAttrs.at(name).type().name();
+            return future_std::any_cast<const DynamicAttributes&>(mAttrs.at(ns)).getAttrType(nsName);
+        }
     }
 
     std::set<std::string> getAttrsName() const override final {
@@ -226,7 +355,15 @@ public:
      * The strategy here is to keep a copy of each attribute in py::object that is updated everytime.
     */
     inline py::object getAttrPy(const std::string& name) const override final {
-        return mAttrsPy.at(name);
+        const auto dot = name.find('.');
+        if (dot == name.npos) {
+            return mAttrsPy.at(name);
+        }
+        else {
+            const auto ns = name.substr(0, dot);
+            const auto nsName = name.substr(dot + 1);
+            return mAttrsPy.at(ns).cast<DynamicAttributes>().getAttrPy(nsName);
+        }
     };
 #endif
 
diff --git a/unit_tests/utils/Test_DynamicAttributes.cpp b/unit_tests/utils/Test_DynamicAttributes.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c687af95d5e865328bc5f9043e65cdec510e7b22
--- /dev/null
+++ b/unit_tests/utils/Test_DynamicAttributes.cpp
@@ -0,0 +1,62 @@
+/********************************************************************************
+ * Copyright (c) 2023 CEA-List
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ ********************************************************************************/
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <string>
+#include <vector>
+
+#include "aidge/utils/DynamicAttributes.hpp"
+
+using namespace Aidge;
+
+TEST_CASE("[core/attributes] DynamicAttributes") {
+    SECTION("TestAttr") {
+        DynamicAttributes attrs;
+        attrs.addAttr("A", 1);
+        attrs.addAttr("B", 1.0f);
+        attrs.addAttr("C", std::string("test"));
+        attrs.addAttr<std::vector<bool>>("D", {false, true, false});
+
+        REQUIRE(attrs.getAttr<int>("A") == 1);
+        REQUIRE(attrs.getAttr<float>("B") == 1.0f);
+        REQUIRE(attrs.getAttr<std::string>("C") == "test");
+        REQUIRE(attrs.getAttr<std::vector<bool>>("D") == std::vector<bool>{{false, true, false}});
+
+        attrs.addAttr("E", DynamicAttributes());
+        attrs.getAttr<DynamicAttributes>("E").addAttr("E1", 1.0f);
+        attrs.getAttr<DynamicAttributes>("E").addAttr("E2", std::string("test"));
+
+        REQUIRE(attrs.getAttr<DynamicAttributes>("E").getAttr<float>("E1") == 1.0f);
+        REQUIRE(attrs.getAttr<DynamicAttributes>("E").getAttr<std::string>("E2") == "test");
+    }
+
+    SECTION("TestAttrNS") {
+        DynamicAttributes attrs;
+        attrs.addAttr("Mem.A", 1);
+        attrs.addAttr("Mem.Data.B", 1.0f);
+        attrs.addAttr("Impl.C", std::string("test"));
+        attrs.addAttr<std::vector<bool>>("D", {false, true, false});
+
+        REQUIRE(attrs.getAttr<int>("Mem.A") == 1);
+        REQUIRE(attrs.getAttr<float>("Mem.Data.B") == 1.0f);
+        REQUIRE(attrs.getAttr<std::string>("Impl.C") == "test");
+        REQUIRE(attrs.getAttr<std::vector<bool>>("D") == std::vector<bool>{{false, true, false}});
+
+        attrs.getAttr<DynamicAttributes>("Mem.Data").addAttr("E", 2.0f);
+        attrs.getAttr<DynamicAttributes>("Impl").addAttr("F", std::string("test2"));
+        REQUIRE(attrs.getAttr<float>("Mem.Data.E") == 2.0f);
+        REQUIRE(attrs.getAttr<std::string>("Impl.F") == "test2");
+
+        REQUIRE(attrs.getAttr<DynamicAttributes>("Mem.Data").getAttr<float>("B") == 1.0f);
+        REQUIRE(attrs.getAttr<DynamicAttributes>("Impl").getAttr<std::string>("C") == "test");
+    }
+}