diff --git a/Graph/graph/graph_1.py b/Graph/graph/graph_1.py
index 450be8b86705a7081f8fd886c4605d59a8db3025..14d6e8e6efecb7d6d3153eb23f100c580914baa3 100644
--- a/Graph/graph/graph_1.py
+++ b/Graph/graph/graph_1.py
@@ -1,5 +1,5 @@
+from __future__ import annotations
 import itertools
-
 from typing import Iterator, Set, Optional
 
 from .helpers import FIFO, LIFO
@@ -16,12 +16,12 @@ class NDGraph:
     """
 
     def __init__(self):
-        self.nodes: Set['Node'] = set()
+        self.nodes: Set[Node] = set()
 
-    def add_node(self, node: 'Node') -> None:
+    def add_node(self, node: Node) -> None:
         self.nodes.add(node)
 
-    def del_node(self, node: 'Node') -> None:
+    def del_node(self, node: Node) -> None:
         self.nodes.remove(node)
 
 
diff --git a/Graph/graph/graph_3.py b/Graph/graph/graph_3.py
new file mode 100644
index 0000000000000000000000000000000000000000..86f4494755bffc9dba3a25a5063eab7437dc766e
--- /dev/null
+++ b/Graph/graph/graph_3.py
@@ -0,0 +1,259 @@
+import itertools
+from abc import ABCMeta
+from typing import Iterator, Dict, Set, Sequence, Union, Tuple
+
+
+class Graph(metaclass=ABCMeta):
+
+    def __init__(self):
+        self.nodes: set[Node] = set()
+        self.vertices: Dict[Node: set[Node]]
+
+    def add_node(self):
+        pass
+
+    def del_node(self):
+        pass
+
+
+class NDGraph:
+    """
+    To handle Non Directed Graph
+    A graph is a collection of Nodes linked by edges
+    there are basically to way to implement a graph
+     - The graph handles Nodes, each nodes know it's neighbors
+     - The graph handles nodes and the edges between nodes
+     below I implemented the 2nd version.
+    """
+
+    def __init__(self, nodes: Sequence['Node']) -> None:
+        self.nodes: Set['Node'] = {n for n in nodes}
+        self.vertices: Dict['Node': Set['Node']] = {n: set() for n in self.nodes}
+
+
+    def add_node(self, node: 'Node') -> None:
+        """
+        Add a node to the graph
+
+        :param node: the node to add
+        :type node: :class:`Node`
+        :return: None
+        """
+        self.nodes.add(node)
+        if node not in self.vertices:
+            self.vertices[node] = set()
+
+
+    def del_node(self, node: 'Node') -> None:
+        """
+        Remove Node from Graph
+
+        :param node: the node to add
+        :type node: :class:`Node`
+        :return: None
+        """
+        self.nodes.remove(node)
+        neighbors = self.vertices[node]
+        for nbh in neighbors:
+            self.vertices[nbh].remove(node)
+        del self.vertices[node]
+
+
+    def add_edge(self, node_1: 'Node', nodes: Sequence['Node']):
+        """
+        Add vertice between node1 and all nodes in nodes
+
+        :param node_1: the reference node
+        :type node_1: :class:`Node`
+        :param nodes: the nodes connected to node_1
+        :type nodes: Sequence of :class:`Node`
+        :return: Node
+        """
+        if node_1 not in self.nodes:
+            raise ValueError("the node_1 must be in Graph")
+        for n in nodes:
+            if n not in self.nodes:
+                raise ValueError("node must be add to Graph before creating edge")
+            self.vertices[node_1].add(n)
+            self.vertices[n].add(node_1)
+
+
+    def neighbors(self, node: 'Node') -> Set['Node']:
+        """
+        return the nodes connected to node
+
+        :param node: the reference node
+        :type node: :class:`Node`
+        :return: a set of :class:`Node`
+        """
+        return {n for n in self.vertices[node]}
+
+
+    def get_weight(self, node_1, node_2):
+        """
+
+        :param node_1:
+        :param node_2:
+        :return:
+        """
+        return 1
+
+
+class Node:
+
+    _id = itertools.count(0)
+
+    def __init__(self) -> None:
+        self.id: int = next(self._id)
+
+    def __hash__(self) -> int:
+        # to be usable in set an object must be hashable
+        # so we need to implement __hash__
+        # which must return an int unique per object
+        # here we ad the identifier of the Node
+        return self.id
+
+    def __str__(self):
+        return f"node_{self.id}"
+
+    def __repr__(self):
+        return str(self)
+
+
+class Edge:
+
+    def __init__(self, src, target, weight):
+        self.src = src
+        self.target = target
+        self.weight = weight
+
+
+class NDWGraph:
+    """
+    To handle Non Directed Graph
+    A graph is a collection of Nodes linked by edges
+    there are basically to way to implement a graph
+     - The graph handles Nodes, each nodes know it's neighbors
+     - The graph handles nodes and the edges between nodes
+     below I implemented the 2nd version.
+    """
+
+    def __init__(self, node) -> None:
+        self.nodes: Set[Node] = {}
+
+
+    def add_node(self, node: Node) -> None:
+        """
+        Add a node to the graph
+
+        :param node: the node to add
+        :type node: :class:`Node`
+        :return: None
+        """
+        if node not in self.nodes:
+            self.nodes[node] = {}
+
+
+    def del_node(self, node: Node) -> None:
+        """
+        Remove Node from Graph
+
+        :param node: the node to add
+        :type node: :class:`Node`
+        :return: None
+        """
+        neighbors = self.nodes[node]
+        for nbh in neighbors:
+            del self.vertices[nbh][node]
+        del self.nodes[node]
+
+
+    def add_edge(self, node_1: Node, node_2: Node, weight: Union[int, float]):
+        """
+        Add vertex between node1 and all nodes in nodes
+
+        :param node_1: the reference node
+        :param node_2: the nodes connected to node_1
+        :param weight: the weight of the edge between node_1 and node_2
+        :return: Node
+        """
+        for n in node_1, node_2:
+            if n not in self.nodes:
+                raise ValueError(f"node {n.id} not found in Graph. The node must be in Graph.")
+        self.nodes[node_1][node_2] = weight
+        self.nodes[node_2][node_1] = weight
+
+
+    def neighbors(self, node):
+        """
+        return the nodes connected to node
+
+        :param node: the reference node
+        :type node: :class:`Node`
+        :return: a set of :class:`Node`
+        """
+        return {n for n, w in self.nodes[node].items()}
+
+
+    def get_weight(self, node_1, node_2):
+        """
+
+        :param node_1:
+        :param node_2:
+        :return:
+        """
+        return self.nodes[node_1][node_2]
+
+
+def DFS(graph: NDGraph, node: Node) -> Iterator[Node]:
+    """
+    **D**epth **F**irst **S**earch.
+    We start the path from the node given as argument,
+    This node is labeled as 'visited'
+    The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit'
+    We pick the last element to visit and visit it
+    (The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit').
+    on so on until there no nodes to visit anymore.
+
+    :param graph:
+    :param node:
+    :return:
+    """
+    to_visit = [node]
+    visited = set()
+    while to_visit:
+        n = to_visit.pop(-1)
+        visited.add(n)
+        new_to_visit = graph.neighbors(n) - visited - set(to_visit)
+        to_visit.extend(new_to_visit)
+        yield n
+
+
+def BFS(graph: NDGraph, node: Node) -> Iterator[Node]:
+    """
+    **B**readth **F**irst **s**earch
+    We start the path from the node given as argument,
+    This node is labeled as 'visited'
+    The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit'
+    we pick the **first** element of the nodes to visit and visit it.
+    (The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit')
+    on so on until there no nodes to visit anymore.
+
+    :param graph:
+    :param node:
+    :return:
+    """
+    to_visit = [node]
+    visited = set()
+    parent = None
+    while to_visit:
+        n = to_visit.pop(0)
+        visited.add(n)
+        new_to_visit = graph.neighbors(n) - visited - set(to_visit)
+        to_visit.extend(new_to_visit)
+        if not parent:
+            weight = 0
+        else:
+            weight = graph.get_weight(parent, n)
+        parent = n
+        yield n, weight
diff --git a/Graph/main_3.py b/Graph/main_3.py
new file mode 100644
index 0000000000000000000000000000000000000000..4ad78ba7e258bc81433579422ad0f03ec378a48b
--- /dev/null
+++ b/Graph/main_3.py
@@ -0,0 +1,62 @@
+from graph.graph_2 import NDGraph, NDWGraph, Node, BFS, DFS
+
+# We want to create a toy graph (not directed) to check our algos
+#              no
+#             /  \
+#            n1  n3
+#            |    |
+#            n2  n4
+#             \  /
+#              n5
+
+
+nodes = [Node() for _ in range(6)]
+g = NDGraph(nodes)
+
+g.add_edge(nodes[0], (nodes[1], nodes[3]))
+g.add_edge(nodes[1], (nodes[2],))
+g.add_edge(nodes[2], (nodes[5],))
+g.add_edge(nodes[3], (nodes[4],))
+g.add_edge(nodes[4], (nodes[5],))
+
+
+# The graph is created we will test
+
+# a Depth First Search
+# starting from n0
+# the possible solutions are
+# n0, n1,n2,n5,n4,n3
+# n0, n3, n4, n5, n2, n1
+
+print("DFS")
+for n in DFS(g, nodes[0]):
+    print(n.id)
+
+# a Breadth First Search
+# starting n0
+# the possible solutions are
+# n0, n1, n3, n2, n4, n5
+# n0, n3, n1, n2, n4, n5
+# n0, n1, n3, n4, n2, n5
+# ....
+
+print("BFS")
+for n, _ in BFS(g, nodes[0]):
+    print(n.id)
+
+
+nodes = [Node() for _ in range(6)]
+g = NDWGraph(nodes)
+
+g.add_edge(nodes[0], nodes[1], 1)
+g.add_edge(nodes[0], nodes[3], 3)
+g.add_edge(nodes[1], nodes[2], 2)
+g.add_edge(nodes[2], nodes[5], 5)
+g.add_edge(nodes[3], nodes[4], 4)
+g.add_edge(nodes[4], nodes[5], 5)
+
+print("BFS")
+path_len = 0
+for n, w in BFS(g, nodes[0]):
+    path_len += w
+    print(n, w, path_len)