diff --git a/include/aidge/graph/GraphView.hpp b/include/aidge/graph/GraphView.hpp
index 3311797d858cf4899a6cfed7a18fb9840afb514e..46fa56ef0e7d63ce10bb3c96a8d7e1c42b191322 100644
--- a/include/aidge/graph/GraphView.hpp
+++ b/include/aidge/graph/GraphView.hpp
@@ -62,10 +62,10 @@ public:
         return mNodes == gv.mNodes;
     }
 
-    NodePtr operator[](const std::string& name)
+    NodePtr operator[](const std::string& nodeName)
     {
-        assert(mNodeRegistry.find(name) != mNodeRegistry.end() && "Could not find Node in the GraphView.");
-        return mNodeRegistry.at(name);
+        AIDGE_ASSERT(mNodeRegistry.find(nodeName) != mNodeRegistry.end(), "No node named {} in graph {}.", nodeName, name());
+        return mNodeRegistry.at(nodeName);
     }
 
 ///////////////////////////////////////////////////////
@@ -379,11 +379,10 @@ public:
      * @param toTensor Input Tensor ID of the new Node. Default to gk_IODefaultIndex, meaning
      * first available data input for the Node.
      */
-    inline void addChild(NodePtr toOtherNode, std::string fromOutNodeName,
+    inline void addChild(NodePtr toOtherNode, const std::string& fromOutNodeName,
                          const IOIndex_t fromTensor = IOIndex_t(0),
                          IOIndex_t toTensor = gk_IODefaultIndex) {
-        assert(mNodeRegistry.find(fromOutNodeName) != mNodeRegistry.end() &&
-               "No Node with this name found in the GraphView.");
+        AIDGE_ASSERT(mNodeRegistry.find(fromOutNodeName) != mNodeRegistry.end(), "No node named {} in graph {}.", fromOutNodeName, name());
         addChild(toOtherNode, mNodeRegistry.at(fromOutNodeName), fromTensor, toTensor);
     }
 
@@ -524,7 +523,6 @@ private:
     //        TOPOLOGY
     ///////////////////////////////////////////////////////
 
-    void _forwardDims(std::set<NodePtr> listNodes);
 };
 
 /**
diff --git a/include/aidge/utils/ErrorHandling.hpp b/include/aidge/utils/ErrorHandling.hpp
index 653a774b92e26513c9ac555e0aec1daed793e208..d4235d2db9b06597df80966e67306d84ac814a3c 100644
--- a/include/aidge/utils/ErrorHandling.hpp
+++ b/include/aidge/utils/ErrorHandling.hpp
@@ -18,13 +18,15 @@
 #include <fmt/format.h>
 #include <fmt/ranges.h>
 
+#include "aidge/utils/Log.hpp"
+
 #ifdef NO_EXCEPTION
 #define AIDGE_THROW_OR_ABORT(ex, ...) \
-do { fmt::print(__VA_ARGS__); std::abort(); } while (false)
+do { Aidge::Log::fatal(__VA_ARGS__); std::abort(); } while (false)
 #else
 #include <stdexcept>
 #define AIDGE_THROW_OR_ABORT(ex, ...) \
-throw ex(fmt::format(__VA_ARGS__))
+do { Aidge::Log::fatal(__VA_ARGS__); throw ex(fmt::format(__VA_ARGS__)); } while (false)
 #endif
 
 /**
@@ -33,7 +35,7 @@ throw ex(fmt::format(__VA_ARGS__))
  * If it asserts, it means an user error.
 */
 #define AIDGE_ASSERT(stm, ...) \
