diff --git a/aidge_core/unit_tests/test_operator_binding.py b/aidge_core/unit_tests/test_operator_binding.py
index 5b25eb7975d439816dbf91cc95b462f217fd0227..8d6f2686d9010ac4ebed80cd04f74effe763e977 100644
--- a/aidge_core/unit_tests/test_operator_binding.py
+++ b/aidge_core/unit_tests/test_operator_binding.py
@@ -73,15 +73,26 @@ class test_operator_binding(unittest.TestCase):
         self.assertEqual(attrs.get_attr("b"), "test")
         self.assertEqual(attrs.has_attr("c"), True)
         self.assertEqual(attrs.get_attr("c"), [True, False, True])
-        self.assertEqual(attrs.dict().keys(), {"a", "b", "c"})
+        self.assertEqual(attrs.dict().keys(), {"a", "b", "c", "mem", "impl"})
         self.assertEqual(attrs.has_attr("d"), False)
+        self.assertEqual(attrs.has_attr("mem.a"), True)
+        self.assertEqual(attrs.get_attr("mem.a"), 1)
+        self.assertEqual(attrs.has_attr("mem.data.b"), True)
+        self.assertEqual(attrs.get_attr("mem.data.b"), 1.0)
+        self.assertEqual(attrs.get_attr("mem").get_attr("data").get_attr("b"), 1.0)
+        self.assertEqual(attrs.has_attr("impl.c"), True)
+        self.assertEqual(attrs.get_attr("impl.c"), "test")
 
         # Add Python attributes
         attrs.add_attr("d", 18.56)
         self.assertEqual(attrs.get_attr("d"), 18.56)
         self.assertEqual(attrs.has_attr("d"), True)
-        self.assertEqual(attrs.dict().keys(), {"a", "b", "c", "d"})
+        self.assertEqual(attrs.dict().keys(), {"a", "b", "c", "d", "mem", "impl"})
         self.assertEqual(attrs.has_attr("e"), False)
+        attrs.add_attr("mem.data.c", 19.36)
+        self.assertEqual(attrs.get_attr("mem.data.c"), 19.36)
+        self.assertEqual(attrs.has_attr("mem.data.c"), True)
+        self.assertEqual(attrs.dict().keys(), {"a", "b", "c", "d", "mem", "impl"})
 
         # Check that added Python attribute is accessible in C++
         # Return the value of an attribute named "d" of type float64 (double in C++)
@@ -89,6 +100,23 @@ class test_operator_binding(unittest.TestCase):
         attrs.d = 23.89
         self.assertEqual(aidge_core.test_DynamicAttributes_binding_check(attrs), 23.89)
 
+        op = aidge_core.GenericOperatorOp("any_type", 1,0,1)
+        with self.assertRaises(RuntimeError):
+            op.attr.something
+
+        op.attr.something = aidge_core.DynamicAttributes()
+        try:
+            self.assertEqual(str(op.attr), "AttrDict({'something': AttrDict({})})")
+        except Exception:
+            self.fail("op.attr.something raised Exception unexpectedly!")
+
+        op.attr.something.arg1 = 4
+        self.assertEqual(op.attr.something.arg1, 4)
+
+        # auto create the namespace another_thing (not enabled)
+        #op.attr.another_thing.arg = 44
+        #self.assertEqual(op.attr.another_thing.arg, 44)
+
     def test_forward_dims(self):
         in_dims=[25, 25]
         input = aidge_core.Producer(in_dims, name="In")
diff --git a/include/aidge/operator/AvgPooling.hpp b/include/aidge/operator/AvgPooling.hpp
index 06ee4327e2f2d4df32c2decd73841bdf5f79a739..920829473d856b2a4c14fc0859abcd4c3b70277a 100644
--- a/include/aidge/operator/AvgPooling.hpp
+++ b/include/aidge/operator/AvgPooling.hpp
@@ -119,8 +119,8 @@ extern template class Aidge::AvgPooling_Op<4>;
 namespace {
 template <>
 const char *const EnumStrings<Aidge::AvgPoolingAttr>::data[] = {
-    "StrideDims",
-    "KernelDims"
+    "stride_dims",
+    "kernel_dims"
 };
 }
 
diff --git a/include/aidge/operator/BatchNorm.hpp b/include/aidge/operator/BatchNorm.hpp
index b5b64eb428d709e804dd9f6711530b348e0be747..08d1f6a88d394e34dd6e351f500429113a52c9fa 100644
--- a/include/aidge/operator/BatchNorm.hpp
+++ b/include/aidge/operator/BatchNorm.hpp
@@ -111,7 +111,7 @@ extern template std::shared_ptr<Aidge::Node> Aidge::BatchNorm<4>(const DimSize_t
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::BatchNormAttr>::data[] = { "Epsilon", "Momentum" };
+const char *const EnumStrings<Aidge::BatchNormAttr>::data[] = { "epsilon", "momentum" };
 }
 
 #endif //AIDGE_CORE_OPERATOR_BATCHNORM_H_
