      "* Do the edges have weights?\n",
      "    * Yes\n",
      "* If we try to add a node that already exists, do we just do nothing?\n",
      "    * Yes\n",
      "* If we try to delete a node that doesn't exist, do we just do nothing?\n",
      "    * Yes\n",
 
</s> remove     "* Can we assume the inputs are valid?\n",
 </s> add     "* Can the graph have cycles?\n", </s> remove     "    def add_undirected_edge(self, source_key, dest_key, weight=0):\n",
    "        if source_key is None or dest_key is None:\n",
    "            raise Exception('Invalid key')\n",
    "        self.add_edge(source_key, dest_key, weight)\n",
    "        self.nodes[dest_key].add_neighbor(self.nodes[source_key],\n",
    "                                          weight)"
 </s> add     "    def add_undirected_edge(self, src_key, dst_key, weight=0):\n",
    "        if src_key is None or dst_key is None:\n",
    "            raise TypeError('key cannot be None')\n",
    "        self.add_edge(src_key, dst_key, weight)\n",
    "        self.add_edge(dst_key, src_key, weight)" </s> remove     "        if neighbor is None or neighbor.key not in self.adj_nodes:\n",
    "            raise Exception('Invalid neighbor')\n",
 </s> add     "        if neighbor is None:\n",
    "            raise TypeError('neighbor cannot be None')\n",
    "        if neighbor.key not in self.adj_nodes:\n",
    "            raise KeyError('neighbor not found')\n",
    "* Can the graph have cycles?\n",
    "    * Yes\n",
      "* Do the edges have weights?\n",
      "    * Yes\n",
    "* Can the graph have cycles?\n",
    "    * Yes\n",
      "* If we try to add a node that already exists, do we just do nothing?\n",
      "    * Yes\n",
      "* If we try to delete a node that doesn't exist, do we just do nothing?\n",
      "    * Yes\n",
keep add keep keep keep keep
 Update graph challenge (#117)

Use more specific exception types. Update constraints. Update add_undirected_edge to use add_edge. 
graphs_trees/graph/graph_solution.ipynb