-if (!(stm)) { fmt::print("Assertion failed: " #stm " in {}:{}", __FILE__, __LINE__); \
+if (!(stm)) { Aidge::Log::error("Assertion failed: " #stm " in {}:{}", __FILE__, __LINE__); \
     AIDGE_THROW_OR_ABORT(std::runtime_error, __VA_ARGS__); }
 
 /**
diff --git a/include/aidge/utils/Log.hpp b/include/aidge/utils/Log.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8a18bbab34d3c1c86252833852abc5faca41dd96
--- /dev/null
+++ b/include/aidge/utils/Log.hpp
@@ -0,0 +1,148 @@
+/********************************************************************************
+ * 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
+ *
+ ********************************************************************************/
+
+
+#ifndef AIDGE_LOG_H_
+#define AIDGE_LOG_H_
+
+#include <memory>
+
+#include <fmt/format.h>
+#include <fmt/ranges.h>
+
+namespace Aidge {
+/**
+ * Aidge logging class, for displaying and file logging of events.
+*/
+class Log {
+public:
+    enum Level {
+        Debug = 0,
+        Info,
+        Notice,
+        Warn,
+        Error,
+        Fatal
+    };
+
+    /**
+     * Detailed messages for debugging purposes, providing information helpful 
+     * for developers to trace and identify issues.
+     * Detailed insights of what is appening in an operation, not useful for the
+     * end-user. The operation is performed nominally.
+     * @note This level is disabled at compile time for Release, therefore
+     * inducing no runtime overhead for Release.
+    */
+    template <typename... Args>
+    constexpr static void debug(Args&&... args) {
+#ifndef NDEBUG
+        // only when compiled in Debug
+        log(Debug, fmt::format(std::forward<Args>(args)...));
+#endif
+    }
+
+    /**
+     * Messages that provide a record of the normal operation, about 
+     * the application's state, progress, or important events.
+     * Reports normal start, end and key steps in an operation. The operation is
+     * performed nominally.
+    */
+    template <typename... Args>
+    constexpr static void info(Args&&... args) {
+        log(Info, fmt::format(std::forward<Args>(args)...));
+    }
+
+    /**
+     * Applies to normal but significant conditions that may require monitoring,
+     * like unusual or normal fallback events.
+     * Reports specific paths in an operation. The operation can still be
+     * performed normally.
+    */
+    template <typename... Args>
+    constexpr static void notice(Args&&... args) {
+        log(Notice, fmt::format(std::forward<Args>(args)...));
+    }
+
+    /**
+     * Indicates potential issues or situations that may lead to errors but do
+     * not necessarily cause immediate problems.
+     * Some specific steps of the operation could not be performed, but it can
+     * still provide an exploitable result.
+    */
+    template <typename... Args>
+    constexpr static void warn(Args&&... args) {
+        log(Warn, fmt::format(std::forward<Args>(args)...));
+    }
+
+    /**
+     * Signifies a problem or unexpected condition that the application can 
+     * recover from, but attention is needed to prevent further issues.
+     * The operation could not be performed, but it does not prevent potential
+     * further operations.
+    */
+    template <typename... Args>
+    constexpr static void error(Args&&... args) {
+        log(Error, fmt::format(std::forward<Args>(args)...));
+    }
+
+    /**
+     * Represents a critical error or condition that leads to the termination of
+     * the application, indicating a severe and unrecoverable problem.
+     * The operation could not be performed and any further operation is
+     * impossible.
+    */
+    template <typename... Args>
+    constexpr static void fatal(Args&&... args) {
+        log(Fatal, fmt::format(std::forward<Args>(args)...));
+    }
+
+    /**
+     * Set the minimum log level displayed in the console.
+    */
+    constexpr static void setConsoleLevel(Level level) {
+        mConsoleLevel = level;
+    }
+
+    /**
+     * Set the minimum log level saved in the log file.
+    */
+    constexpr static void setFileLevel(Level level) {
+        mFileLevel = level;
+    }
+
+    /**
+     * Set the log file name.
+     * Close the current log file and open the one with the new file name.
+     * If empty, stop logging into a file.
+    */
+    static void setFileName(const std::string& fileName) {
+        if (fileName != mFileName) {
+            mFileName = fileName;
+            mFile.release();
+
+            if (!fileName.empty()) {
+                initFile(fileName);
+            }
+        }
+    }
+
+private:
+    static void log(Level level, const std::string& msg);
+    static void initFile(const std::string& fileName);
+
+    static Level mConsoleLevel;
+    static Level mFileLevel;
+    static std::string mFileName;
+    static std::unique_ptr<FILE, decltype(&std::fclose)> mFile;
+};
+}
+
+#endif //AIDGE_LOG_H_
diff --git a/include/aidge/utils/Registrar.hpp b/include/aidge/utils/Registrar.hpp
index a5bd260ec189ac998134b738ca1ae757f2a0038c..567270d63c092aef6411a4438f59b7770ee3d5bf 100644
--- a/include/aidge/utils/Registrar.hpp
+++ b/include/aidge/utils/Registrar.hpp
@@ -132,11 +132,13 @@ void declare_registrable(py::module& m, const std::string& class_name){
 #ifdef PYBIND
 #define SET_IMPL_MACRO(T_Op, op, backend_name) \
      \
-        if(Py_IsInitialized()) { \
-            auto obj = py::cast(&(op)); \
-            (op).setImpl(Registrar<T_Op>::create(backend_name)(op)); \
-        } else { \
-            (op).setImpl(Registrar<T_Op>::create(backend_name)(op)); \
+        if (Registrar<T_Op>::exists(backend_name)) { \
+            if(Py_IsInitialized()) { \
+                auto obj = py::cast(&(op)); \
+                (op).setImpl(Registrar<T_Op>::create(backend_name)(op)); \
+            } else { \
+                (op).setImpl(Registrar<T_Op>::create(backend_name)(op)); \
+            } \
         }
 #else
 #define SET_IMPL_MACRO(T_Op, op, backend_name)                          \
diff --git a/python_binding/pybind_core.cpp b/python_binding/pybind_core.cpp
index 6c4dd29dfbb158774ea86b181503e7e7e718bda4..52863735ca431e797fab3426d7e61796a8725dd2 100644
--- a/python_binding/pybind_core.cpp
+++ b/python_binding/pybind_core.cpp
@@ -23,6 +23,7 @@ void init_DataProvider(py::module&);
 void init_Tensor(py::module&);
 void init_OperatorImpl(py::module&);
 void init_Attributes(py::module&);
+void init_Log(py::module&);
 void init_Operator(py::module&);
 void init_OperatorTensor(py::module&);
 
@@ -85,6 +86,7 @@ void init_Aidge(py::module& m){
 
     init_OperatorImpl(m);
     init_Attributes(m);
+    init_Log(m);
     init_Operator(m);
     init_OperatorTensor(m);
     init_Add(m);
diff --git a/python_binding/utils/pybind_Log.cpp b/python_binding/utils/pybind_Log.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..10a02dcafefe089c8836ee7d4e3a9783a2aa96a6
--- /dev/null
+++ b/python_binding/utils/pybind_Log.cpp
@@ -0,0 +1,103 @@
+#include <pybind11/pybind11.h>
+#include "aidge/utils/Log.hpp"
+
+namespace py = pybind11;
+namespace Aidge {
+void init_Log(py::module& m){
+    py::enum_<Log::Level>(m, "Level")
+        .value("Debug", Log::Debug)
+        .value("Info", Log::Info)
+        .value("Notice", Log::Notice)
+        .value("Warn", Log::Warn)
+        .value("Error", Log::Error)
+        .value("Fatal", Log::Fatal);
+
+    py::class_<Log>(m, "Log")
+    .def_static("debug", [](const std::string& msg) { Log::debug(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Detailed messages for debugging purposes, providing information helpful 
+          for developers to trace and identify issues.
+          Detailed insights of what is appening in an operation, not useful for the
+          end-user. The operation is performed nominally.
+          Note: This level is disabled at compile time for Release, therefore
+          inducing no runtime overhead for Release.
+
+          :param msg: Debug message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("info", [](const std::string& msg) { Log::info(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Messages that provide a record of the normal operation, about 
+          the application's state, progress, or important events.
+          Reports normal start, end and key steps in an operation. The operation is
+          performed nominally.
+
+          :param msg: Info message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("notice", [](const std::string& msg) { Log::notice(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Applies to normal but significant conditions that may require monitoring,
+          like unusual or normal fallback events.
+          Reports specific paths in an operation. The operation can still be
+          performed normally.
+
+          :param msg: Notice message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("warn", [](const std::string& msg) { Log::warn(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Indicates potential issues or situations that may lead to errors but do
+          not necessarily cause immediate problems.
+          Some specific steps of the operation could not be performed, but it can
+          still provide an exploitable result.
+
+          :param msg: Warning message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("error",[](const std::string& msg) { Log::error(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Signifies a problem or unexpected condition that the application can 
+          recover from, but attention is needed to prevent further issues.
+          The operation could not be performed, but it does not prevent potential
+          further operations.
+
+          :param msg: Error message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("fatal", [](const std::string& msg) { Log::fatal(msg); }, py::arg("msg"),
+          R"mydelimiter(
+          Represents a critical error or condition that leads to the termination of
+          the application, indicating a severe and unrecoverable problem.
+          The operation could not be performed and any further operation is
+          impossible.
+
+          :param msg: Fatal message.
+          :type msg: str
+          )mydelimiter")
+    .def_static("setConsoleLevel", &Log::setConsoleLevel, py::arg("level"),
+          R"mydelimiter(
+          Set the minimum log level displayed in the console.
+
+          :param level: Log level.
+          :type level: Level
+          )mydelimiter")
+    .def_static("setFileLevel", &Log::setFileLevel, py::arg("level"),
+          R"mydelimiter(
+          Set the minimum log level saved in the log file.
+
+          :param level: Log level.
+          :type level: Level
+          )mydelimiter")
+    .def_static("setFileName", &Log::setFileName, py::arg("fileName"),
+          R"mydelimiter(
+          Set the log file name.
+          Close the current log file and open the one with the new file name.
+          If empty, stop logging into a file.
+
+          :param fileName: Log file name.
+          :type fileName: str
+          )mydelimiter");
+}
+
+}
diff --git a/src/backend/OperatorImpl.cpp b/src/backend/OperatorImpl.cpp
index 1911da228c83d66117a2591adf47dc07cd8dc674..1439391b2e22fe0bea3b5a7692941afc67bc1c6b 100644
--- a/src/backend/OperatorImpl.cpp
+++ b/src/backend/OperatorImpl.cpp
@@ -25,14 +25,18 @@ Aidge::OperatorImpl::OperatorImpl(const Operator& op):
 }
 
 Aidge::NbElts_t Aidge::OperatorImpl::getNbRequiredData(const Aidge::IOIndex_t inputIdx) const {
-    assert(mOp.getRawInput(inputIdx) && "requires valid input");
+    AIDGE_ASSERT(mOp.getRawInput(inputIdx),
+        "a valid input is required at index {} for operator type {}",
+        inputIdx, mOp.type());
 
     // Requires the whole tensor by default
     return std::static_pointer_cast<Tensor>(mOp.getRawInput(inputIdx))->size();
 }
 
 Aidge::NbElts_t Aidge::OperatorImpl::getNbRequiredProtected(IOIndex_t inputIdx) const {
-    assert(mOp.getRawInput(inputIdx) && "requires valid input");
+    AIDGE_ASSERT(mOp.getRawInput(inputIdx),
+        "a valid input is required at index {} for operator type {}",
+        inputIdx, mOp.type());
 
     // Protect the whole tensor by default
     return std::static_pointer_cast<Tensor>(mOp.getRawInput(inputIdx))->size();
@@ -40,19 +44,25 @@ Aidge::NbElts_t Aidge::OperatorImpl::getNbRequiredProtected(IOIndex_t inputIdx)
 
 Aidge::NbElts_t Aidge::OperatorImpl::getRequiredMemory(const Aidge::IOIndex_t outputIdx,
                                                          const std::vector<Aidge::DimSize_t> &/*inputsSize*/) const {
-    assert(mOp.getRawOutput(outputIdx) && "requires valid output");
+    AIDGE_ASSERT(mOp.getRawOutput(outputIdx),
+        "a valid output is required at index {} for operator type {}",
+        outputIdx, mOp.type());
 
     // Requires the whole tensor by default, regardless of available data on inputs
     return std::static_pointer_cast<Tensor>(mOp.getRawOutput(outputIdx))->size();
 }
 
 Aidge::NbElts_t Aidge::OperatorImpl::getNbConsumedData(Aidge::IOIndex_t inputIdx) const {
-    assert(static_cast<std::size_t>(inputIdx) < mNbConsumedData.size());
+    AIDGE_ASSERT(static_cast<std::size_t>(inputIdx) < mNbConsumedData.size(),
+        "input index ({}) is out of bound ({}) for operator type {}",
+        inputIdx, mNbConsumedData.size(), mOp.type());
     return mNbConsumedData[static_cast<std::size_t>(inputIdx)];
 }
 
 Aidge::NbElts_t Aidge::OperatorImpl::getNbProducedData(Aidge::IOIndex_t outputIdx) const {
-    assert(static_cast<std::size_t>(outputIdx) < mNbProducedData.size());
+    AIDGE_ASSERT(static_cast<std::size_t>(outputIdx) < mNbProducedData.size(),
+        "output index ({}) is out of bound ({}) for operator type {}",
+        outputIdx, mNbProducedData.size(), mOp.type());
     return mNbProducedData[static_cast<std::size_t>(outputIdx)];
 }
 
diff --git a/src/graph/GraphView.cpp b/src/graph/GraphView.cpp
index 3681ac533cab36d68e5243fe0486b7d0febca694..005a7e679da5941d0995204b6c2a28a01ce376b4 100644
--- a/src/graph/GraphView.cpp
+++ b/src/graph/GraphView.cpp
@@ -328,19 +328,18 @@ void Aidge::GraphView::compile(const std::string& backend, const Aidge::DataType
 }
 
 void Aidge::GraphView::forwardDims(const std::vector<std::vector<Aidge::DimSize_t>> dims) {
-    std::set<NodePtr> startNodes = inputNodes();
-
     // setInputs
     // Link every tensor to the right pointer
     // following parent - children informations
     if (!dims.empty()){
-      AIDGE_ASSERT(dims.size() == mInputNodes.size(), "GraphView forwardDims error - Inconsistent number of dimensions and graph inputs");
+      AIDGE_ASSERT(dims.size() == mInputNodes.size(), "GraphView forwardDims error - Inconsistent number of given dimensions ({}) and graph inputs ({})", dims.size(), mInputNodes.size());
       for (std::size_t i = 0; i < dims.size(); ++i){
         auto tensor = std::make_shared<Tensor>(dims[i]);
         mInputNodes[i].first->getOperator()->setInput(mInputNodes[i].second, tensor);
       }
     }
-      
+
+    // Ensure every node in the graph is correctly connected
     for (std::shared_ptr<Node> nodePtr : getNodes()) {
         for (IOIndex_t i = 0; i < nodePtr->nbInputs(); ++i) {
             // assess if the input was not already set and is a Tensor then link it to parent output
@@ -352,7 +351,7 @@ void Aidge::GraphView::forwardDims(const std::vector<std::vector<Aidge::DimSize_
                         nodePtr->getOperator()->associateInput(i, inputI.first->getOperator()->getRawOutput(inputI.second));
                     }
                     else {
-                        AIDGE_ASSERT(false, "Non-tensor entries not handled yet.\n");
+                        AIDGE_ASSERT(false, "Non-tensor entries not handled yet, for node {} (of type {}).", nodePtr->name(), nodePtr->type());
                     }
                 }
             } else {
@@ -362,54 +361,37 @@ void Aidge::GraphView::forwardDims(const std::vector<std::vector<Aidge::DimSize_
             }
 
         }
-
-        if (nodePtr->type() == Producer_Op::Type) {
-          startNodes.insert(nodePtr);
-        }
     }
-    // Compute dimensions of every node
-    _forwardDims(startNodes);
 
-}
-
-void Aidge::GraphView::_forwardDims(std::set<std::shared_ptr<Node>> listNodes) {
-    // TODO: support multi-inputs/outputs
-    std::set<std::shared_ptr<Node>> nextList = std::set<std::shared_ptr<Node>>();
-    for (std::shared_ptr<Node> nodePtr : listNodes) {
-        if (nodePtr->getOperator()->operatorType() == OperatorType::Tensor) {
-            const auto op = std::static_pointer_cast<OperatorTensor>(nodePtr->getOperator());
-            if (!op->outputDimsForwarded()) {
-                op->computeOutputDims();
-            }
-            if (!op->outputDimsForwarded()) { // try to compute output dimensions again later
-                nextList.insert(nodePtr);
-            } else { // compute output dimensions of children
-                std::set<std::shared_ptr<Node>> children = nodePtr->getChildren();
-                for (auto child : children) {
-                  const auto childOp = std::static_pointer_cast<OperatorTensor>(child->getOperator());
-                  if (!childOp->outputDimsForwarded()) {
-                    nextList.insert(child);
-                  }
-                }
-            }
-        }
-    }
-    if (nextList.empty()) {
-        for (std::shared_ptr<Node> nodePtr : getNodes()) {
+    // Compute dimensions of every node
+    std::set<std::shared_ptr<Node>> listNodes = getNodes();
+    do {
+        std::set<std::shared_ptr<Node>> nextList;
+        for (std::shared_ptr<Node> nodePtr : listNodes) {
             if (nodePtr->getOperator()->operatorType() == OperatorType::Tensor) {
-                if (!std::static_pointer_cast<OperatorTensor>(nodePtr->getOperator())->outputDimsForwarded()) {
-                    nextList.insert(nodePtr);
-                }
+              const auto op = std::static_pointer_cast<OperatorTensor>(nodePtr->getOperator());
+              // Recompute everytime, even if it was already computed in a
+              // previous call of forwardDims(), as the graph may have changed!
+              op->computeOutputDims();
+              if (!op->outputDimsForwarded()) {
+                  nextList.insert(nodePtr);
+              }
             }
         }
-    }
 
-    // Internal check to make sure we won't enter in an infinite loop!
-    AIDGE_ASSERT(nextList != listNodes, "Unable to forward dimensions (circular dependency and/or wrong dimensions?)");
+        // Internal check to make sure we won't enter in an infinite loop!
+        if (nextList == listNodes) {
+            // We are stuck!
+            std::vector<std::string> nodesName;
+            std::transform(nextList.begin(), nextList.end(),
+                std::back_inserter(nodesName),
+                [](auto val){ return val->name() + " (" + val->type() + ")"; });
+            AIDGE_THROW_OR_ABORT(std::runtime_error, "Unable to forward dimensions (circular dependency and/or wrong dimensions?). Unable to compute output dims for nodes {}.", nodesName);
+        }
 
-    if (!nextList.empty()) {
-        _forwardDims(nextList);
+        listNodes.swap(nextList);
     }
+    while (!listNodes.empty());
 }
 
 void Aidge::GraphView::setBackend(const std::string &backend, DeviceIdx_t device) {
@@ -458,7 +440,7 @@ Aidge::GraphView::outputs(const std::string& nodeName) const {
 
 void Aidge::GraphView::setInputId(Aidge::IOIndex_t /*inID*/,
                                Aidge::IOIndex_t /*newNodeOutID*/) {
-  fmt::print("Not implemented yet.\n");
+  AIDGE_THROW_OR_ABORT(std::runtime_error, "Not implemented yet.");
 }
 
 void Aidge::GraphView::add(std::shared_ptr<Node> node, bool includeLearnableParam) {
@@ -714,10 +696,7 @@ std::set<std::shared_ptr<Aidge::Node>> Aidge::GraphView::getParents() const {
 
 std::vector<std::shared_ptr<Aidge::Node>> Aidge::GraphView::getParents(const std::string nodeName) const {
   std::map<std::string, std::shared_ptr<Node>>::const_iterator it = mNodeRegistry.find(nodeName);
-  if (it == mNodeRegistry.end()) {
-    fmt::print("No such node a {} in {} graph.\n", nodeName, name());
-    exit(-1);
-  }
+  AIDGE_ASSERT(it != mNodeRegistry.end(), "No node named {} in graph {}.", nodeName, name());
   return (it->second)->getParents();
 }
 
@@ -743,20 +722,15 @@ std::vector<std::vector<std::shared_ptr<Aidge::Node>>>
 Aidge::GraphView::getChildren(const std::string nodeName) const {
   std::map<std::string, std::shared_ptr<Node>>::const_iterator it =
       mNodeRegistry.find(nodeName);
-  if (it == mNodeRegistry.end()) {
-    fmt::print("No such node a {} in {} graph.\n", nodeName, name());
-    exit(-1);
-  }
+  AIDGE_ASSERT(it != mNodeRegistry.end(), "No node named {} in graph {}.", nodeName, name());
   return (it->second)->getOrderedChildren();
 }
 
 std::set<std::shared_ptr<Aidge::Node>>
 Aidge::GraphView::getChildren(const std::shared_ptr<Node> otherNode) const {
   std::set<std::shared_ptr<Node>>::const_iterator it = mNodes.find(otherNode);
-  if (it == mNodes.end()) {
-    fmt::print("No such node in graph.\n");
-    exit(-1);
-  }
+  AIDGE_ASSERT(it != mNodes.end(), "The node {} (of type {}) is not in graph {}.",
+    (otherNode) ? otherNode->name() : "#nullptr", (otherNode) ? otherNode->type() : "", name());
   return (*it)->getChildren();
 }
 
@@ -768,7 +742,7 @@ Aidge::GraphView::getNode(const std::string& nodeName) const {
   if (it != mNodeRegistry.cend()) {
     return it->second;
   } else {
-    fmt::print("No Node named {} in the current GraphView.\n", nodeName);
+    Log::warn("No Node named {} in the current GraphView {}.", nodeName, name());
     return nullptr;
   }
 }
diff --git a/src/graph/Node.cpp b/src/graph/Node.cpp
index 5d210144e2faa122416186734c52b67f1a0f8281..14e166402039230a283ce617e4997c9ad099eed9 100644
--- a/src/graph/Node.cpp
+++ b/src/graph/Node.cpp
@@ -169,7 +169,9 @@ Aidge::IOIndex_t Aidge::Node::nbValidOutputs() const {
 }
 
 void Aidge::Node::setInputId(const IOIndex_t inId, const IOIndex_t newNodeoutId) {
-    assert(inId != gk_IODefaultIndex && (inId < nbInputs()) && "Must be a valid index");
+    AIDGE_ASSERT(inId != gk_IODefaultIndex && inId < nbInputs(),
+        "Input index ({}) is out of bound ({}) for node {} (of type {})",
+        inId, nbInputs(), name(), type());
     if (mIdOutParents[inId] != gk_IODefaultIndex) {
         fmt::print("Warning: filling a Tensor already attributed\n");
         auto originalParent = input(inId);
@@ -194,7 +196,7 @@ void Aidge::Node::addChildOp(std::shared_ptr<Node> otherNode, const IOIndex_t ou
         "Output index (#{}) of the node {} (of type {}) is out of bound (it has {} outputs), when trying to add the child node {} (of type {})",
         outId, name(), type(), nbOutputs(), otherNode->name(), otherNode->type());
     if (otherNode->input(otherInId).second != gk_IODefaultIndex) {
-        fmt::print("Warning, the {}-th Parent of the child node already existed.\n", otherInId);
+        Log::notice("Notice: the {}-th Parent of the child node {} (of type {}) already existed", otherInId, otherNode->name(), otherNode->type());
     }
     // manage tensors and potential previous parent
     otherNode->setInputId(otherInId, outId);
@@ -239,23 +241,29 @@ void Aidge::Node::addChild(std::shared_ptr<GraphView> otherView, const IOIndex_t
 
 void Aidge::Node::addParent(const std::shared_ptr<Node> other_node, const IOIndex_t inId) {
     if (getParent(inId) != nullptr) {
-        fmt::print("Warning, you're replacing a Parent.\n");
+        Log::notice("Notice: you are replacing an existing parent for node {} (of type {})", name(), type());
     }
-    assert((inId != gk_IODefaultIndex) && (inId < nbInputs()) && "Input index out of bound.");
+    AIDGE_ASSERT(inId != gk_IODefaultIndex && inId < nbInputs(),
+        "Input index ({}) is out of bound ({}) for node {} (of type {})",
+        inId, nbInputs(), name(), type());
     mParents[inId] = other_node;
 }
 
 std::vector<std::shared_ptr<Aidge::Node>> Aidge::Node::getParents() const { return mParents; }
 
 std::shared_ptr<Aidge::Node> Aidge::Node::popParent(const IOIndex_t inId) {
-    assert((inId != gk_IODefaultIndex) && (inId < nbInputs()) && "Input index out of bound.");
+    AIDGE_ASSERT(inId != gk_IODefaultIndex && inId < nbInputs(),
+        "Input index ({}) is out of bound ({}) for node {} (of type {})",
+        inId, nbInputs(), name(), type());
     std::shared_ptr<Node> val = mParents[inId];
     removeParent(inId);
     return val;
 }
 
 bool Aidge::Node::removeParent(const IOIndex_t inId) {
-    assert((inId != gk_IODefaultIndex) && (inId < nbInputs()) && "Parent index out of bound.");
+    AIDGE_ASSERT(inId != gk_IODefaultIndex && inId < nbInputs(),
+        "Input index ({}) is out of bound ({}) for node {} (of type {})",
+        inId, nbInputs(), name(), type());
     if (mParents[inId]) {
         mParents[inId] = nullptr;
         mIdOutParents[inId] = gk_IODefaultIndex;
diff --git a/src/graphRegex/GraphRegex.cpp b/src/graphRegex/GraphRegex.cpp
index 00a031e3fa9b03ff1870446b9ae58e8d3eb65bf7..ca15ff8dec5ff5ebd4ea69141c6e286849162bb5 100644
--- a/src/graphRegex/GraphRegex.cpp
+++ b/src/graphRegex/GraphRegex.cpp
@@ -117,6 +117,8 @@ std::set<std::shared_ptr<MatchSolution>> GraphRegex::match(std::shared_ptr<Graph
             std::vector<std::shared_ptr<MatchSolution>> solution = fsm->test(combination);
             solutions.insert(solutions.end(), solution.begin(), solution.end());
         }
+
+
     }
     return _findLargestCompatibleSet(solutions);
 }
@@ -142,7 +144,10 @@ void GraphRegex::setNodeKey(const std::string key,std::function<bool(NodePtr)> f
         throw std::runtime_error(key + " is define");
     }
     mAllLambda[key] = f;
+    
     _majConditionalInterpreterLambda();
+    //we add the lambda as key by default 
+    setNodeKey(key, key + "($)==true");
 }
 
 void GraphRegex::_majConditionalInterpreterLambda(){
diff --git a/src/nodeTester/ConditionalLexer.cpp b/src/nodeTester/ConditionalLexer.cpp
index 9379bd8409f8f7ec4bae3e0122f88de79718e9dd..e70772fc1a5d6136fb56f5981d73bf6cb0622991 100644
--- a/src/nodeTester/ConditionalLexer.cpp
+++ b/src/nodeTester/ConditionalLexer.cpp
@@ -120,7 +120,7 @@ std::shared_ptr<ParsingToken<ConditionalTokenTypes>> ConditionalLexer::getNextTo
             }
 
 
-            if (std::regex_match(currentChars,std::regex("(true|false)"))){
+            if (std::regex_match(currentChars,std::regex("(true|false|True|False)"))){
                 return std::make_shared<ParsingToken<ConditionalTokenTypes>>(ConditionalTokenTypes::BOOL,currentChars);
 
             } else if (isLambda){
diff --git a/src/operator/Operator.cpp b/src/operator/Operator.cpp
index 289b2be90735d848e5083090d2ae4319a7490fde..e4213cad80ebdc177649b0c25e4fc49222993211 100644
--- a/src/operator/Operator.cpp
+++ b/src/operator/Operator.cpp
@@ -75,4 +75,7 @@ void Aidge::Operator::forward() {
     runHooks();
 }
 
-void Aidge::Operator::backward() { mImpl->backward(); }
+void Aidge::Operator::backward() {
+    AIDGE_ASSERT(mImpl != nullptr, "backward(): an implementation is required for {}!", type());
+    mImpl->backward(); 
+}
diff --git a/src/recipes/FuseMulAdd.cpp b/src/recipes/FuseMulAdd.cpp
index f408959a13d007853c24e30c1ef683648cf9c200..b57c1c3fc5e4b12dbd0004472a864ddaa864116e 100644
--- a/src/recipes/FuseMulAdd.cpp
+++ b/src/recipes/FuseMulAdd.cpp
@@ -64,7 +64,7 @@ void Aidge::fuseMulAdd(std::shared_ptr<Aidge::Node> matmulNode, std::shared_ptr<
     {
         // If both inputs are producers, there is an ambiguity, but both options
         // result in a correct solution.
-        fmt::print("Warning: both MatMul inputs are Producers, assume data at input#0 and weights at input#1.\n");
+        Log::notice("Notice: both MatMul inputs are Producers, assume data at input#0 and weights at input#1.");
         weight = matmulNode->getParent(1)->cloneSharedOperators();
     }
     AIDGE_ASSERT(weight != nullptr, "Could not deduce weight input for MatMul operator.");
diff --git a/src/utils/Log.cpp b/src/utils/Log.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7649809339f4ebf716a7287f5744fb94a5b67ce2
--- /dev/null
+++ b/src/utils/Log.cpp
@@ -0,0 +1,59 @@
+/********************************************************************************
+ * 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/Log.hpp"
+#include "aidge/utils/ErrorHandling.hpp"
+
+#include <fmt/color.h>
+#include <fmt/chrono.h>
+
+Aidge::Log::Level Aidge::Log::mConsoleLevel = Info;
+Aidge::Log::Level Aidge::Log::mFileLevel = Debug;
+std::string Aidge::Log::mFileName = "aidge.log";
+std::unique_ptr<FILE, decltype(&std::fclose)> Aidge::Log::mFile {nullptr, nullptr};
+
+void Aidge::Log::log(Level level, const std::string& msg) {
+    if (level >= mConsoleLevel) {
+        // Apply log level style only for console.
+        // Styles that were already applied to msg with fmt are kept also in 
+        // the log file.
+        const auto modifier
+            = (level == Debug) ? fmt::fg(fmt::color::gray)
+            : (level == Notice) ? fmt::fg(fmt::color::light_yellow)
+            : (level == Warn) ? fmt::fg(fmt::color::orange)
+            : (level == Error) ? fmt::fg(fmt::color::red)
+            : (level == Fatal) ? fmt::bg(fmt::color::red)
+            : fmt::text_style();
+
+        fmt::println("{}", fmt::styled(msg, modifier));
+    }
+
+    if (level >= mFileLevel && !mFileName.empty()) {
+        if (!mFile) {
+            initFile(mFileName);
+        }
+
+        fmt::println(mFile.get(), msg);
+    }
+}
+
+void Aidge::Log::initFile(const std::string& fileName) {
+    mFile = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen(fileName.c_str(), "a"), &std::fclose);
+
+    if (!mFile) {
+        mFileName.clear(); // prevents AIDGE_THROW_OR_ABORT() to try to log into file
+        AIDGE_THROW_OR_ABORT(std::runtime_error,
+            "Could not create log file: {}", fileName);
+    }
+
+    const std::time_t t = std::time(nullptr);
+    fmt::println(mFile.get(), "###### {:%Y-%m-%d %H:%M:%S} ######", fmt::localtime(t));
+}
diff --git a/unit_tests/graphRegex/Test_GraphRegex.cpp b/unit_tests/graphRegex/Test_GraphRegex.cpp
index bcd6d0f4cd9ba32ee4318188343b7e6360670d3b..a62b9a8602b494f26fb47061b899eaba41129a1f 100644
--- a/unit_tests/graphRegex/Test_GraphRegex.cpp
+++ b/unit_tests/graphRegex/Test_GraphRegex.cpp
@@ -18,6 +18,32 @@ using namespace Aidge;
 
 TEST_CASE("GraphRegexUser") {
 
+
+    SECTION("Match using custom lambda") {
+
+        std::shared_ptr<GraphView> g1 = std::make_shared<GraphView>("TestGraph");
+        std::shared_ptr<Node> conv = GenericOperator("Conv", 1, 0, 1, "c");
+        std::shared_ptr<Node> fc = GenericOperator("FC", 1, 0, 1, "c1");
+        std::shared_ptr<Node> conv2 = GenericOperator("Conv", 1, 0, 1, "c2");
+        std::shared_ptr<Node> fc2 = GenericOperator("FC", 1, 0, 1, "c3");
+
+        g1->add(conv);
+        g1->addChild(fc, "c");
+        g1->addChild(conv2, "c1");
+        g1->addChild(fc2, "c2");
+        
+        ///
+        std::shared_ptr<GraphRegex> sut = std::make_shared<GraphRegex>();
+        sut->setNodeKey("C",+[](NodePtr NodeOp){return NodeOp->type() == "FC";});
+        
+        sut->setNodeKey("A","C($)==True");
+        sut->addQuery("A");
+        auto match = sut->match(g1);
+        REQUIRE(match.size() == 2);
+
+    }
+
+
     SECTION("INIT") {
 
         const std::string query = "Conv->FC";
diff --git a/unit_tests/utils/Test_Log.cpp b/unit_tests/utils/Test_Log.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3d8e672b84f5055a12185c3684c34bd888f0545b
--- /dev/null
+++ b/unit_tests/utils/Test_Log.cpp
@@ -0,0 +1,31 @@
+/********************************************************************************
+ * 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 "aidge/utils/Log.hpp"
+
+#include <fmt/color.h>
+
+using namespace Aidge;
+
+TEST_CASE("[core/log] Log") {
+    SECTION("TestLog") {
+        Log::setConsoleLevel(Log::Debug);
+        Log::debug("debug");
+        Log::debug("{}", fmt::styled("green debug", fmt::fg(fmt::color::green)));
+        Log::info("info");
+        Log::notice("notice");
+        Log::warn("warn");
+        Log::error("error");
+        Log::fatal("fatal");
+    }
+}