Repository: TheAlgorithms/Python

Issue:
Improving Graph implementation with more functions
### Feature description

I noticed that in some graph implementations (such as the one in `graph_list.py`, there's only one function, `add_edge()`. However, this does not allow floating nodes to exist, nor does it allow nodes and edges to be deleted. To make the graph implementation a lot more feature rich. I would like the recommend the following functions for any graph implementation that could use it (this example is for `graph_list.py`:

- [ ] `add_vertex(self, vertex: T) -> GraphAdjacencyList[T]:`
- [ ] `remove_vertex(self, vertex: T) -> GraphAdjacencyList[T]:`
- [ ] `remove_edge(self, source_vertex: T, destination_vertex: T) -> GraphAdjacencyList[T]:`
- [ ] `clear_graph(self)`

Each of these functions would be dummy-safe, so no need for error-throwing (unless we want to do that). So if the user wants an edge or node to be deleted that doesn't exist, the functions would run existence checks before doing anything hazardous, execute the request if possible, otherwise skip it, and then return successfully.

Unless this is something that y'all don't want implemented, I would like to take on this issue. Can I get a confirmation that I can work on this?

**Edit:** I noticed these were implemented in the other language repos, so I'm assuming it's cool to work on it.


Discussion:
Thank you for your proposal to add additional functionality to the graph_list.py implementation. The suggested functions, add_vertex(), remove_vertex(), remove_edge(), and clear_graph() would certainly make the implementation more versatile and feature-rich. It is important to ensure that these functions are dummy-safe, as you have suggested, to prevent any potential errors or issues.

Before proceeding with this work, it would be best to confirm that is this an area that our contributors should focus on? Please discuss with @cclauss @tianyizheng02.

Once discussed, anyone is welcome to proceed with the implementation.
Sure, would love to discuss it with them (either through this thread or elsewhere, in which case I would appreciate any redirection to a different chat application if necessary). I've already completed the implementation for `graph_list.py` and I was going to move on to `graph_matrix.py` on my local fork. But ngl, `graph_matrix.py` looks like it could use some remodeling and implementation in terms of how node values are chosen, maybe modeling it similar to `graph_list.py`.
We try not to slow down implementation so if you think that a pull request would be a useful addition to this project then please do not wait for permission.  It is easier for all concerned to see the Python code and judge it on its merits.

It is a correct statement to say that most files focus on a single algorithm instead of all the possible manipulations that can be done with all forms of a data structure but this is not a hard rule.  If you want to create a complete Graph data structure / class with lots of functions / methods and covering many different types of Graph then that would be acceptable as well.  We love to see strong tests and guards against accidental misuse.
Sounds good! I'll get a PR up later.
>  the functions would run existence checks before doing anything hazardous, execute the request if possible, otherwise skip it, and then return successfully.

It is quite Pythonic to raise Exceptions and "_Errors should never pass silently_" is part of the Zen of Python.
- `python3 -c "import this"`
@cclauss Fair! Then for functions listed above, should errors be thrown if for example, the edge to be deleted doesn't exist, or the node to be added already exists? I assumed not because the existing implementation for `add_edge` seems to just create nodes in case they don't exist before adding the edge.
Let's not assume that the current implementations are perfect. -- They are not.  Let's make future implementations as perfect as we know how.  That means that appropriate Exceptions should be raise if there are errors in the input.
Hmmm, sounds good. Then as you mentioned, I'll write up the code such that if any input parameters are faulty in any way (such as referencing non-existent or existent nodes and edges when they shouldn't), I'll have the appropriate exception thrown in all cases, and minimize any hand-holding in the back-end.

To make this easier on the client, I'll include `contains_edge()` and `contains_node()` functions as well.
**Update**: finished implementations for `graph_list.py` and `graph_matrix.py` with all of the requested modifications, currently in the process (25% done) of writing tests for both using randomized inputs. This part is taking a hot sec because it's unironically harder than the graph implementation, but it's in progress!

