Repository: networkx/networkx

Issue:
Failed to save graph generated using stochastic_block_model
Saving a graph that has been generated with a stochastic_block_model is not possible using write_graphml.


### Current Behavior
When generating a random network using the stochastic block model, the returned networkx object cannot be written to a graphml file using the write_graphml function.

### Expected Behavior
When generating random graphs using the networkx package, it should be possible to export them using write_graphml.

### Steps to Reproduce
The following piece of code reproduces the error message:

```
import networkx as nx

wg = 0.8 # can be any number representing within group probability
bg = 0.2 # can be any number representing between group probability
community_probs = [[wg, bg, bg], 
                   [bg, wg, bg], 
                   [bg, bg, wg]]
community_sizes = [10,10,10]

G = nx.stochastic_block_model(community_sizes, community_probs)
nx.write_graphml(G, 'file_name.graphml')
```

### Environment

Python version: 3.9.6
NetworkX version: 2.6.2


### Additional context
Error message:

<img width="795" alt="Screenshot 2021-08-16 at 10 09 05" src="https://user-images.githubusercontent.com/44324476/129532047-658f7207-448b-4e33-8ad2-8092d498c87e.png">




Discussion:
Data attributes that have list values are not part of the GraphML spec. That is, you can't store graphs with list or dict attribute values using the GraphML standard. See #485 #3663 for similar discussions.

I would suggest that you save the graph in a different format.  OR you could clear out the offending data attributes before converting to GraphML.
Thanks @dschult - the only thing that comes to my mind as potentially "resolving" this issue would be to improve the exception message. Otherwise I'd say this can be closed as duplicate.
Thank you very much. The solution is indeed to use a different export method. Pickling the graph for example worked perfectly for me. What could be improved is the exception message (like mentioned by @rossbar). I also suggest that  the graph generator algorithm's documentation gets updated to include a warning that it generates graphs that cannot be exported to graphml. It will help future users to identify exactly what is happening. Thanks again for helping me out.

Resolving patch:
diff --git a/networkx/readwrite/graphml.py b/networkx/readwrite/graphml.py
--- a/networkx/readwrite/graphml.py
+++ b/networkx/readwrite/graphml.py
@@ -447,6 +447,17 @@ def construct_types(self):
         1: True,
     }
 
+    def get_xml_type(self, key):
+        """Wrapper around the xml_type dict that raises a more informative
+        exception message when a user attempts to use data of a type not
+        supported by GraphML."""
+        try:
+            return self.xml_type[key]
+        except KeyError as e:
+            raise TypeError(
+                f"GraphML does not support type {type(key)} as data values."
+            ) from e
+
 
 class GraphMLWriter(GraphML):
     def __init__(
@@ -504,7 +515,7 @@ def attr_type(self, name, scope, value):
             types = self.attribute_types[(name, scope)]
 
             if len(types) > 1:
-                types = {self.xml_type[t] for t in types}
+                types = {self.get_xml_type(t) for t in types}
                 if "string" in types:
                     return str
                 elif "float" in types or "double" in types:
@@ -551,7 +562,7 @@ def add_data(self, name, element_type, value, scope="all", default=None):
             raise nx.NetworkXError(
                 f"GraphML writer does not support {element_type} as data values."
             )
-        keyid = self.get_key(name, self.xml_type[element_type], scope, default)
+        keyid = self.get_key(name, self.get_xml_type(element_type), scope, default)
         data_element = self.myElement("data", key=keyid)
         data_element.text = str(value)
         return data_element
@@ -765,7 +776,7 @@ def add_graph_element(self, G):
         for k, v in graphdata.items():
             self.attribute_types[(str(k), "graph")].add(type(v))
         for k, v in graphdata.items():
-            element_type = self.xml_type[self.attr_type(k, "graph", v)]
+            element_type = self.get_xml_type(self.attr_type(k, "graph", v))
             self.get_key(str(k), element_type, "graph", None)
         # Nodes and data
         for node, d in G.nodes(data=True):
@@ -773,7 +784,7 @@ def add_graph_element(self, G):
                 self.attribute_types[(str(k), "node")].add(type(v))
         for node, d in G.nodes(data=True):
             for k, v in d.items():
-                T = self.xml_type[self.attr_type(k, "node", v)]
+                T = self.get_xml_type(self.attr_type(k, "node", v))
                 self.get_key(str(k), T, "node", node_default.get(k))
         # Edges and data
         if G.is_multigraph():
@@ -782,7 +793,7 @@ def add_graph_element(self, G):
                     self.attribute_types[(str(k), "edge")].add(type(v))
             for u, v, ekey, d in G.edges(keys=True, data=True):
                 for k, v in d.items():
-                    T = self.xml_type[self.attr_type(k, "edge", v)]
+                    T = self.get_xml_type(self.attr_type(k, "edge", v))
                     self.get_key(str(k), T, "edge", edge_default.get(k))
         else:
             for u, v, d in G.edges(data=True):
@@ -790,7 +801,7 @@ def add_graph_element(self, G):
                     self.attribute_types[(str(k), "edge")].add(type(v))
             for u, v, d in G.edges(data=True):
                 for k, v in d.items():
-                    T = self.xml_type[self.attr_type(k, "edge", v)]
+                    T = self.get_xml_type(self.attr_type(k, "edge", v))
                     self.get_key(str(k), T, "edge", edge_default.get(k))
 
         # Now add attribute keys to the xml file


Test patch:
diff --git a/networkx/readwrite/tests/test_graphml.py b/networkx/readwrite/tests/test_graphml.py
--- a/networkx/readwrite/tests/test_graphml.py
+++ b/networkx/readwrite/tests/test_graphml.py
@@ -1500,3 +1500,39 @@ class TestXMLGraphML(TestWriteGraphML):
     @classmethod
     def setup_class(cls):
         TestWriteGraphML.setup_class()
+
+
+def test_exception_for_unsupported_datatype_node_attr():
+    """Test that a detailed exception is raised when an attribute is of a type
+    not supported by GraphML, e.g. a list"""
+    pytest.importorskip("lxml.etree")
+    # node attribute
+    G = nx.Graph()
+    G.add_node(0, my_list_attribute=[0, 1, 2])
+    fh = io.BytesIO()
+    with pytest.raises(TypeError, match="GraphML does not support"):
+        nx.write_graphml(G, fh)
+
+
+def test_exception_for_unsupported_datatype_edge_attr():
+    """Test that a detailed exception is raised when an attribute is of a type
+    not supported by GraphML, e.g. a list"""
+    pytest.importorskip("lxml.etree")
+    # edge attribute
+    G = nx.Graph()
+    G.add_edge(0, 1, my_list_attribute=[0, 1, 2])
+    fh = io.BytesIO()
+    with pytest.raises(TypeError, match="GraphML does not support"):
+        nx.write_graphml(G, fh)
+
+
+def test_exception_for_unsupported_datatype_graph_attr():
+    """Test that a detailed exception is raised when an attribute is of a type
+    not supported by GraphML, e.g. a list"""
+    pytest.importorskip("lxml.etree")
+    # graph attribute
+    G = nx.Graph()
+    G.graph["my_list_attribute"] = [0, 1, 2]
+    fh = io.BytesIO()
+    with pytest.raises(TypeError, match="GraphML does not support"):
+        nx.write_graphml(G, fh)