diff --git a/include/aidge/operator/Cast.hpp b/include/aidge/operator/Cast.hpp
index 6911053932afff6675be4eb2c713d8d3cd34b462..291669b7c57c14a77ffa6b40fa2aefab8d281fc7 100644
--- a/include/aidge/operator/Cast.hpp
+++ b/include/aidge/operator/Cast.hpp
@@ -93,7 +93,7 @@ inline std::shared_ptr<Node> Cast(const DataType targetType, const std::string&
 
 namespace {
 template <>
-const char* const EnumStrings<Aidge::CastAttr>::data[] = { "TargetType" };
+const char* const EnumStrings<Aidge::CastAttr>::data[] = { "target_type" };
 }
 
 #endif /* AIDGE_CORE_OPERATOR_CAST_H_ */
diff --git a/include/aidge/operator/Concat.hpp b/include/aidge/operator/Concat.hpp
index 8341a93fe66d260ae3687170629b8759d0305a9c..ab14bf527dd9949f3bb2b6157619e58c7c7580ee 100644
--- a/include/aidge/operator/Concat.hpp
+++ b/include/aidge/operator/Concat.hpp
@@ -108,7 +108,7 @@ inline std::shared_ptr<Node> Concat(const IOIndex_t nbIn, const std::int32_t axi
 namespace {
     template <>
     const char* const EnumStrings<Aidge::ConcatAttr>::data[] = {
-        "Axis"
+        "axis"
     };
 }
 
diff --git a/include/aidge/operator/Conv.hpp b/include/aidge/operator/Conv.hpp
index 87ff5854b310ca472994bd6b68fd6ae58d31e806..e89c94f968ab89f43e6ef2d95a40a6f557cc41c7 100644
--- a/include/aidge/operator/Conv.hpp
+++ b/include/aidge/operator/Conv.hpp
@@ -178,9 +178,9 @@ extern template class Aidge::Conv_Op<2>;
 namespace {
 template <>
 const char *const EnumStrings<Aidge::ConvAttr>::data[] = {
-    "StrideDims",
-    "DilationDims",
-    "KernelDims"
+    "stride_dims",
+    "dilation_dims",
+    "kernel_dims"
 };
 }
 
diff --git a/include/aidge/operator/ConvDepthWise.hpp b/include/aidge/operator/ConvDepthWise.hpp
index c8a83ff7de62a61e8125eac29d61c3938115cd09..1acf240bfcdd256953cd96b92e3622a265aafa0b 100644
--- a/include/aidge/operator/ConvDepthWise.hpp
+++ b/include/aidge/operator/ConvDepthWise.hpp
@@ -140,8 +140,8 @@ extern template class Aidge::ConvDepthWise_Op<2>;
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::ConvDepthWiseAttr>::data[] = {"StrideDims", "DilationDims",
-                                                          "KernelDims"};
+const char *const EnumStrings<Aidge::ConvDepthWiseAttr>::data[] = {"stride_dims", "dilation_dims",
+                                                          "kernel_dims"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_CONVDEPTHWISE_H_ */
diff --git a/include/aidge/operator/Fold.hpp b/include/aidge/operator/Fold.hpp
index 28127f9efe437531a64d228f7ed9c168edc39eb6..caf904e870425c000687ccd95397c92744020eec 100644
--- a/include/aidge/operator/Fold.hpp
+++ b/include/aidge/operator/Fold.hpp
@@ -133,10 +133,10 @@ extern template class Aidge::Fold_Op<2>;
 namespace {
 template <>
 const char *const EnumStrings<Aidge::FoldAttr>::data[] = {
-    "OutputDims",
-    "StrideDims",
-    "DilationDims",
-    "KernelDims"
+    "output_dims",
+    "stride_dims",
+    "dilation_dims",
+    "kernel_dims"
 };
 }
 
diff --git a/include/aidge/operator/Gather.hpp b/include/aidge/operator/Gather.hpp
index 3e9b780732fa9144f2e58bef854d1b42d063d0bf..5f3917e486e2e2188bfd23bd58a13b51d5fc7a59 100644
--- a/include/aidge/operator/Gather.hpp
+++ b/include/aidge/operator/Gather.hpp
@@ -114,7 +114,7 @@ inline std::shared_ptr<Node> Gather(std::int8_t axis = 0, const std::vector<int6
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::GatherAttr>::data[] = {"Axis", "Indices", "GatheredShape"};
+const char *const EnumStrings<Aidge::GatherAttr>::data[] = {"axis", "indices", "gathered_shape"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_GATHER_H_ */
diff --git a/include/aidge/operator/LeakyReLU.hpp b/include/aidge/operator/LeakyReLU.hpp
index 294e7ebb009ff184c9150d2aa18067a15deeba22..3057b99f70fa3693f7e434be29dcd40fb98d4bea 100644
--- a/include/aidge/operator/LeakyReLU.hpp
+++ b/include/aidge/operator/LeakyReLU.hpp
@@ -99,7 +99,7 @@ inline std::shared_ptr<Node> LeakyReLU(float negativeSlope = 0.0f, const std::st
 namespace {
 template <>
 const char* const EnumStrings<Aidge::LeakyReLUAttr>::data[]
-    = {"NegativeSlope"};
+    = {"negative_slope"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_RELU_H_ */
diff --git a/include/aidge/operator/MaxPooling.hpp b/include/aidge/operator/MaxPooling.hpp
index 082aa26bbdf1d55dcae29d1ffb2b9810db8b17d0..7e2c68681e645133812103a94e4c39ab9d1dc970 100644
--- a/include/aidge/operator/MaxPooling.hpp
+++ b/include/aidge/operator/MaxPooling.hpp
@@ -156,7 +156,7 @@ inline std::shared_ptr<Node> MaxPooling(
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::MaxPoolingAttr>::data[] = {"StrideDims", "KernelDims", "CeilMode"};
+const char *const EnumStrings<Aidge::MaxPoolingAttr>::data[] = {"stride_dims", "kernel_dims", "ceil_mode"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_MAXPOOLING_H_ */
diff --git a/include/aidge/operator/Memorize.hpp b/include/aidge/operator/Memorize.hpp
index d6af56f2faad18b9e39c793ea68e39eac4dd2f01..bb652e833ad06df37f55d3582afd0e66cc3e97c8 100644
--- a/include/aidge/operator/Memorize.hpp
+++ b/include/aidge/operator/Memorize.hpp
@@ -113,9 +113,9 @@ inline std::shared_ptr<Node> Memorize(const std::uint32_t endStep, const std::st
 namespace {
 template <>
 const char *const EnumStrings<Aidge::MemorizeAttr>::data[] = {
-    "ScheduleStep",
-    "ForwardStep",
-    "EndStep"
+    "schedule_step",
+    "forward_step",
+    "end_step"
 };
 }
 
diff --git a/include/aidge/operator/Pad.hpp b/include/aidge/operator/Pad.hpp
index 5fd0f93986206e6cd958a85055159783eeb8bc8f..215fafb7fee10587dec38e77685d705f7c1bb980 100644
--- a/include/aidge/operator/Pad.hpp
+++ b/include/aidge/operator/Pad.hpp
@@ -139,7 +139,7 @@ extern template class Aidge::Pad_Op<2>;
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::PadAttr>::data[] = {"BeginEndBorders", "BorderType", "BorderValue"};
+const char *const EnumStrings<Aidge::PadAttr>::data[] = {"begin_end_borders", "border_type", "border_value"};
 
 template <>
 const char *const EnumStrings<Aidge::PadBorderType>::data[] = {"Constant", "Edge", "Reflect", "Wrap"};
diff --git a/include/aidge/operator/Pop.hpp b/include/aidge/operator/Pop.hpp
index 575d56b455940ea98571110dbaa9a83de09fef37..fb3b32eeacf2e199df88b6bd0256cf6cbdaa1065 100644
--- a/include/aidge/operator/Pop.hpp
+++ b/include/aidge/operator/Pop.hpp
@@ -100,7 +100,7 @@ inline std::shared_ptr<Node> Pop(const std::string& name = "") {
 namespace {
 template <>
 const char *const EnumStrings<Aidge::PopAttr>::data[] = {
-    "ForwardStep"
+    "forward_step"
 };
 }
 
diff --git a/include/aidge/operator/Producer.hpp b/include/aidge/operator/Producer.hpp
index 6b37fbd13b6e29f4b11a9e1628da9dbd7b35bb35..1647c563d38ab4931cc3a0c2a4281555215f990e 100644
--- a/include/aidge/operator/Producer.hpp
+++ b/include/aidge/operator/Producer.hpp
@@ -160,7 +160,7 @@ std::shared_ptr<Node> addProducer(std::shared_ptr<Node>& otherNode, const IOInde
 namespace {
 template <>
 const char *const EnumStrings<Aidge::ProdAttr>::data[] = {
-    "Constant"
+    "constant"
 };
 }
 #endif /* AIDGE_CORE_OPERATOR_PRODUCER_H_ */
diff --git a/include/aidge/operator/ReduceMean.hpp b/include/aidge/operator/ReduceMean.hpp
index 3fcf19ffd13645fb28b6efcfefaf8e347b148c89..000607c60e4e3c85671e70a941bd11f3427333dd 100644
--- a/include/aidge/operator/ReduceMean.hpp
+++ b/include/aidge/operator/ReduceMean.hpp
@@ -127,7 +127,7 @@ inline std::shared_ptr<Node> ReduceMean(const std::vector<std::int32_t> &axes,
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::ReduceMeanAttr>::data[] = {"Axes", "KeepDims"};
+const char *const EnumStrings<Aidge::ReduceMeanAttr>::data[] = {"axes", "keep_dims"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_REDUCEMEAN_H_ */
diff --git a/include/aidge/operator/Reshape.hpp b/include/aidge/operator/Reshape.hpp
index 4ea0cca30089555ff7979f141f94e5c84f04ffa1..29a08c76c248018fff87a5f765a0b62cbd23b6b7 100644
--- a/include/aidge/operator/Reshape.hpp
+++ b/include/aidge/operator/Reshape.hpp
@@ -107,7 +107,7 @@ inline std::shared_ptr<Node> Reshape(const std::vector<std::int64_t>& shape = {}
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::ReshapeAttr>::data[] = { "Shape", "AllowZero" };
+const char *const EnumStrings<Aidge::ReshapeAttr>::data[] = { "shape", "allow_zero" };
 }
 
 #endif /* AIDGE_CORE_OPERATOR_RESHAPE_H_ */
diff --git a/include/aidge/operator/Scaling.hpp b/include/aidge/operator/Scaling.hpp
index 7d8e11b31546cd87a8d6b2d36e2929c9ef6df7a2..0683a26f6e9d8ef462c2af4693f372b43c33a144 100644
--- a/include/aidge/operator/Scaling.hpp
+++ b/include/aidge/operator/Scaling.hpp
@@ -105,7 +105,7 @@ inline std::shared_ptr<Node> Scaling(float scalingFactor = 1.0f,
 namespace {
 template <>
 const char* const EnumStrings<Aidge::ScalingAttr>::data[]
-    = {"ScalingFactor", "QuantizedNbBits", "IsOutputUnsigned"};
+    = {"scaling_factor", "quantized_nb_bits", "is_output_unsigned"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_SCALING_H_ */
diff --git a/include/aidge/operator/Shape.hpp b/include/aidge/operator/Shape.hpp
index 6d2d1b5e7c212fafa5ad6457d9e0a260e96b1c90..94f237726e79d8fe7824ff2c9b2f7640bbfc716f 100644
--- a/include/aidge/operator/Shape.hpp
+++ b/include/aidge/operator/Shape.hpp
@@ -105,7 +105,7 @@ inline std::shared_ptr<Node> Shape(const std::int64_t start = 0, const std::int6
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::ShapeAttr>::data[] = {"Start", "End"};
+const char *const EnumStrings<Aidge::ShapeAttr>::data[] = {"start", "end"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_SHAPE_H_ */
diff --git a/include/aidge/operator/Slice.hpp b/include/aidge/operator/Slice.hpp
index 7d425a0f3589e74b54ee0834fdc4291ea7f49bad..04a67fe98f7682737bff6df18f28d568ee33e093 100644
--- a/include/aidge/operator/Slice.hpp
+++ b/include/aidge/operator/Slice.hpp
@@ -115,7 +115,7 @@ inline std::shared_ptr<Node> Slice(const std::vector<std::int64_t>& starts = {},
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::SliceAttr>::data[] = { "Starts", "Ends", "Axes", "Steps" };
+const char *const EnumStrings<Aidge::SliceAttr>::data[] = { "starts", "ends", "axes", "steps" };
 }
 
 #endif /* AIDGE_CORE_OPERATOR_RELU_H_ */
diff --git a/include/aidge/operator/Softmax.hpp b/include/aidge/operator/Softmax.hpp
index 70f3a561ae5c9ba4720de8419bcd5aaf32a51e47..0b7a8e57193439872c6fcc2699b9f5e55c643961 100644
--- a/include/aidge/operator/Softmax.hpp
+++ b/include/aidge/operator/Softmax.hpp
@@ -92,7 +92,7 @@ inline std::shared_ptr<Node> Softmax(std::int32_t axis, const std::string& name
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::SoftmaxAttr>::data[] = {"Axis"};
+const char *const EnumStrings<Aidge::SoftmaxAttr>::data[] = {"axis"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_SOFTMAX_H_ */
diff --git a/include/aidge/operator/Split.hpp b/include/aidge/operator/Split.hpp
index 42baf66e6722c6f9a0d3f40f12d4f4685fcc6980..7bdec1579c8a8f46640de5caf42c01568d208059 100644
--- a/include/aidge/operator/Split.hpp
+++ b/include/aidge/operator/Split.hpp
@@ -34,20 +34,24 @@ enum class SplitAttr { Axis, Split };
 
 class Split_Op
     : public OperatorTensor,
-      public Registrable<Split_Op, std::string, std::shared_ptr<OperatorImpl>(const Split_Op &)>,
-      public StaticAttributes<SplitAttr, std::int8_t, std::vector<DimSize_t>> {
+      public Registrable<Split_Op, std::string, std::shared_ptr<OperatorImpl>(const Split_Op &)> {
 
 public:
     static const std::string Type;
 
+private:
+    using Attributes_ = StaticAttributes<SplitAttr, std::int8_t, std::vector<DimSize_t>>;
+    template <SplitAttr e> using attr = typename Attributes_::template attr<e>;
+    const std::shared_ptr<Attributes_> mAttributes;
+
+public:
     Split_Op() = delete;
 
-    using Attributes_ = StaticAttributes<SplitAttr,  std::int8_t, std::vector<DimSize_t>>;
-    template <SplitAttr e> using attr = typename Attributes_::template attr<e>;
     Split_Op( std::int8_t axis, DimSize_t nbOutputs, const std::vector<DimSize_t>& split)
         : OperatorTensor(Type, {InputCategory::Data, InputCategory::OptionalData}, nbOutputs),
-          Attributes_(attr<SplitAttr::Axis>(axis),
-                      attr<SplitAttr::Split>(split))
+          mAttributes(std::make_shared<Attributes_>(
+            attr<SplitAttr::Axis>(axis),
+            attr<SplitAttr::Split>(split)))
     {
         mImpl = std::make_shared<Split_OpImpl>(*this);
     }
@@ -60,7 +64,7 @@ public:
      */
     Split_Op(const Split_Op &op)
         : OperatorTensor(op),
-          Attributes_(op)
+          mAttributes(op.mAttributes)
     {
         if (!op.backend().empty()) {
             SET_IMPL_MACRO(Split_Op, *this, op.backend());
@@ -81,6 +85,10 @@ public:
 
     void setBackend(const std::string &name, DeviceIdx_t device = 0) override;
 
+    inline std::shared_ptr<Attributes> attributes() const override { return mAttributes; }
+    inline std::int8_t& axis() const { return mAttributes->template getAttr<SplitAttr::Axis>(); }
+    inline std::vector<DimSize_t>& split() const { return mAttributes->template getAttr<SplitAttr::Split>(); }
+
     static const std::vector<std::string> getInputsName(){
         return {"data_input", "split"};
     }
@@ -105,7 +113,7 @@ inline std::shared_ptr<Node> Split(DimSize_t nbOutput,
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::SplitAttr>::data[] = { "Axis", "Split" };
+const char *const EnumStrings<Aidge::SplitAttr>::data[] = { "axis", "split" };
 }
 
 #endif /* AIDGE_CORE_OPERATOR_SPLIT_H_ */
diff --git a/include/aidge/operator/Transpose.hpp b/include/aidge/operator/Transpose.hpp
index 72096448ebf0e00d73e33bdab094ca7f0b7d0633..efd9e1792d530f45754809913a7c648d82c7985e 100644
--- a/include/aidge/operator/Transpose.hpp
+++ b/include/aidge/operator/Transpose.hpp
@@ -105,7 +105,7 @@ inline std::shared_ptr<Node> Transpose(const std::vector<DimSize_t> &outputDimsO
 
 namespace {
 template <>
-const char *const EnumStrings<Aidge::TransposeAttr>::data[] = {"OutputDimsOrder"};
+const char *const EnumStrings<Aidge::TransposeAttr>::data[] = {"output_dims_order"};
 }
 
 #endif /* AIDGE_CORE_OPERATOR_TRANSPOSE_H_ */
diff --git a/include/aidge/operator/Unfold.hpp b/include/aidge/operator/Unfold.hpp
index 169fbb05ebeff0e5d38eb9606133d6279cc31cd8..58cbcd2d756ad44ef2ec6a38d46909a114b187c2 100644
--- a/include/aidge/operator/Unfold.hpp
+++ b/include/aidge/operator/Unfold.hpp
@@ -137,9 +137,9 @@ extern template class Aidge::Unfold_Op<2>;
 namespace {
 template <>
 const char *const EnumStrings<Aidge::UnfoldAttr>::data[] = {
-    "StrideDims",
-    "DilationDims",
-    "KernelDims"
+    "stride_dims",
+    "dilation_dims",
+    "kernel_dims"
 };
 }
 
diff --git a/include/aidge/utils/Attributes.hpp b/include/aidge/utils/Attributes.hpp
index c1f6a8a7f704b4bd813983cb178d9e5acba5a5e1..7dce3d327d42de15dc2589788b4643742ed1a463 100644
--- a/include/aidge/utils/Attributes.hpp
+++ b/include/aidge/utils/Attributes.hpp
@@ -42,32 +42,6 @@ constexpr std::size_t size(T (&)[N]) { return N; }
 *  Attributes in the binding code.
 */
 class Attributes {
-protected:
-    /**
-     * @brief Convert snake_case to PascalCase.
-     * @param snakeCase string to convert.
-    */
-    static std::string snakeToPascal(const std::string& snakeCase);
-
-
-    /**
-     * @brief Convert PascalCase to snake_case.
-     * @param pascalCase string to convert.
-    */
-    static std::string pascalToSnake(const std::string& pascalCase);
-
-    /**
-     * @brief Check whether a given string is in PascalCase.
-     * @param str String to check.
-     */
-    static bool isPascalCase(const std::string& str);
-
-    /**
-     * @brief Check whether a given string is in snake_case.
-     * @param str String to check.
-     */
-    static bool isSnakeCase(const std::string& str);
-
 public:
     /**
      * @brief Check if the attribute exists.
diff --git a/include/aidge/utils/DynamicAttributes.hpp b/include/aidge/utils/DynamicAttributes.hpp
index c5054eb2fd2e8bfa5e7fca898f343ce630643dbd..cf7f048dbe5999f433277c46e4e3cb9798c43674 100644
--- a/include/aidge/utils/DynamicAttributes.hpp
+++ b/include/aidge/utils/DynamicAttributes.hpp
@@ -46,40 +46,35 @@ 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.");
+        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(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>())));
+            // 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<T&>(mAttrs.at(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>())));
-            }
+            return future_std::any_cast<const T&>(mAttrs.at(name));
         }
-#endif
+        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&>(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 +83,26 @@ 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) {
+            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
-            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
+                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);
+        }
     }
 
     ///\brief Set an Attribute value, identified by its name. If it already exists, its value (and type, if different) is changed.
@@ -107,56 +111,94 @@ 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())));
+            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
     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) {
+            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()));
 
-        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) {
+            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));
+        }
     }
 
     py::dict dict() const override {
         py::dict attributes;
+        for (const auto& elt : mAttrs) {
+            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) {
-            const std::string snakeName = pascalToSnake(elt.first);
-            attributes[snakeName.c_str()] = elt.second;
+            attributes[elt.first.c_str()] = elt.second;
         }
         return attributes;
     }
@@ -177,15 +219,46 @@ 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) {
+#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);
+            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) {
+            // 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
 
@@ -193,18 +266,26 @@ 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();
+            return mAttrs.at(name).type().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)).getAttrType(nsName);
+        }
     }
 
     std::set<std::string> getAttrsName() const override final {
@@ -226,7 +307,24 @@ 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) {
+            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;
+            }
+        }
+        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)).getAttrPy(nsName);
+        }
     };
 #endif
 
diff --git a/include/aidge/utils/StaticAttributes.hpp b/include/aidge/utils/StaticAttributes.hpp
index 8fc88ff79c50751ba7b79662fc9fc430d4ed601d..3bb41b5bb0d9c2727d95a2656a1a2d5b96ff950b 100644
--- a/include/aidge/utils/StaticAttributes.hpp
+++ b/include/aidge/utils/StaticAttributes.hpp
@@ -180,7 +180,6 @@ public:
     //////////////////////////////////////
     // Runtime existance check with name
     bool hasAttr(const std::string& name) const override final {
-        AIDGE_ASSERT(isPascalCase(name), "Aidge standard requires PascalCase for C++ Attributes.");
         for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
             if (name == EnumStrings<ATTRS_ENUM>::data[i]) {
                 return true;
@@ -192,10 +191,8 @@ public:
 
 #ifdef PYBIND
         bool hasAttrPy(const std::string& name) const override final {
-        AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python, got '{}'.", name);
-        const std::string pascalName = snakeToPascal(name);
         for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
-            if (pascalName == EnumStrings<ATTRS_ENUM>::data[i]) {
+            if (name == EnumStrings<ATTRS_ENUM>::data[i]) {
                 return true;
             }
         }
@@ -234,7 +231,7 @@ public:
     static std::set<std::string> staticGetAttrsName() {
         std::set<std::string> attrsName;
         for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
-            attrsName.insert(pascalToSnake(std::string(EnumStrings<ATTRS_ENUM>::data[i])));
+            attrsName.insert(std::string(EnumStrings<ATTRS_ENUM>::data[i]));
         }
         return attrsName;
     }
@@ -244,10 +241,9 @@ public:
         if (name == "__dict__") {
             return py::none();
         }
-        AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python, got '{}'.", name);
-        const std::string pascalName = snakeToPascal(name);
+
         for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
-            if (pascalName == EnumStrings<ATTRS_ENUM>::data[i]) {
+            if (name == EnumStrings<ATTRS_ENUM>::data[i]) {
                 // https://github.com/pybind/pybind11/blob/f3e0602802c7840992c97f4960515777cad6a5c7/include/pybind11/pytypes.h#L1119-L1138
                 // Normal accessor would not work as we convert the tuple to a py::object which can be anything
                 return py::detail::accessor_policies::tuple_item::get(py::cast(mAttrs), static_cast<py::size_t>(i));
@@ -264,10 +260,8 @@ public:
 
 
     void setAttrPy(const std::string& name, py::object&& value) override final{
-        AIDGE_ASSERT(isSnakeCase(name), "Aidge standard requires snake_case for Attributes with Python, got '{}'.", name);
-        const std::string pascalName = snakeToPascal(name);
         for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
-            if (pascalName == EnumStrings<ATTRS_ENUM>::data[i]) {
+            if (name == EnumStrings<ATTRS_ENUM>::data[i]) {
                 // Cannot update attribute using reference has it would require templating
                 // Use a dirty
                 auto tmpAttr = py::cast(mAttrs);
@@ -282,10 +276,9 @@ public:
     py::dict dict() const override {
         py::dict attributes;
         for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
-            const std::string snakeName = pascalToSnake(EnumStrings<ATTRS_ENUM>::data[i]);
-                // https://github.com/pybind/pybind11/blob/f3e0602802c7840992c97f4960515777cad6a5c7/include/pybind11/pytypes.h#L1119-L1138
-                // Normal accessor would not work as we convert the tuple to a py::object which can be anything
-            attributes[snakeName.c_str()] = py::detail::accessor_policies::tuple_item::get(py::cast(mAttrs), static_cast<py::size_t>(i));
+            // https://github.com/pybind/pybind11/blob/f3e0602802c7840992c97f4960515777cad6a5c7/include/pybind11/pytypes.h#L1119-L1138
+            // Normal accessor would not work as we convert the tuple to a py::object which can be anything
+            attributes[EnumStrings<ATTRS_ENUM>::data[i]] = py::detail::accessor_policies::tuple_item::get(py::cast(mAttrs), static_cast<py::size_t>(i));
         }
         return attributes;
     }
@@ -306,8 +299,7 @@ public:
     // AttrDict get_a() const {
     //     py::dict attributes_;
     //     for (std::size_t i = 0; i < size(EnumStrings<ATTRS_ENUM>::data); ++i) {
-    //         const std::string snakeName = pascalToSnake(std::string(EnumStrings<ATTRS_ENUM>::data[i]));
-    //         attributes_[snakeName.c_str()] = py::detail::accessor_policies::tuple_item::get(py::cast(mAttrs), static_cast<py::size_t>(i));
+    //         attributes_[EnumStrings<ATTRS_ENUM>::data[i]] = py::detail::accessor_policies::tuple_item::get(py::cast(mAttrs), static_cast<py::size_t>(i));
     //     }
     //     return AttrDict(attributes_);
     // }
diff --git a/python_binding/operator/pybind_Split.cpp b/python_binding/operator/pybind_Split.cpp
index 6efc123864f21bf8ea02008b29fe59f31685f17c..f63a01f9815aa59cfbad0aea36f148899f44c9ea 100644
--- a/python_binding/operator/pybind_Split.cpp
+++ b/python_binding/operator/pybind_Split.cpp
@@ -21,14 +21,13 @@ namespace py = pybind11;
 namespace Aidge {
 
 void init_Split(py::module& m) {
-    py::class_<Split_Op, std::shared_ptr<Split_Op>, Attributes, OperatorTensor>(m, "SplitOp", py::multiple_inheritance())
+    py::class_<Split_Op, std::shared_ptr<Split_Op>, OperatorTensor>(m, "SplitOp", py::multiple_inheritance())
         .def(py::init<DimSize_t, std::int8_t, std::vector<DimSize_t>&>(),
                 py::arg("nb_outputs"),
                 py::arg("axis"),
                 py::arg("split"))
         .def_static("get_inputs_name", &Split_Op::getInputsName)
-        .def_static("get_outputs_name", &Split_Op::getOutputsName)
-        .def_static("attributes_name", &Split_Op::staticGetAttrsName);
+        .def_static("get_outputs_name", &Split_Op::getOutputsName);
 
     declare_registrable<Split_Op>(m, "SplitOp");
 
diff --git a/python_binding/utils/pybind_Attributes.cpp b/python_binding/utils/pybind_Attributes.cpp
index 7f5dde63c4835eb694d5fd2d571d7c9c1fd5a9ac..bc0ccb3f4053e37c186acd919fcadae9d5d19a40 100644
--- a/python_binding/utils/pybind_Attributes.cpp
+++ b/python_binding/utils/pybind_Attributes.cpp
@@ -21,14 +21,17 @@ namespace Aidge {
 
 DynamicAttributes test_DynamicAttributes_binding() {
     DynamicAttributes attrs;
-    attrs.addAttr<int>("A", 42);
-    attrs.addAttr<std::string>("B", "test");
-    attrs.addAttr<std::vector<bool>>("C", {true, false, true});
+    attrs.addAttr<int>("a", 42);
+    attrs.addAttr<std::string>("b", "test");
+    attrs.addAttr<std::vector<bool>>("c", {true, false, true});
+    attrs.addAttr("mem.a", 1);
+    attrs.addAttr("mem.data.b", 1.0f);
+    attrs.addAttr("impl.c", std::string("test"));
     return attrs;
 }
 
 double test_DynamicAttributes_binding_check(DynamicAttributes& attrs) {
-    return attrs.getAttr<double>("D");
+    return attrs.getAttr<double>("d");
 }
 
 void init_Attributes(py::module& m){
@@ -44,6 +47,7 @@ void init_Attributes(py::module& m){
 
 
     py::class_<DynamicAttributes, std::shared_ptr<DynamicAttributes>, Attributes>(m, "DynamicAttributes")
+    .def(py::init<>())
     .def("add_attr", &DynamicAttributes::addAttrPy, py::arg("name"), py::arg("value"))
     .def("del_attr", &DynamicAttributes::delAttr, py::arg("name"));
 
diff --git a/src/operator/Shape.cpp b/src/operator/Shape.cpp
index 8166712e1e5fd967bb9328e95ecf8c5388636ba7..39f5e2fe09b7ac750b8ea9d48d17fc2e97013c1a 100644
--- a/src/operator/Shape.cpp
+++ b/src/operator/Shape.cpp
@@ -34,17 +34,17 @@ const std::string Aidge::Shape_Op::Type = "Shape";
 
 bool Aidge::Shape_Op::forwardDims(bool /*allowDataDependency*/) {
     if (inputsAssociated()) {
-        if (mAttributes->template getAttr<std::int64_t>("Start") < 0)
-            mAttributes->template getAttr<std::int64_t>("Start") += static_cast<std::int64_t>(getInput(0)->nbDims());
-        if (mAttributes->template getAttr<std::int64_t>("End") < 0)
-            mAttributes->template getAttr<std::int64_t>("End") += static_cast<std::int64_t>(getInput(0)->nbDims());
+        if (this->start() < 0)
+            this->start() += static_cast<std::int64_t>(getInput(0)->nbDims());
+        if (this->end() < 0)
+            this->end() += static_cast<std::int64_t>(getInput(0)->nbDims());
 
-        const auto start = mAttributes->template getAttr<std::int64_t>("Start");
-        const auto end = mAttributes->template getAttr<std::int64_t>("End");
+        const auto start = this->start();
+        const auto end = this->end();
         const auto nbDims = static_cast<std::int64_t>(getInput(0)->nbDims());
         const DimSize_t roi = end - start + 1;
 
-        AIDGE_ASSERT(start < nbDims && end < nbDims, "'Start' and 'End' must be < {}", nbDims);
+        AIDGE_ASSERT(start < nbDims && end < nbDims, "'start' and 'end' must be < {}", nbDims);
         AIDGE_ASSERT(roi> 1, "Unvalid ROI for Shape");
 
         mOutputs[0]->resize({roi});
diff --git a/src/operator/Split.cpp b/src/operator/Split.cpp
index 31de75e410afef98843d1f59a1221ecd3ba91832..af7474d8a21db9ece237440b46ecf57db9b270b4 100644
--- a/src/operator/Split.cpp
+++ b/src/operator/Split.cpp
@@ -28,8 +28,8 @@
 
 void Aidge::Split_OpImpl::forward() {
     const Split_Op& op = dynamic_cast<const Split_Op&>(mOp);
-    const auto axis = op.template getAttr<std::int8_t>("Axis");
-    const auto splits = op.template getAttr<std::vector<DimSize_t>>("Split");
+    const auto axis = op.axis();
+    const auto splits = op.split();
     const auto dims = op.getInput(0)->dims();
 
     //Compute pre/post axis strides
@@ -68,7 +68,7 @@ bool Aidge::Split_Op::forwardDims(bool allowDataDependency) {
     if (inputsAssociated()) {
         // Copy optional input #1, if present, to attribute Split
         if (getInput(1)) {
-            if (!this->template getAttr<SplitAttr::Split>().empty()) {
+            if (!this->split().empty()) {
                 Log::notice("Split_Op: ignoring non-empty Split attribute because input#1 takes precedence");
             }
 
@@ -78,21 +78,22 @@ bool Aidge::Split_Op::forwardDims(bool allowDataDependency) {
             }
 
             std::shared_ptr<Tensor> fallback;
-            this->template getAttr<SplitAttr::Split>().reserve(getInput(1)->size());
+            this->split().clear(); // If both are provided input would override attrs
+            this->split().reserve(getInput(1)->size());
             const auto& splits = getInput(1)->refCastFrom(fallback, NativeType<DimSize_t>::type, "cpu");
             std::copy_n(static_cast<DimSize_t*>(splits.getImpl()->hostPtr()),
                         splits.size(),
-                        std::back_inserter(this->template getAttr<SplitAttr::Split>()));
+                        std::back_inserter(this->split()));
         }
 
         // Compute output dims
-        if (this->template getAttr<std::int8_t>("Axis") < 0)
-            this->template getAttr<std::int8_t>("Axis") += static_cast<std::int8_t>(getInput(0)->nbDims());
+        if (this->axis() < 0)
+            this->axis() += static_cast<std::int8_t>(getInput(0)->nbDims());
 
-        DimSize_t dimToSplit = getInput(0)->dims()[this->template getAttr<std::int8_t>("Axis")];
+        DimSize_t dimToSplit = getInput(0)->dims()[this->axis()];
         DimSize_t nbOutput = this->nbOutputs();
         // Fill Split attr if empty
-        if(this->template getAttr<SplitAttr::Split>().empty()) {
+        if(this->split().empty()) {
             // In case the input Split is not provided, divide the dimension of Axis into equal slices
             AIDGE_ASSERT(dimToSplit > nbOutput, "Split_Op: Output number {} musn't be bigger than dimension {}.", nbOutput, dimToSplit);
             DimSize_t baseSliceSize = dimToSplit / nbOutput;
@@ -100,12 +101,12 @@ bool Aidge::Split_Op::forwardDims(bool allowDataDependency) {
             DimSize_t remainder = dimToSplit % nbOutput;
 
             for (DimSize_t i = 0; i < static_cast<DimSize_t>(nbOutput -1); ++i) {
-                    this->template getAttr<SplitAttr::Split>().push_back(baseSliceSize);
+                    this->split().push_back(baseSliceSize);
             }
-            this->template getAttr<SplitAttr::Split>().push_back(baseSliceSize + remainder);
+            this->split().push_back(baseSliceSize + remainder);
         }
 
-        const auto splits = this->template getAttr<SplitAttr::Split>();
+        const auto splits = this->split();
         AIDGE_ASSERT(splits.size() == nbOutput, "Split_Op: number of slices {} must be equal to number of outputs {}", splits, nbOutput);
         DimSize_t totalSplitSize = std::accumulate(splits.cbegin(), splits.cend(), 0);
         AIDGE_ASSERT(totalSplitSize == dimToSplit, "Split_Op: Total chunks size {} is different from dimension size {}.", totalSplitSize, dimToSplit);
@@ -113,7 +114,7 @@ bool Aidge::Split_Op::forwardDims(bool allowDataDependency) {
         std::vector<DimSize_t> outDims = getInput(0)->dims();
         for (std::size_t i = 0; i < nbOutput; ++i)
         {
-            outDims[this->template getAttr<std::int8_t>("Axis")] = this->template getAttr<SplitAttr::Split>()[i];
+            outDims[this->axis()] = this->split()[i];
             mOutputs[i]->resize(outDims);
         }
 
diff --git a/src/utils/Attributes.cpp b/src/utils/Attributes.cpp
deleted file mode 100644
index e79db53a60a955e3502e070cda5818d3d7b6c922..0000000000000000000000000000000000000000
--- a/src/utils/Attributes.cpp
+++ /dev/null
@@ -1,96 +0,0 @@
-/********************************************************************************
- * 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 "aidge/utils/Attributes.hpp"
-
-#include <cctype>  // std::isdigit, std::islower, std::isupper, std::tolower,
-                   // std::toupper
-#include <string>
-
-std::string Aidge::Attributes::snakeToPascal(const std::string& snakeCase) {
-    std::string result;
-    bool to_upper = true; // Start with uppercase for PascalCase
-
-    for (char ch : snakeCase) {
-        if (ch == '_') {
-            to_upper = true; // Next character should be uppercase
-        } else {
-            if (to_upper) {
-                result += std::toupper(ch);
-                to_upper = false; // Reset flag after making a character uppercase
-            } else {
-                result += ch;
-            }
-        }
-    }
-    return result;
-}
-
-std::string Aidge::Attributes::pascalToSnake(const std::string& pascalCase) {
-    std::string result;
-
-    for (char ch : pascalCase) {
-        if (std::isupper(ch)) {
-            if (!result.empty()) {
-                result += '_';
-            }
-            result += std::tolower(ch);
-        } else {
-            result += ch;
-        }
-    }
-    return result;
-}
-
-bool Aidge::Attributes::isPascalCase(const std::string& str) {
-    if (str.empty() || !std::isupper(str[0])) {
-        return false;
-    }
-
-    bool expectUpper = false;
-    for (size_t i = 1; i < str.size(); ++i) {
-        if (str[i] == '_') {
-            return false;
-        }
-        if (std::isupper(str[i])) {
-            if (!expectUpper) {
-                return false;
-            }
-            expectUpper = false;
-        } else if (std::islower(str[i]) || std::isdigit(str[i])) {
-            expectUpper = true;
-        } else {
-            return false;
-        }
-    }
-    return true;
-}
-
-bool Aidge::Attributes::isSnakeCase(const std::string& str) {
-    if (str.empty()) {
-        return false;
-    }
-
-    bool lastCharWasUnderscore = false;
-    for (char ch : str) {
-        if (ch == '_') {
-            if (lastCharWasUnderscore) {
-                return false;
-            }
-            lastCharWasUnderscore = true;
-        } else if (!std::islower(ch) && !std::isdigit(ch)) {
-            return false;
-        } else {
-            lastCharWasUnderscore = false;
-        }
-    }
-    return true;
-}
diff --git a/unit_tests/utils/Test_DynamicAttributes.cpp b/unit_tests/utils/Test_DynamicAttributes.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b8a1264b3ad954e776a5ae4c47f03cd0c3fb82c9
--- /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");
+    }
+}