Writers Guide
The writer subsystem serialises a Graph to external formats for use in other tools — Gephi, yEd, pandas, R, Excel, text-analysis packages, or any tool that reads GraphML, CSV, or plain text. The single entry point is graph.write(path, format=...). Researchers who want to hand off filtered graph data or graph-associated material to a separate analysis environment will use this most often.
Format registration is open: third-party packages can add new formats by subclassing Writer and calling register_writer. The built-in formats are GraphML, CSV, and plain text.
Tabular export
Two paths to a tabular view of a graph:
| Output | Method | When to use |
|---|---|---|
In-memory list[dict] |
graph.entity_records() / graph.relationship_records() |
Notebook analysis, wrapping with pandas / polars / pyarrow, immediate exploration. No third-party dependencies. |
On-disk nodes.csv / edges.csv |
graph.write(path, format="csv") (see CSV below) |
Hand-off to other tools (R, Excel, Gephi), archival, sharing. All values become strings on disk. |
The records methods preserve native Python types (lists stay lists, dicts stay dicts, None stays None); the CSV writer flattens to scalars because CSV cells require it. Pick whichever matches your downstream workflow.
In-memory records
from crategraph import Crate
crate = Crate("path/to/crate/")
entity_rows = crate.entity_records()
relationship_rows = crate.relationship_records()
# Wrap with whichever DataFrame library you prefer:
import pandas as pd
nodes_df = pd.DataFrame(entity_rows)
edges_df = pd.DataFrame(relationship_rows)
entity_records() returns one dict per entity with promoted keys id, label, type, types first, then non-colliding property keys sorted alphabetically, then any properties whose names collide with a promoted key emitted as prop_<key> (e.g. an entity with a property named "id" becomes prop_id so the entity's own id stays untouched). relationship_records() returns one dict per relationship with promoted keys source, target, type, rel_id first under the same ordering rule. rel_id is None for inline (non-reified) relationships and a string for reified ones — the CSV writer uses an empty string in the same position, so this is the one place the two paths differ. Property values are deep-copied — mutating a returned record does not affect graph state.
Text export
Two paths to a text view of graph-associated content:
| Output | Method | When to use |
|---|---|---|
| In-memory text records | graph.text_records() |
Notebook analysis, NLP pipelines, preserving document boundaries and provenance. |
| On-disk corpus file | graph.write(path, format="text") (see Text below) |
Hand-off to text-analysis tools, archival, sharing, or any workflow that expects a plain text file. |
The text writer is a thin export layer over text_records(). It does not perform separate extraction logic: file text, property text, filtering, cached index reads, and view restriction all flow through the public text-records API.
graph.text_records() defaults to source_kind="file". Pass
source_kind="properties" to work with metadata-derived text records, or
source_kind="all" to include both file and property text.
For notebook and NLP workflows that need lightweight metadata alongside
the extracted text, pass include_properties:
Requested properties are added as top-level keys in each record when
present. Pass include_properties=True to include all public entity
properties (internal _-prefixed loader flags such as _is_root are
excluded; an explicit allowlist is honoured verbatim). The default is
False, so the base record shape remains compact.
Formats
| Format key | Output shape | Typical consumer | Notes |
|---|---|---|---|
"graphml" |
Single .graphml file |
Gephi, yEd, NetworkX read_graphml, pandas |
All attributes serialised as scalars; lxml preferred, pure-Python fallback. Round-tripping preserves string values; numeric types (int, float, bool) survive GraphML's typed attribute system. |
"csv" |
Directory with nodes.csv + edges.csv |
pandas read_csv, R read.csv, Excel |
All cells are strings when read back. Pipe-delimited lists and JSON blobs can be decoded with decode_pipe_list and json.loads respectively. |
"text" / "txt" |
Single .txt or .md file |
Voyant, AntConc, quanteda, NLP notebooks, plain text readers | Writes text records with optional provenance headers. Defaults to file-derived text; can also export property text. |
Usage
GraphML
from crategraph import Crate
crate = Crate("path/to/ro-crate")
# Write to a GraphML file
crate.write("output.graphml", format="graphml")
# Overwrite an existing file
crate.write("output.graphml", format="graphml", overwrite=True)
The path must end with .graphml (or otherwise be a file path — GraphML writes a single file, not a directory).
On a graph with no entities the output is still valid GraphML with an empty graph element.
CSV
# Write nodes.csv and edges.csv into a new directory
crate.write("output_tables/", format="csv")
# Overwrite contents of an existing non-empty directory
crate.write("output_tables/", format="csv", overwrite=True)
The directory is created (including parent directories) if it does not exist. If it already exists and is non-empty, FileExistsError is raised unless overwrite=True. If the path exists as a non-directory file, FileExistsError is raised unconditionally.
Reading the output back with pandas:
import pandas as pd
nodes = pd.read_csv("output_tables/nodes.csv")
edges = pd.read_csv("output_tables/edges.csv")
CSV is export-only — there is no CSV reader that reconstructs a Graph. Use GraphML (via nx.read_graphml) if you need a round-trippable format, or reload the data into pandas/R for downstream analysis.
Text
# Write extracted file text to one corpus file
crate.write("corpus.txt", format="text")
# Include text built from selected metadata properties instead
crate.write("metadata-text.md", format="text", source_kind="properties")
# Export both file text and property text
crate.write("all-text.txt", format="text", source_kind="all")
# Use cached text from a previously-built semantic index
crate.write("corpus.txt", format="text", store_path="index.db", overwrite=True)
Text export writes one UTF-8 file. By default it exports source_kind="file", meaning file contents extracted by graph.text_records(). Pass source_kind="properties" for metadata-derived text records, or source_kind="all" to export both.
Each text unit is separated by a plain --- delimiter and, by default, begins with provenance headers:
# source_id: minimal-crate
# entity_id: sample.txt
# source_kind: file
# entity_types: File
Extracted text...
The writer accepts the same filtering concepts as text_records():
crate.write(
"selected-corpus.txt",
format="text",
filters={"entity_id": ["sample.txt", "notes.md"]},
)
When using the writer-level source_kind option, do not also include source_kind inside filters; pass source_kind="all" if you want full control through filters.
Listing registered formats
Third-party packages that register additional formats show up here automatically once imported.
Graph.to_networkx()
graph.to_networkx(copy=True) builds and returns an nx.MultiDiGraph view of the graph, bypassing all writer machinery. This is useful when you need NetworkX algorithms not exposed through the crategraph API.
import networkx as nx
G = crate.to_networkx() # deep copy — safe to mutate
G_raw = crate.to_networkx(copy=False) # original Entity/Relationship objects — do not mutate
With copy=True (the default), the returned graph is a fully detached deep copy. You can mutate nodes, edges, and any nested dicts without affecting the source Graph. This matters because @dataclass(frozen=True) freezes the top-level Entity and Relationship objects but not their properties dicts — a deep copy is the only safe isolation.
With copy=False, the returned MultiDiGraph is still rebuilt, but the original Entity and Relationship objects are attached directly. Only use this when you need lower copy overhead and can guarantee you will not mutate those objects or their nested properties.
Attribute Flattening Rules
GraphML and CSV both require scalar attribute values (str, int, float, bool). The shared flattening module (crategraph.writers._flatten) converts rich Entity and Relationship objects to flat dicts before writing.
Promoted columns
Every node row begins with these columns in this order:
| Column | Source |
|---|---|
id |
entity.id |
label |
properties["name"] → properties["title"] → entity.id (truthy check — empty strings fall through) |
type |
First entry of entity.types, or "" |
types |
All types, pipe-delimited (see below) |
Every edge row begins with:
| Column | Source |
|---|---|
source |
rel.source |
target |
rel.target |
type |
rel.type |
rel_id |
rel.id if set, else "" |
Remaining property keys are appended in alphabetical order after the promoted columns.
Property name collisions
If a property key in entity.properties or rel.properties collides with a promoted column name (e.g. an entity with a "type" property), it is emitted as prop_<key> (e.g. prop_type). This avoids silently overwriting the promoted value.
If a user-defined property is already called prop_<promoted> (e.g. both "id" and "prop_id" appear in properties), the user-defined key keeps its name and the promoted-collision value is pushed one level further out. In that example:
idpromoted column →entity.id.- User's
prop_idproperty → stays atprop_id. - User's
idproperty (the promoted-name collision) →prop_prop_id.
The prefix chain lengthens as needed so no property is ever dropped.
Scalar passthrough
| Input type | Output |
|---|---|
None |
"" (empty string) |
bool |
bool (passed through; checked before int because bool is a subclass) |
int, float, str |
passed through unchanged |
Lists of scalars — pipe-delimited encoding
Lists (or tuples) containing only scalar values (str, int, float, bool, None) are encoded as pipe-delimited strings:
- Backslashes are escaped first:
\→\\ - Then pipes are escaped:
|→\| - Items are joined with
|
Example: ["a", "b|c", "d\\e"] → "a|b\|c|d\\\\e"
This encoding is reversible. Import decode_pipe_list from crategraph.writers._flatten to decode:
from crategraph.writers._flatten import decode_pipe_list
types_str = nodes_df["types"].iloc[0] # e.g. "Person|Agent"
types_list = decode_pipe_list(types_str) # ["Person", "Agent"]
An empty string decodes to []. The decoder returns strings — original numeric types are not preserved.
Nested dicts and lists of dicts
Any value that is a dict, or a list/tuple containing at least one non-scalar element, is serialised with json.dumps(value, sort_keys=True, ensure_ascii=False). Use json.loads to decode:
Format-Specific Notes
GraphML
- Writes a single
.graphmlfile (XML-based). - Parallel edges with the same source, target, and type are preserved using globally unique edge IDs (
e0,e1, ...). Reified relationship IDs are preserved in therel_idattribute. - Uses
nx.write_graphml_lxml(requireslxml) for performance. Falls back tonx.write_graphml(pure Python) iflxmlis not installed. - Reading back with NetworkX:
nx.read_graphml("output.graphml"). Entity IDs round-trip as the NetworkX node keys, and they also appear on each node under the promotedidattribute. GraphML attribute values come back typed whenlxmlis available, as strings otherwise — passnode_type=strif you want to force string keys regardless. - Reading back with pandas:
nx.to_pandas_edgelist(nx.read_graphml("output.graphml")).
CSV
- Writes
nodes.csvandedges.csvusing Python's stdlibcsv.DictWriter. - Column order: promoted columns first (in the fixed order above), then remaining property keys alphabetically.
- Line endings: stdlib
csvdefault (\r\n) for maximum interoperability with Excel and RFC 4180. - All values are strings when read back — numeric columns in the original graph are not typed in CSV. Cast explicitly in pandas:
nodes["degree"] = nodes["degree"].astype(int). - An empty graph still produces valid CSV files with header rows.
Extending — Custom Writers
To add a new export format:
- Subclass
Writerfromcrategraph.core.interfacesand implementcan_write(path) -> boolandwrite(graph, path, **kwargs) -> None. - Call
register_writer("myformat", MyWriter)at import time (e.g. at the bottom of your module). - Users can then call
graph.write("output.xyz", format="myformat").
The existing GraphMLWriter (crategraph/writers/graphml.py), CsvWriter (crategraph/writers/csv_writer.py), and TextWriter (crategraph/writers/text_writer.py) are the canonical references. GraphML and CSV use flatten_node and flatten_edge from crategraph.writers._flatten to handle the scalar-conversion step — reuse these if your target format also requires scalar-only attributes. Text export instead uses the public graph.text_records() API, which is the better pattern for writers over graph-associated content rather than the graph structure itself.