Resolving patch:
diff --git a/graphs/graph_adjacency_list.py b/graphs/graph_adjacency_list.py
new file mode 100644
--- /dev/null
+++ b/graphs/graph_adjacency_list.py
@@ -0,0 +1,589 @@
+#!/usr/bin/env python3
+"""
+Author: Vikram Nithyanandam
+
+Description:
+The following implementation is a robust unweighted Graph data structure
+implemented using an adjacency list. This vertices and edges of this graph can be
+effectively initialized and modified while storing your chosen generic
+value in each vertex.
+
+Adjacency List: https://en.wikipedia.org/wiki/Adjacency_list
+
+Potential Future Ideas:
+- Add a flag to set edge weights on and set edge weights
+- Make edge weights and vertex values customizable to store whatever the client wants
+- Support multigraph functionality if the client wants it
+"""
+from __future__ import annotations
+
+import random
+import unittest
+from pprint import pformat
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+
+class GraphAdjacencyList(Generic[T]):
+    def __init__(
+        self, vertices: list[T], edges: list[list[T]], directed: bool = True
+    ) -> None:
+        """
+        Parameters:
+         - vertices: (list[T]) The list of vertex names the client wants to
+        pass in. Default is empty.
+        - edges: (list[list[T]]) The list of edges the client wants to
+        pass in. Each edge is a 2-element list. Default is empty.
+        - directed: (bool) Indicates if graph is directed or undirected.
+        Default is True.
+        """
+        self.adj_list: dict[T, list[T]] = {}  # dictionary of lists of T
+        self.directed = directed
+
+        # Falsey checks
+        edges = edges or []
+        vertices = vertices or []
+
+        for vertex in vertices:
+            self.add_vertex(vertex)
+
+        for edge in edges:
+            if len(edge) != 2:
+                msg = f"Invalid input: {edge} is the wrong length."
+                raise ValueError(msg)
+            self.add_edge(edge[0], edge[1])
+
+    def add_vertex(self, vertex: T) -> None:
+        """
+        Adds a vertex to the graph. If the given vertex already exists,
+        a ValueError will be thrown.
+        """
+        if self.contains_vertex(vertex):
+            msg = f"Incorrect input: {vertex} is already in the graph."
+            raise ValueError(msg)
+        self.adj_list[vertex] = []
+
+    def add_edge(self, source_vertex: T, destination_vertex: T) -> None:
+        """
+        Creates an edge from source vertex to destination vertex. If any
+        given vertex doesn't exist or the edge already exists, a ValueError
+        will be thrown.
+        """
+        if not (
+            self.contains_vertex(source_vertex)
+            and self.contains_vertex(destination_vertex)
+        ):
+            msg = (
+                f"Incorrect input: Either {source_vertex} or "
+                f"{destination_vertex} does not exist"
+            )
+            raise ValueError(msg)
+        if self.contains_edge(source_vertex, destination_vertex):
+            msg = (
+                "Incorrect input: The edge already exists between "
+                f"{source_vertex} and {destination_vertex}"
+            )
+            raise ValueError(msg)
+
+        # add the destination vertex to the list associated with the source vertex
+        # and vice versa if not directed
+        self.adj_list[source_vertex].append(destination_vertex)
+        if not self.directed:
+            self.adj_list[destination_vertex].append(source_vertex)
+
+    def remove_vertex(self, vertex: T) -> None:
+        """
+        Removes the given vertex from the graph and deletes all incoming and
+        outgoing edges from the given vertex as well. If the given vertex
+        does not exist, a ValueError will be thrown.
+        """
+        if not self.contains_vertex(vertex):
+            msg = f"Incorrect input: {vertex} does not exist in this graph."
+            raise ValueError(msg)
+
+        if not self.directed:
+            # If not directed, find all neighboring vertices and delete all references
+            # of edges connecting to the given vertex
+            for neighbor in self.adj_list[vertex]:
+                self.adj_list[neighbor].remove(vertex)
+        else:
+            # If directed, search all neighbors of all vertices and delete all
+            # references of edges connecting to the given vertex
+            for edge_list in self.adj_list.values():
+                if vertex in edge_list:
+                    edge_list.remove(vertex)
+
+        # Finally, delete the given vertex and all of its outgoing edge references
+        self.adj_list.pop(vertex)
+
+    def remove_edge(self, source_vertex: T, destination_vertex: T) -> None:
+        """
+        Removes the edge between the two vertices. If any given vertex
+        doesn't exist or the edge does not exist, a ValueError will be thrown.
+        """
+        if not (
+            self.contains_vertex(source_vertex)
+            and self.contains_vertex(destination_vertex)
+        ):
+            msg = (
+                f"Incorrect input: Either {source_vertex} or "
+                f"{destination_vertex} does not exist"
+            )
+            raise ValueError(msg)
+        if not self.contains_edge(source_vertex, destination_vertex):
+            msg = (
+                "Incorrect input: The edge does NOT exist between "
+                f"{source_vertex} and {destination_vertex}"
+            )
+            raise ValueError(msg)
+
+        # remove the destination vertex from the list associated with the source
+        # vertex and vice versa if not directed
+        self.adj_list[source_vertex].remove(destination_vertex)
+        if not self.directed:
+            self.adj_list[destination_vertex].remove(source_vertex)
+
+    def contains_vertex(self, vertex: T) -> bool:
+        """
+        Returns True if the graph contains the vertex, False otherwise.
+        """
+        return vertex in self.adj_list
+
+    def contains_edge(self, source_vertex: T, destination_vertex: T) -> bool:
+        """
+        Returns True if the graph contains the edge from the source_vertex to the
+        destination_vertex, False otherwise. If any given vertex doesn't exist, a
+        ValueError will be thrown.
+        """
+        if not (
+            self.contains_vertex(source_vertex)
+            and self.contains_vertex(destination_vertex)
+        ):
+            msg = (
+                f"Incorrect input: Either {source_vertex} "
+                f"or {destination_vertex} does not exist."
+            )
+            raise ValueError(msg)
+
+        return destination_vertex in self.adj_list[source_vertex]
+
+    def clear_graph(self) -> None:
+        """
+        Clears all vertices and edges.
+        """
+        self.adj_list = {}
+
+    def __repr__(self) -> str:
+        return pformat(self.adj_list)
+
+
+class TestGraphAdjacencyList(unittest.TestCase):
+    def __assert_graph_edge_exists_check(
+        self,
+        undirected_graph: GraphAdjacencyList,
+        directed_graph: GraphAdjacencyList,
+        edge: list[int],
+    ) -> None:
+        self.assertTrue(undirected_graph.contains_edge(edge[0], edge[1]))
+        self.assertTrue(undirected_graph.contains_edge(edge[1], edge[0]))
+        self.assertTrue(directed_graph.contains_edge(edge[0], edge[1]))
+
+    def __assert_graph_edge_does_not_exist_check(
+        self,
+        undirected_graph: GraphAdjacencyList,
+        directed_graph: GraphAdjacencyList,
+        edge: list[int],
+    ) -> None:
+        self.assertFalse(undirected_graph.contains_edge(edge[0], edge[1]))
+        self.assertFalse(undirected_graph.contains_edge(edge[1], edge[0]))
+        self.assertFalse(directed_graph.contains_edge(edge[0], edge[1]))
+
+    def __assert_graph_vertex_exists_check(
+        self,
+        undirected_graph: GraphAdjacencyList,
+        directed_graph: GraphAdjacencyList,
+        vertex: int,
+    ) -> None:
+        self.assertTrue(undirected_graph.contains_vertex(vertex))
+        self.assertTrue(directed_graph.contains_vertex(vertex))
+
+    def __assert_graph_vertex_does_not_exist_check(
+        self,
+        undirected_graph: GraphAdjacencyList,
+        directed_graph: GraphAdjacencyList,
+        vertex: int,
+    ) -> None:
+        self.assertFalse(undirected_graph.contains_vertex(vertex))
+        self.assertFalse(directed_graph.contains_vertex(vertex))
+
+    def __generate_random_edges(
+        self, vertices: list[int], edge_pick_count: int
+    ) -> list[list[int]]:
+        self.assertTrue(edge_pick_count <= len(vertices))
+
+        random_source_vertices: list[int] = random.sample(
+            vertices[0 : int(len(vertices) / 2)], edge_pick_count
+        )
+        random_destination_vertices: list[int] = random.sample(
+            vertices[int(len(vertices) / 2) :], edge_pick_count
+        )
+        random_edges: list[list[int]] = []
+
+        for source in random_source_vertices:
+            for dest in random_destination_vertices:
+                random_edges.append([source, dest])
+
+        return random_edges
+
+    def __generate_graphs(
+        self, vertex_count: int, min_val: int, max_val: int, edge_pick_count: int
+    ) -> tuple[GraphAdjacencyList, GraphAdjacencyList, list[int], list[list[int]]]:
+        if max_val - min_val + 1 < vertex_count:
+            raise ValueError(
+                "Will result in duplicate vertices. Either increase range "
+                "between min_val and max_val or decrease vertex count."
+            )
+
+        # generate graph input
+        random_vertices: list[int] = random.sample(
+            range(min_val, max_val + 1), vertex_count
+        )
+        random_edges: list[list[int]] = self.__generate_random_edges(
+            random_vertices, edge_pick_count
+        )
+
+        # build graphs
+        undirected_graph = GraphAdjacencyList(
+            vertices=random_vertices, edges=random_edges, directed=False
+        )
+        directed_graph = GraphAdjacencyList(
+            vertices=random_vertices, edges=random_edges, directed=True
+        )
+
+        return undirected_graph, directed_graph, random_vertices, random_edges
+
+    def test_init_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        # test graph initialization with vertices and edges
+        for num in random_vertices:
+            self.__assert_graph_vertex_exists_check(
+                undirected_graph, directed_graph, num
+            )
+
+        for edge in random_edges:
+            self.__assert_graph_edge_exists_check(
+                undirected_graph, directed_graph, edge
+            )
+        self.assertFalse(undirected_graph.directed)
+        self.assertTrue(directed_graph.directed)
+
+    def test_contains_vertex(self) -> None:
+        random_vertices: list[int] = random.sample(range(101), 20)
+
+        # Build graphs WITHOUT edges
+        undirected_graph = GraphAdjacencyList(
+            vertices=random_vertices, edges=[], directed=False
+        )
+        directed_graph = GraphAdjacencyList(
+            vertices=random_vertices, edges=[], directed=True
+        )
+
+        # Test contains_vertex
+        for num in range(101):
+            self.assertEqual(
+                num in random_vertices, undirected_graph.contains_vertex(num)
+            )
+            self.assertEqual(
+                num in random_vertices, directed_graph.contains_vertex(num)
+            )
+
+    def test_add_vertices(self) -> None:
+        random_vertices: list[int] = random.sample(range(101), 20)
+
+        # build empty graphs
+        undirected_graph: GraphAdjacencyList = GraphAdjacencyList(
+            vertices=[], edges=[], directed=False
+        )
+        directed_graph: GraphAdjacencyList = GraphAdjacencyList(
+            vertices=[], edges=[], directed=True
+        )
+
+        # run add_vertex
+        for num in random_vertices:
+            undirected_graph.add_vertex(num)
+
+        for num in random_vertices:
+            directed_graph.add_vertex(num)
+
+        # test add_vertex worked
+        for num in random_vertices:
+            self.__assert_graph_vertex_exists_check(
+                undirected_graph, directed_graph, num
+            )
+
+    def test_remove_vertices(self) -> None:
+        random_vertices: list[int] = random.sample(range(101), 20)
+
+        # build graphs WITHOUT edges
+        undirected_graph = GraphAdjacencyList(
+            vertices=random_vertices, edges=[], directed=False
+        )
+        directed_graph = GraphAdjacencyList(
+            vertices=random_vertices, edges=[], directed=True
+        )
+
+        # test remove_vertex worked
+        for num in random_vertices:
+            self.__assert_graph_vertex_exists_check(
+                undirected_graph, directed_graph, num
+            )
+
+            undirected_graph.remove_vertex(num)
+            directed_graph.remove_vertex(num)
+
+            self.__assert_graph_vertex_does_not_exist_check(
+                undirected_graph, directed_graph, num
+            )
+
+    def test_add_and_remove_vertices_repeatedly(self) -> None:
+        random_vertices1: list[int] = random.sample(range(51), 20)
+        random_vertices2: list[int] = random.sample(range(51, 101), 20)
+
+        # build graphs WITHOUT edges
+        undirected_graph = GraphAdjacencyList(
+            vertices=random_vertices1, edges=[], directed=False
+        )
+        directed_graph = GraphAdjacencyList(
+            vertices=random_vertices1, edges=[], directed=True
+        )
+
+        # test adding and removing vertices
+        for i, _ in enumerate(random_vertices1):
+            undirected_graph.add_vertex(random_vertices2[i])
+            directed_graph.add_vertex(random_vertices2[i])
+
+            self.__assert_graph_vertex_exists_check(
+                undirected_graph, directed_graph, random_vertices2[i]
+            )
+
+            undirected_graph.remove_vertex(random_vertices1[i])
+            directed_graph.remove_vertex(random_vertices1[i])
+
+            self.__assert_graph_vertex_does_not_exist_check(
+                undirected_graph, directed_graph, random_vertices1[i]
+            )
+
+        # remove all vertices
+        for i, _ in enumerate(random_vertices1):
+            undirected_graph.remove_vertex(random_vertices2[i])
+            directed_graph.remove_vertex(random_vertices2[i])
+
+            self.__assert_graph_vertex_does_not_exist_check(
+                undirected_graph, directed_graph, random_vertices2[i]
+            )
+
+    def test_contains_edge(self) -> None:
+        # generate graphs and graph input
+        vertex_count = 20
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(vertex_count, 0, 100, 4)
+
+        # generate all possible edges for testing
+        all_possible_edges: list[list[int]] = []
+        for i in range(vertex_count - 1):
+            for j in range(i + 1, vertex_count):
+                all_possible_edges.append([random_vertices[i], random_vertices[j]])
+                all_possible_edges.append([random_vertices[j], random_vertices[i]])
+
+        # test contains_edge function
+        for edge in all_possible_edges:
+            if edge in random_edges:
+                self.__assert_graph_edge_exists_check(
+                    undirected_graph, directed_graph, edge
+                )
+            elif [edge[1], edge[0]] in random_edges:
+                # since this edge exists for undirected but the reverse
+                # may not exist for directed
+                self.__assert_graph_edge_exists_check(
+                    undirected_graph, directed_graph, [edge[1], edge[0]]
+                )
+            else:
+                self.__assert_graph_edge_does_not_exist_check(
+                    undirected_graph, directed_graph, edge
+                )
+
+    def test_add_edge(self) -> None:
+        # generate graph input
+        random_vertices: list[int] = random.sample(range(101), 15)
+        random_edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4)
+
+        # build graphs WITHOUT edges
+        undirected_graph = GraphAdjacencyList(
+            vertices=random_vertices, edges=[], directed=False
+        )
+        directed_graph = GraphAdjacencyList(
+            vertices=random_vertices, edges=[], directed=True
+        )
+
+        # run and test add_edge
+        for edge in random_edges:
+            undirected_graph.add_edge(edge[0], edge[1])
+            directed_graph.add_edge(edge[0], edge[1])
+            self.__assert_graph_edge_exists_check(
+                undirected_graph, directed_graph, edge
+            )
+
+    def test_remove_edge(self) -> None:
+        # generate graph input and graphs
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        # run and test remove_edge
+        for edge in random_edges:
+            self.__assert_graph_edge_exists_check(
+                undirected_graph, directed_graph, edge
+            )
+            undirected_graph.remove_edge(edge[0], edge[1])
+            directed_graph.remove_edge(edge[0], edge[1])
+            self.__assert_graph_edge_does_not_exist_check(
+                undirected_graph, directed_graph, edge
+            )
+
+    def test_add_and_remove_edges_repeatedly(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        # make some more edge options!
+        more_random_edges: list[list[int]] = []
+
+        while len(more_random_edges) != len(random_edges):
+            edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4)
+            for edge in edges:
+                if len(more_random_edges) == len(random_edges):
+                    break
+                elif edge not in more_random_edges and edge not in random_edges:
+                    more_random_edges.append(edge)
+
+        for i, _ in enumerate(random_edges):
+            undirected_graph.add_edge(more_random_edges[i][0], more_random_edges[i][1])
+            directed_graph.add_edge(more_random_edges[i][0], more_random_edges[i][1])
+
+            self.__assert_graph_edge_exists_check(
+                undirected_graph, directed_graph, more_random_edges[i]
+            )
+
+            undirected_graph.remove_edge(random_edges[i][0], random_edges[i][1])
+            directed_graph.remove_edge(random_edges[i][0], random_edges[i][1])
+
+            self.__assert_graph_edge_does_not_exist_check(
+                undirected_graph, directed_graph, random_edges[i]
+            )
+
+    def test_add_vertex_exception_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        for vertex in random_vertices:
+            with self.assertRaises(ValueError):
+                undirected_graph.add_vertex(vertex)
+            with self.assertRaises(ValueError):
+                directed_graph.add_vertex(vertex)
+
+    def test_remove_vertex_exception_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        for i in range(101):
+            if i not in random_vertices:
+                with self.assertRaises(ValueError):
+                    undirected_graph.remove_vertex(i)
+                with self.assertRaises(ValueError):
+                    directed_graph.remove_vertex(i)
+
+    def test_add_edge_exception_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        for edge in random_edges:
+            with self.assertRaises(ValueError):
+                undirected_graph.add_edge(edge[0], edge[1])
+            with self.assertRaises(ValueError):
+                directed_graph.add_edge(edge[0], edge[1])
+
+    def test_remove_edge_exception_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        more_random_edges: list[list[int]] = []
+
+        while len(more_random_edges) != len(random_edges):
+            edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4)
+            for edge in edges:
+                if len(more_random_edges) == len(random_edges):
+                    break
+                elif edge not in more_random_edges and edge not in random_edges:
+                    more_random_edges.append(edge)
+
+        for edge in more_random_edges:
+            with self.assertRaises(ValueError):
+                undirected_graph.remove_edge(edge[0], edge[1])
+            with self.assertRaises(ValueError):
+                directed_graph.remove_edge(edge[0], edge[1])
+
+    def test_contains_edge_exception_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        for vertex in random_vertices:
+            with self.assertRaises(ValueError):
+                undirected_graph.contains_edge(vertex, 102)
+            with self.assertRaises(ValueError):
+                directed_graph.contains_edge(vertex, 102)
+
+        with self.assertRaises(ValueError):
+            undirected_graph.contains_edge(103, 102)
+        with self.assertRaises(ValueError):
+            directed_graph.contains_edge(103, 102)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/graphs/graph_adjacency_matrix.py b/graphs/graph_adjacency_matrix.py
new file mode 100644
--- /dev/null
+++ b/graphs/graph_adjacency_matrix.py
@@ -0,0 +1,608 @@
+#!/usr/bin/env python3
+"""
+Author: Vikram Nithyanandam
+
+Description:
+The following implementation is a robust unweighted Graph data structure
+implemented using an adjacency matrix. This vertices and edges of this graph can be
+effectively initialized and modified while storing your chosen generic
+value in each vertex.
+
+Adjacency Matrix: https://mathworld.wolfram.com/AdjacencyMatrix.html
+
+Potential Future Ideas:
+- Add a flag to set edge weights on and set edge weights
+- Make edge weights and vertex values customizable to store whatever the client wants
+- Support multigraph functionality if the client wants it
+"""
+from __future__ import annotations
+
+import random
+import unittest
+from pprint import pformat
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+
+class GraphAdjacencyMatrix(Generic[T]):
+    def __init__(
+        self, vertices: list[T], edges: list[list[T]], directed: bool = True
+    ) -> None:
+        """
+        Parameters:
+        - vertices: (list[T]) The list of vertex names the client wants to
+        pass in. Default is empty.
+        - edges: (list[list[T]]) The list of edges the client wants to
+        pass in. Each edge is a 2-element list. Default is empty.
+        - directed: (bool) Indicates if graph is directed or undirected.
+        Default is True.
+        """
+        self.directed = directed
+        self.vertex_to_index: dict[T, int] = {}
+        self.adj_matrix: list[list[int]] = []
+
+        # Falsey checks
+        edges = edges or []
+        vertices = vertices or []
+
+        for vertex in vertices:
+            self.add_vertex(vertex)
+
+        for edge in edges:
+            if len(edge) != 2:
+                msg = f"Invalid input: {edge} must have length 2."
+                raise ValueError(msg)
+            self.add_edge(edge[0], edge[1])
+
+    def add_edge(self, source_vertex: T, destination_vertex: T) -> None:
+        """
+        Creates an edge from source vertex to destination vertex. If any
+        given vertex doesn't exist or the edge already exists, a ValueError
+        will be thrown.
+        """
+        if not (
+            self.contains_vertex(source_vertex)
+            and self.contains_vertex(destination_vertex)
+        ):
+            msg = (
+                f"Incorrect input: Either {source_vertex} or "
+                f"{destination_vertex} does not exist"
+            )
+            raise ValueError(msg)
+        if self.contains_edge(source_vertex, destination_vertex):
+            msg = (
+                "Incorrect input: The edge already exists between "
+                f"{source_vertex} and {destination_vertex}"
+            )
+            raise ValueError(msg)
+
+        # Get the indices of the corresponding vertices and set their edge value to 1.
+        u: int = self.vertex_to_index[source_vertex]
+        v: int = self.vertex_to_index[destination_vertex]
+        self.adj_matrix[u][v] = 1
+        if not self.directed:
+            self.adj_matrix[v][u] = 1
+
+    def remove_edge(self, source_vertex: T, destination_vertex: T) -> None:
+        """
+        Removes the edge between the two vertices. If any given vertex
+        doesn't exist or the edge does not exist, a ValueError will be thrown.
+        """
+        if not (
+            self.contains_vertex(source_vertex)
+            and self.contains_vertex(destination_vertex)
+        ):
+            msg = (
+                f"Incorrect input: Either {source_vertex} or "
+                f"{destination_vertex} does not exist"
+            )
+            raise ValueError(msg)
+        if not self.contains_edge(source_vertex, destination_vertex):
+            msg = (
+                "Incorrect input: The edge does NOT exist between "
+                f"{source_vertex} and {destination_vertex}"
+            )
+            raise ValueError(msg)
+
+        # Get the indices of the corresponding vertices and set their edge value to 0.
+        u: int = self.vertex_to_index[source_vertex]
+        v: int = self.vertex_to_index[destination_vertex]
+        self.adj_matrix[u][v] = 0
+        if not self.directed:
+            self.adj_matrix[v][u] = 0
+
+    def add_vertex(self, vertex: T) -> None:
+        """
+        Adds a vertex to the graph. If the given vertex already exists,
+        a ValueError will be thrown.
+        """
+        if self.contains_vertex(vertex):
+            msg = f"Incorrect input: {vertex} already exists in this graph."
+            raise ValueError(msg)
+
+        # build column for vertex
+        for row in self.adj_matrix:
+            row.append(0)
+
+        # build row for vertex and update other data structures
+        self.adj_matrix.append([0] * (len(self.adj_matrix) + 1))
+        self.vertex_to_index[vertex] = len(self.adj_matrix) - 1
+
+    def remove_vertex(self, vertex: T) -> None:
+        """
+        Removes the given vertex from the graph and deletes all incoming and
+        outgoing edges from the given vertex as well. If the given vertex
+        does not exist, a ValueError will be thrown.
+        """
+        if not self.contains_vertex(vertex):
+            msg = f"Incorrect input: {vertex} does not exist in this graph."
+            raise ValueError(msg)
+
+        # first slide up the rows by deleting the row corresponding to
+        # the vertex being deleted.
+        start_index = self.vertex_to_index[vertex]
+        self.adj_matrix.pop(start_index)
+
+        # next, slide the columns to the left by deleting the values in
+        # the column corresponding to the vertex being deleted
+        for lst in self.adj_matrix:
+            lst.pop(start_index)
+
+        # final clean up
+        self.vertex_to_index.pop(vertex)
+
+        # decrement indices for vertices shifted by the deleted vertex in the adj matrix
+        for vertex in self.vertex_to_index:
+            if self.vertex_to_index[vertex] >= start_index:
+                self.vertex_to_index[vertex] = self.vertex_to_index[vertex] - 1
+
+    def contains_vertex(self, vertex: T) -> bool:
+        """
+        Returns True if the graph contains the vertex, False otherwise.
+        """
+        return vertex in self.vertex_to_index
+
+    def contains_edge(self, source_vertex: T, destination_vertex: T) -> bool:
+        """
+        Returns True if the graph contains the edge from the source_vertex to the
+        destination_vertex, False otherwise. If any given vertex doesn't exist, a
+        ValueError will be thrown.
+        """
+        if not (
+            self.contains_vertex(source_vertex)
+            and self.contains_vertex(destination_vertex)
+        ):
+            msg = (
+                f"Incorrect input: Either {source_vertex} "
+                f"or {destination_vertex} does not exist."
+            )
+            raise ValueError(msg)
+
+        u = self.vertex_to_index[source_vertex]
+        v = self.vertex_to_index[destination_vertex]
+        return self.adj_matrix[u][v] == 1
+
+    def clear_graph(self) -> None:
+        """
+        Clears all vertices and edges.
+        """
+        self.vertex_to_index = {}
+        self.adj_matrix = []
+
+    def __repr__(self) -> str:
+        first = "Adj Matrix:\n" + pformat(self.adj_matrix)
+        second = "\nVertex to index mapping:\n" + pformat(self.vertex_to_index)
+        return first + second
+
+
+class TestGraphMatrix(unittest.TestCase):
+    def __assert_graph_edge_exists_check(
+        self,
+        undirected_graph: GraphAdjacencyMatrix,
+        directed_graph: GraphAdjacencyMatrix,
+        edge: list[int],
+    ) -> None:
+        self.assertTrue(undirected_graph.contains_edge(edge[0], edge[1]))
+        self.assertTrue(undirected_graph.contains_edge(edge[1], edge[0]))
+        self.assertTrue(directed_graph.contains_edge(edge[0], edge[1]))
+
+    def __assert_graph_edge_does_not_exist_check(
+        self,
+        undirected_graph: GraphAdjacencyMatrix,
+        directed_graph: GraphAdjacencyMatrix,
+        edge: list[int],
+    ) -> None:
+        self.assertFalse(undirected_graph.contains_edge(edge[0], edge[1]))
+        self.assertFalse(undirected_graph.contains_edge(edge[1], edge[0]))
+        self.assertFalse(directed_graph.contains_edge(edge[0], edge[1]))
+
+    def __assert_graph_vertex_exists_check(
+        self,
+        undirected_graph: GraphAdjacencyMatrix,
+        directed_graph: GraphAdjacencyMatrix,
+        vertex: int,
+    ) -> None:
+        self.assertTrue(undirected_graph.contains_vertex(vertex))
+        self.assertTrue(directed_graph.contains_vertex(vertex))
+
+    def __assert_graph_vertex_does_not_exist_check(
+        self,
+        undirected_graph: GraphAdjacencyMatrix,
+        directed_graph: GraphAdjacencyMatrix,
+        vertex: int,
+    ) -> None:
+        self.assertFalse(undirected_graph.contains_vertex(vertex))
+        self.assertFalse(directed_graph.contains_vertex(vertex))
+
+    def __generate_random_edges(
+        self, vertices: list[int], edge_pick_count: int
+    ) -> list[list[int]]:
+        self.assertTrue(edge_pick_count <= len(vertices))
+
+        random_source_vertices: list[int] = random.sample(
+            vertices[0 : int(len(vertices) / 2)], edge_pick_count
+        )
+        random_destination_vertices: list[int] = random.sample(
+            vertices[int(len(vertices) / 2) :], edge_pick_count
+        )
+        random_edges: list[list[int]] = []
+
+        for source in random_source_vertices:
+            for dest in random_destination_vertices:
+                random_edges.append([source, dest])
+
+        return random_edges
+
+    def __generate_graphs(
+        self, vertex_count: int, min_val: int, max_val: int, edge_pick_count: int
+    ) -> tuple[GraphAdjacencyMatrix, GraphAdjacencyMatrix, list[int], list[list[int]]]:
+        if max_val - min_val + 1 < vertex_count:
+            raise ValueError(
+                "Will result in duplicate vertices. Either increase "
+                "range between min_val and max_val or decrease vertex count"
+            )
+
+        # generate graph input
+        random_vertices: list[int] = random.sample(
+            range(min_val, max_val + 1), vertex_count
+        )
+        random_edges: list[list[int]] = self.__generate_random_edges(
+            random_vertices, edge_pick_count
+        )
+
+        # build graphs
+        undirected_graph = GraphAdjacencyMatrix(
+            vertices=random_vertices, edges=random_edges, directed=False
+        )
+        directed_graph = GraphAdjacencyMatrix(
+            vertices=random_vertices, edges=random_edges, directed=True
+        )
+
+        return undirected_graph, directed_graph, random_vertices, random_edges
+
+    def test_init_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        # test graph initialization with vertices and edges
+        for num in random_vertices:
+            self.__assert_graph_vertex_exists_check(
+                undirected_graph, directed_graph, num
+            )
+
+        for edge in random_edges:
+            self.__assert_graph_edge_exists_check(
+                undirected_graph, directed_graph, edge
+            )
+
+        self.assertFalse(undirected_graph.directed)
+        self.assertTrue(directed_graph.directed)
+
+    def test_contains_vertex(self) -> None:
+        random_vertices: list[int] = random.sample(range(101), 20)
+
+        # Build graphs WITHOUT edges
+        undirected_graph = GraphAdjacencyMatrix(
+            vertices=random_vertices, edges=[], directed=False
+        )
+        directed_graph = GraphAdjacencyMatrix(
+            vertices=random_vertices, edges=[], directed=True
+        )
+
+        # Test contains_vertex
+        for num in range(101):
+            self.assertEqual(
+                num in random_vertices, undirected_graph.contains_vertex(num)
+            )
+            self.assertEqual(
+                num in random_vertices, directed_graph.contains_vertex(num)
+            )
+
+    def test_add_vertices(self) -> None:
+        random_vertices: list[int] = random.sample(range(101), 20)
+
+        # build empty graphs
+        undirected_graph: GraphAdjacencyMatrix = GraphAdjacencyMatrix(
+            vertices=[], edges=[], directed=False
+        )
+        directed_graph: GraphAdjacencyMatrix = GraphAdjacencyMatrix(
+            vertices=[], edges=[], directed=True
+        )
+
+        # run add_vertex
+        for num in random_vertices:
+            undirected_graph.add_vertex(num)
+
+        for num in random_vertices:
+            directed_graph.add_vertex(num)
+
+        # test add_vertex worked
+        for num in random_vertices:
+            self.__assert_graph_vertex_exists_check(
+                undirected_graph, directed_graph, num
+            )
+
+    def test_remove_vertices(self) -> None:
+        random_vertices: list[int] = random.sample(range(101), 20)
+
+        # build graphs WITHOUT edges
+        undirected_graph = GraphAdjacencyMatrix(
+            vertices=random_vertices, edges=[], directed=False
+        )
+        directed_graph = GraphAdjacencyMatrix(
+            vertices=random_vertices, edges=[], directed=True
+        )
+
+        # test remove_vertex worked
+        for num in random_vertices:
+            self.__assert_graph_vertex_exists_check(
+                undirected_graph, directed_graph, num
+            )
+
+            undirected_graph.remove_vertex(num)
+            directed_graph.remove_vertex(num)
+
+            self.__assert_graph_vertex_does_not_exist_check(
+                undirected_graph, directed_graph, num
+            )
+
+    def test_add_and_remove_vertices_repeatedly(self) -> None:
+        random_vertices1: list[int] = random.sample(range(51), 20)
+        random_vertices2: list[int] = random.sample(range(51, 101), 20)
+
+        # build graphs WITHOUT edges
+        undirected_graph = GraphAdjacencyMatrix(
+            vertices=random_vertices1, edges=[], directed=False
+        )
+        directed_graph = GraphAdjacencyMatrix(
+            vertices=random_vertices1, edges=[], directed=True
+        )
+
+        # test adding and removing vertices
+        for i, _ in enumerate(random_vertices1):
+            undirected_graph.add_vertex(random_vertices2[i])
+            directed_graph.add_vertex(random_vertices2[i])
+
+            self.__assert_graph_vertex_exists_check(
+                undirected_graph, directed_graph, random_vertices2[i]
+            )
+
+            undirected_graph.remove_vertex(random_vertices1[i])
+            directed_graph.remove_vertex(random_vertices1[i])
+
+            self.__assert_graph_vertex_does_not_exist_check(
+                undirected_graph, directed_graph, random_vertices1[i]
+            )
+
+        # remove all vertices
+        for i, _ in enumerate(random_vertices1):
+            undirected_graph.remove_vertex(random_vertices2[i])
+            directed_graph.remove_vertex(random_vertices2[i])
+
+            self.__assert_graph_vertex_does_not_exist_check(
+                undirected_graph, directed_graph, random_vertices2[i]
+            )
+
+    def test_contains_edge(self) -> None:
+        # generate graphs and graph input
+        vertex_count = 20
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(vertex_count, 0, 100, 4)
+
+        # generate all possible edges for testing
+        all_possible_edges: list[list[int]] = []
+        for i in range(vertex_count - 1):
+            for j in range(i + 1, vertex_count):
+                all_possible_edges.append([random_vertices[i], random_vertices[j]])
+                all_possible_edges.append([random_vertices[j], random_vertices[i]])
+
+        # test contains_edge function
+        for edge in all_possible_edges:
+            if edge in random_edges:
+                self.__assert_graph_edge_exists_check(
+                    undirected_graph, directed_graph, edge
+                )
+            elif [edge[1], edge[0]] in random_edges:
+                # since this edge exists for undirected but the reverse may
+                # not exist for directed
+                self.__assert_graph_edge_exists_check(
+                    undirected_graph, directed_graph, [edge[1], edge[0]]
+                )
+            else:
+                self.__assert_graph_edge_does_not_exist_check(
+                    undirected_graph, directed_graph, edge
+                )
+
+    def test_add_edge(self) -> None:
+        # generate graph input
+        random_vertices: list[int] = random.sample(range(101), 15)
+        random_edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4)
+
+        # build graphs WITHOUT edges
+        undirected_graph = GraphAdjacencyMatrix(
+            vertices=random_vertices, edges=[], directed=False
+        )
+        directed_graph = GraphAdjacencyMatrix(
+            vertices=random_vertices, edges=[], directed=True
+        )
+
+        # run and test add_edge
+        for edge in random_edges:
+            undirected_graph.add_edge(edge[0], edge[1])
+            directed_graph.add_edge(edge[0], edge[1])
+            self.__assert_graph_edge_exists_check(
+                undirected_graph, directed_graph, edge
+            )
+
+    def test_remove_edge(self) -> None:
+        # generate graph input and graphs
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        # run and test remove_edge
+        for edge in random_edges:
+            self.__assert_graph_edge_exists_check(
+                undirected_graph, directed_graph, edge
+            )
+            undirected_graph.remove_edge(edge[0], edge[1])
+            directed_graph.remove_edge(edge[0], edge[1])
+            self.__assert_graph_edge_does_not_exist_check(
+                undirected_graph, directed_graph, edge
+            )
+
+    def test_add_and_remove_edges_repeatedly(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        # make some more edge options!
+        more_random_edges: list[list[int]] = []
+
+        while len(more_random_edges) != len(random_edges):
+            edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4)
+            for edge in edges:
+                if len(more_random_edges) == len(random_edges):
+                    break
+                elif edge not in more_random_edges and edge not in random_edges:
+                    more_random_edges.append(edge)
+
+        for i, _ in enumerate(random_edges):
+            undirected_graph.add_edge(more_random_edges[i][0], more_random_edges[i][1])
+            directed_graph.add_edge(more_random_edges[i][0], more_random_edges[i][1])
+
+            self.__assert_graph_edge_exists_check(
+                undirected_graph, directed_graph, more_random_edges[i]
+            )
+
+            undirected_graph.remove_edge(random_edges[i][0], random_edges[i][1])
+            directed_graph.remove_edge(random_edges[i][0], random_edges[i][1])
+
+            self.__assert_graph_edge_does_not_exist_check(
+                undirected_graph, directed_graph, random_edges[i]
+            )
+
+    def test_add_vertex_exception_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        for vertex in random_vertices:
+            with self.assertRaises(ValueError):
+                undirected_graph.add_vertex(vertex)
+            with self.assertRaises(ValueError):
+                directed_graph.add_vertex(vertex)
+
+    def test_remove_vertex_exception_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        for i in range(101):
+            if i not in random_vertices:
+                with self.assertRaises(ValueError):
+                    undirected_graph.remove_vertex(i)
+                with self.assertRaises(ValueError):
+                    directed_graph.remove_vertex(i)
+
+    def test_add_edge_exception_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        for edge in random_edges:
+            with self.assertRaises(ValueError):
+                undirected_graph.add_edge(edge[0], edge[1])
+            with self.assertRaises(ValueError):
+                directed_graph.add_edge(edge[0], edge[1])
+
+    def test_remove_edge_exception_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        more_random_edges: list[list[int]] = []
+
+        while len(more_random_edges) != len(random_edges):
+            edges: list[list[int]] = self.__generate_random_edges(random_vertices, 4)
+            for edge in edges:
+                if len(more_random_edges) == len(random_edges):
+                    break
+                elif edge not in more_random_edges and edge not in random_edges:
+                    more_random_edges.append(edge)
+
+        for edge in more_random_edges:
+            with self.assertRaises(ValueError):
+                undirected_graph.remove_edge(edge[0], edge[1])
+            with self.assertRaises(ValueError):
+                directed_graph.remove_edge(edge[0], edge[1])
+
+    def test_contains_edge_exception_check(self) -> None:
+        (
+            undirected_graph,
+            directed_graph,
+            random_vertices,
+            random_edges,
+        ) = self.__generate_graphs(20, 0, 100, 4)
+
+        for vertex in random_vertices:
+            with self.assertRaises(ValueError):
+                undirected_graph.contains_edge(vertex, 102)
+            with self.assertRaises(ValueError):
+                directed_graph.contains_edge(vertex, 102)
+
+        with self.assertRaises(ValueError):
+            undirected_graph.contains_edge(103, 102)
+        with self.assertRaises(ValueError):
+            directed_graph.contains_edge(103, 102)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/graphs/graph_matrix.py b/graphs/graph_matrix.py
deleted file mode 100644
--- a/graphs/graph_matrix.py
+++ /dev/null
@@ -1,24 +0,0 @@
-class Graph:
-    def __init__(self, vertex):
-        self.vertex = vertex
-        self.graph = [[0] * vertex for i in range(vertex)]
-
-    def add_edge(self, u, v):
-        self.graph[u - 1][v - 1] = 1
-        self.graph[v - 1][u - 1] = 1
-
-    def show(self):
-        for i in self.graph:
-            for j in i:
-                print(j, end=" ")
-            print(" ")
-
-
-g = Graph(100)
-
-g.add_edge(1, 4)
-g.add_edge(4, 2)
-g.add_edge(4, 5)
-g.add_edge(2, 5)
-g.add_edge(5, 3)
-g.show()


Test patch:
diff --git a/graphs/tests/__init__.py b/graphs/tests/__init__.py
new file mode 100644