Repository: rigetti/pyquil (python)

Issue:
Change the namedtuples in device.py to dataclasses
As discussed in #961, using `dataclasses` instead of `namedtuples` would greatly improve readability, understanding, and use of the structures in the `device` module.


Discussion:

Patch:
diff --git a/pyquil/device/_isa.py b/pyquil/device/_isa.py
--- a/pyquil/device/_isa.py
+++ b/pyquil/device/_isa.py
@@ -13,8 +13,8 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
-from collections import namedtuple
-from typing import Union
+import sys
+from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
 
 import networkx as nx
 import numpy as np
@@ -22,35 +22,64 @@
 from pyquil.quilatom import Parameter, unpack_qubit
 from pyquil.quilbase import Gate
 
-THETA = Parameter("theta")
-"Used as the symbolic parameter in RZ, CPHASE gates."
+if sys.version_info < (3, 7):
+    from pyquil.external.dataclasses import dataclass
+else:
+    from dataclasses import dataclass
 
 DEFAULT_QUBIT_TYPE = "Xhalves"
 DEFAULT_EDGE_TYPE = "CZ"
+THETA = Parameter("theta")
+"Used as the symbolic parameter in RZ, CPHASE gates."
+
 
-Qubit = namedtuple("Qubit", ["id", "type", "dead", "gates"])
-Edge = namedtuple("Edge", ["targets", "type", "dead", "gates"])
-_ISA = namedtuple("_ISA", ["qubits", "edges"])
+@dataclass
+class MeasureInfo:
+    operator: Optional[str] = None
+    qubit: Optional[int] = None
+    target: Optional[Union[int, str]] = None
+    duration: Optional[float] = None
+    fidelity: Optional[float] = None
 
-MeasureInfo = namedtuple("MeasureInfo", ["operator", "qubit", "target", "duration", "fidelity"])
-GateInfo = namedtuple("GateInfo", ["operator", "parameters", "arguments", "duration", "fidelity"])
 
-# make Qubit and Edge arguments optional
-Qubit.__new__.__defaults__ = (None,) * len(Qubit._fields)
-Edge.__new__.__defaults__ = (None,) * len(Edge._fields)
-MeasureInfo.__new__.__defaults__ = (None,) * len(MeasureInfo._fields)
-GateInfo.__new__.__defaults__ = (None,) * len(GateInfo._fields)
+@dataclass
+class GateInfo:
+    operator: Optional[str] = None
+    parameters: Optional[Sequence[Union[str, float]]] = None
+    arguments: Optional[Sequence[Union[str, float]]] = None
+    duration: Optional[float] = None
+    fidelity: Optional[float] = None
 
 
-class ISA(_ISA):
+@dataclass
+class Qubit:
+    id: int
+    type: Optional[str] = None
+    dead: Optional[bool] = None
+    gates: Optional[Sequence[Union[GateInfo, MeasureInfo]]] = None
+
+
+@dataclass
+class Edge:
+    targets: Tuple[int, ...]
+    type: Optional[str] = None
+    dead: Optional[bool] = None
+    gates: Optional[Sequence[GateInfo]] = None
+
+
+@dataclass
+class ISA:
     """
     Basic Instruction Set Architecture specification.
 
-    :ivar Sequence[Qubit] qubits: The qubits associated with the ISA.
-    :ivar Sequence[Edge] edges: The multi-qubit gates.
+    :ivar qubits: The qubits associated with the ISA.
+    :ivar edges: The multi-qubit gates.
     """
 
-    def to_dict(self):
+    qubits: Sequence[Qubit]
+    edges: Sequence[Edge]
+
+    def to_dict(self) -> Dict[str, Any]:
         """
         Create a JSON-serializable representation of the ISA.
 
@@ -80,19 +109,17 @@ def to_dict(self):
             }
 
         :return: A dictionary representation of self.
-        :rtype: Dict[str, Any]
         """
 
-        def _maybe_configure(o, t):
-            # type: (Union[Qubit,Edge], str) -> dict
+        def _maybe_configure(o: Union[Qubit, Edge], t: str) -> Dict[str, Any]:
             """
             Exclude default values from generated dictionary.
 
-            :param Union[Qubit,Edge] o: The object to serialize
-            :param str t: The default value for ``o.type``.
+            :param o: The object to serialize
+            :param t: The default value for ``o.type``.
             :return: d
             """
-            d = {}
+            d: Dict[str, Any] = {}
             if o.gates is not None:
                 d["gates"] = [
                     {
@@ -127,13 +154,12 @@ def _maybe_configure(o, t):
         }
 
     @staticmethod
-    def from_dict(d):
+    def from_dict(d: Dict[str, Any]) -> "ISA":
         """
         Re-create the ISA from a dictionary representation.
 
-        :param Dict[str,Any] d: The dictionary representation.
+        :param d: The dictionary representation.
         :return: The restored ISA.
-        :rtype: ISA
         """
         return ISA(
             qubits=sorted(
@@ -150,7 +176,7 @@ def from_dict(d):
             edges=sorted(
                 [
                     Edge(
-                        targets=[int(q) for q in eid.split("-")],
+                        targets=tuple(int(q) for q in eid.split("-")),
                         type=e.get("type", DEFAULT_EDGE_TYPE),
                         dead=e.get("dead", False),
                     )
@@ -161,13 +187,12 @@ def from_dict(d):
         )
 
 
-def gates_in_isa(isa):
+def gates_in_isa(isa: ISA) -> List[Gate]:
     """
     Generate the full gateset associated with an ISA.
 
-    :param ISA isa: The instruction set architecture for a QPU.
+    :param isa: The instruction set architecture for a QPU.
     :return: A sequence of Gate objects encapsulating all gates compatible with the ISA.
-    :rtype: Sequence[Gate]
     """
     gates = []
     for q in isa.qubits:
@@ -211,6 +236,7 @@ def gates_in_isa(isa):
             gates.append(Gate("XY", [THETA], targets))
             gates.append(Gate("XY", [THETA], targets[::-1]))
             continue
+        assert e.type is not None
         if "WILDCARD" in e.type:
             gates.append(Gate("_", "_", targets))
             gates.append(Gate("_", "_", targets[::-1]))
@@ -220,7 +246,7 @@ def gates_in_isa(isa):
     return gates
 
 
-def isa_from_graph(graph: nx.Graph, oneq_type="Xhalves", twoq_type="CZ") -> ISA:
+def isa_from_graph(graph: nx.Graph, oneq_type: str = "Xhalves", twoq_type: str = "CZ") -> ISA:
     """
     Generate an ISA object from a NetworkX graph.
 
@@ -230,7 +256,7 @@ def isa_from_graph(graph: nx.Graph, oneq_type="Xhalves", twoq_type="CZ") -> ISA:
     """
     all_qubits = list(range(max(graph.nodes) + 1))
     qubits = [Qubit(i, type=oneq_type, dead=i not in graph.nodes) for i in all_qubits]
-    edges = [Edge(sorted((a, b)), type=twoq_type, dead=False) for a, b in graph.edges]
+    edges = [Edge(tuple(sorted((a, b))), type=twoq_type, dead=False) for a, b in graph.edges]
     return ISA(qubits, edges)
 
 
diff --git a/pyquil/device/_main.py b/pyquil/device/_main.py
--- a/pyquil/device/_main.py
+++ b/pyquil/device/_main.py
@@ -15,7 +15,7 @@
 ##############################################################################
 import warnings
 from abc import ABC, abstractmethod
-from typing import List, Tuple
+from typing import Any, Dict, List, Optional, Tuple, Union
 
 import networkx as nx
 import numpy as np
@@ -42,7 +42,7 @@
 
 class AbstractDevice(ABC):
     @abstractmethod
-    def qubits(self):
+    def qubits(self) -> List[int]:
         """
         A sorted list of qubits in the device topology.
         """
@@ -54,7 +54,7 @@ def qubit_topology(self) -> nx.Graph:
         """
 
     @abstractmethod
-    def get_isa(self, oneq_type="Xhalves", twoq_type="CZ") -> ISA:
+    def get_isa(self, oneq_type: str = "Xhalves", twoq_type: str = "CZ") -> ISA:
         """
         Construct an ISA suitable for targeting by compilation.
 
@@ -65,7 +65,7 @@ def get_isa(self, oneq_type="Xhalves", twoq_type="CZ") -> ISA:
         """
 
     @abstractmethod
-    def get_specs(self) -> Specs:
+    def get_specs(self) -> Optional[Specs]:
         """
         Construct a Specs object required by compilation
         """
@@ -86,7 +86,7 @@ class Device(AbstractDevice):
     :ivar NoiseModel noise_model: The noise model for the device.
     """
 
-    def __init__(self, name, raw):
+    def __init__(self, name: str, raw: Dict[str, Any]):
         """
         :param name: name of the device
         :param raw: raw JSON response from the server with additional information about this device.
@@ -102,23 +102,25 @@ def __init__(self, name, raw):
         )
 
     @property
-    def isa(self):
+    def isa(self) -> Optional[ISA]:
         warnings.warn("Accessing the static ISA is deprecated. Use `get_isa`", DeprecationWarning)
         return self._isa
 
-    def qubits(self):
+    def qubits(self) -> List[int]:
+        assert self._isa is not None
         return sorted(q.id for q in self._isa.qubits if not q.dead)
 
     def qubit_topology(self) -> nx.Graph:
         """
         The connectivity of qubits in this device given as a NetworkX graph.
         """
+        assert self._isa is not None
         return isa_to_graph(self._isa)
 
-    def get_specs(self):
+    def get_specs(self) -> Optional[Specs]:
         return self.specs
 
-    def get_isa(self, oneq_type=None, twoq_type=None) -> ISA:
+    def get_isa(self, oneq_type: Optional[str] = None, twoq_type: Optional[str] = None) -> ISA:
         """
         Construct an ISA suitable for targeting by compilation.
 
@@ -130,7 +132,7 @@ def get_isa(self, oneq_type=None, twoq_type=None) -> ISA:
                 "make an ISA with custom gate types, you'll have to do it by hand."
             )
 
-        def safely_get(attr, index, default):
+        def safely_get(attr: str, index: Union[int, Tuple[int, ...]], default: Any) -> Any:
             if self.specs is None:
                 return default
 
@@ -144,8 +146,8 @@ def safely_get(attr, index, default):
             else:
                 return default
 
-        def qubit_type_to_gates(q):
-            gates = [
+        def qubit_type_to_gates(q: Qubit) -> List[Union[GateInfo, MeasureInfo]]:
+            gates: List[Union[GateInfo, MeasureInfo]] = [
                 MeasureInfo(
                     operator="MEASURE",
                     qubit=q.id,
@@ -200,9 +202,9 @@ def qubit_type_to_gates(q):
                 ]
             return gates
 
-        def edge_type_to_gates(e):
-            gates = []
-            if e is None or "CZ" in e.type:
+        def edge_type_to_gates(e: Edge) -> List[GateInfo]:
+            gates: List[GateInfo] = []
+            if e is None or isinstance(e.type, str) and "CZ" in e.type:
                 gates += [
                     GateInfo(
                         operator="CZ",
@@ -212,7 +214,7 @@ def edge_type_to_gates(e):
                         fidelity=safely_get("fCZs", tuple(e.targets), DEFAULT_CZ_FIDELITY),
                     )
                 ]
-            if e is not None and "ISWAP" in e.type:
+            if e is None or isinstance(e.type, str) and "ISWAP" in e.type:
                 gates += [
                     GateInfo(
                         operator="ISWAP",
@@ -222,7 +224,7 @@ def edge_type_to_gates(e):
                         fidelity=safely_get("fISWAPs", tuple(e.targets), DEFAULT_ISWAP_FIDELITY),
                     )
                 ]
-            if e is not None and "CPHASE" in e.type:
+            if e is None or isinstance(e.type, str) and "CPHASE" in e.type:
                 gates += [
                     GateInfo(
                         operator="CPHASE",
@@ -232,7 +234,7 @@ def edge_type_to_gates(e):
                         fidelity=safely_get("fCPHASEs", tuple(e.targets), DEFAULT_CPHASE_FIDELITY),
                     )
                 ]
-            if e is not None and "XY" in e.type:
+            if e is None or isinstance(e.type, str) and "XY" in e.type:
                 gates += [
                     GateInfo(
                         operator="XY",
@@ -242,7 +244,7 @@ def edge_type_to_gates(e):
                         fidelity=safely_get("fXYs", tuple(e.targets), DEFAULT_XY_FIDELITY),
                     )
                 ]
-            if e is not None and "WILDCARD" in e.type:
+            if e is None or isinstance(e.type, str) and "WILDCARD" in e.type:
                 gates += [
                     GateInfo(
                         operator="_",
@@ -254,6 +256,7 @@ def edge_type_to_gates(e):
                 ]
             return gates
 
+        assert self._isa is not None
         qubits = [
             Qubit(id=q.id, type=None, dead=q.dead, gates=qubit_type_to_gates(q))
             for q in self._isa.qubits
@@ -264,10 +267,10 @@ def edge_type_to_gates(e):
         ]
         return ISA(qubits, edges)
 
-    def __str__(self):
+    def __str__(self) -> str:
         return "<Device {}>".format(self.name)
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return str(self)
 
 
@@ -284,17 +287,17 @@ class NxDevice(AbstractDevice):
     def __init__(self, topology: nx.Graph) -> None:
         self.topology = topology
 
-    def qubit_topology(self):
+    def qubit_topology(self) -> nx.Graph:
         return self.topology
 
-    def get_isa(self, oneq_type="Xhalves", twoq_type="CZ"):
+    def get_isa(self, oneq_type: str = "Xhalves", twoq_type: str = "CZ") -> ISA:
         return isa_from_graph(self.topology, oneq_type=oneq_type, twoq_type=twoq_type)
 
-    def get_specs(self):
+    def get_specs(self) -> Specs:
         return specs_from_graph(self.topology)
 
     def qubits(self) -> List[int]:
         return sorted(self.topology.nodes)
 
-    def edges(self) -> List[Tuple[int, int]]:
-        return sorted(tuple(sorted(pair)) for pair in self.topology.edges)  # type: ignore
+    def edges(self) -> List[Tuple[Any, ...]]:
+        return sorted(tuple(sorted(pair)) for pair in self.topology.edges)
diff --git a/pyquil/device/_specs.py b/pyquil/device/_specs.py
--- a/pyquil/device/_specs.py
+++ b/pyquil/device/_specs.py
@@ -13,104 +13,105 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 ##############################################################################
+import sys
 import warnings
-from collections import namedtuple
+from typing import Any, Dict, Optional, Sequence, Tuple
 
 import networkx as nx
 
-QubitSpecs = namedtuple(
-    "_QubitSpecs",
-    [
-        "id",
-        "fRO",
-        "f1QRB",
-        "f1QRB_std_err",
-        "f1Q_simultaneous_RB",
-        "f1Q_simultaneous_RB_std_err",
-        "T1",
-        "T2",
-        "fActiveReset",
-    ],
-)
-EdgeSpecs = namedtuple(
-    "_QubitQubitSpecs",
-    [
-        "targets",
-        "fBellState",
-        "fCZ",
-        "fCZ_std_err",
-        "fCPHASE",
-        "fCPHASE_std_err",
-        "fXY",
-        "fXY_std_err",
-        "fISWAP",
-        "fISWAP_std_err",
-    ],
-)
-_Specs = namedtuple("_Specs", ["qubits_specs", "edges_specs"])
-
-
-class Specs(_Specs):
+if sys.version_info < (3, 7):
+    from pyquil.external.dataclasses import dataclass
+else:
+    from dataclasses import dataclass
+
+
+@dataclass
+class QubitSpecs:
+    id: int
+    fRO: Optional[float]
+    f1QRB: Optional[float]
+    f1QRB_std_err: Optional[float]
+    f1Q_simultaneous_RB: Optional[float]
+    f1Q_simultaneous_RB_std_err: Optional[float]
+    T1: Optional[float]
+    T2: Optional[float]
+    fActiveReset: Optional[float]
+
+
+@dataclass
+class EdgeSpecs:
+    targets: Tuple[int, ...]
+    fBellState: Optional[float]
+    fCZ: Optional[float]
+    fCZ_std_err: Optional[float]
+    fCPHASE: Optional[float]
+    fCPHASE_std_err: Optional[float]
+    fXY: Optional[float]
+    fXY_std_err: Optional[float]
+    fISWAP: Optional[float]
+    fISWAP_std_err: Optional[float]
+
+
+@dataclass
+class Specs:
     """
     Basic specifications for the device, such as gate fidelities and coherence times.
 
-    :ivar List[QubitSpecs] qubits_specs: The specs associated with individual qubits.
-    :ivar List[EdgesSpecs] edges_specs: The specs associated with edges, or qubit-qubit pairs.
+    :ivar qubits_specs: The specs associated with individual qubits.
+    :ivar edges_specs: The specs associated with edges, or qubit-qubit pairs.
     """
 
-    def f1QRBs(self):
+    qubits_specs: Sequence[QubitSpecs]
+    edges_specs: Sequence[EdgeSpecs]
+
+    def f1QRBs(self) -> Dict[int, Optional[float]]:
         """
         Get a dictionary of single-qubit randomized benchmarking fidelities (for individual gate
         operation, normalized to unity) from the specs, keyed by qubit index.
 
         :return: A dictionary of 1Q RB fidelities, normalized to unity.
-        :rtype: Dict[int, float]
         """
         return {qs.id: qs.f1QRB for qs in self.qubits_specs}
 
-    def f1QRB_std_errs(self):
+    def f1QRB_std_errs(self) -> Dict[int, Optional[float]]:
         """
         Get a dictionary of the standard errors of single-qubit randomized
         benchmarking fidelities (for individual gate operation, normalized to unity)
         from the specs, keyed by qubit index.
 
         :return: A dictionary of 1Q RB fidelity standard errors, normalized to unity.
-        :rtype: Dict[int, float]
         """
         return {qs.id: qs.f1QRB_std_err for qs in self.qubits_specs}
 
-    def f1Q_simultaneous_RBs(self):
+    def f1Q_simultaneous_RBs(self) -> Dict[int, Optional[float]]:
         """
         Get a dictionary of single-qubit randomized benchmarking fidelities (for simultaneous gate
         operation across the chip, normalized to unity) from the specs, keyed by qubit index.
 
         :return: A dictionary of simultaneous 1Q RB fidelities, normalized to unity.
-        :rtype: Dict[int, float]
         """
         return {qs.id: qs.f1Q_simultaneous_RB for qs in self.qubits_specs}
 
-    def f1Q_simultaneous_RB_std_errs(self):
+    def f1Q_simultaneous_RB_std_errs(self) -> Dict[int, Optional[float]]:
         """
         Get a dictionary of the standard errors of single-qubit randomized
         benchmarking fidelities (for simultaneous gate operation across the chip, normalized to
         unity) from the specs, keyed by qubit index.
 
         :return: A dictionary of simultaneous 1Q RB fidelity standard errors, normalized to unity.
-        :rtype: Dict[int, float]
         """
         return {qs.id: qs.f1Q_simultaneous_RB_std_err for qs in self.qubits_specs}
 
-    def fROs(self):
+    def fROs(self) -> Dict[int, Optional[float]]:
         """
         Get a dictionary of single-qubit readout fidelities (normalized to unity)
         from the specs, keyed by qubit index.
 
         :return: A dictionary of RO fidelities, normalized to unity.
-        :rtype: Dict[int, float]
         """
         return {qs.id: qs.fRO for qs in self.qubits_specs}
 
-    def fActiveResets(self):
+    def fActiveResets(self) -> Dict[int, Optional[float]]:
         """
         Get a dictionary of single-qubit active reset fidelities (normalized to unity) from the
         specs, keyed by qubit index.
@@ -119,31 +120,28 @@ def fActiveResets(self):
         """
         return {qs.id: qs.fActiveReset for qs in self.qubits_specs}
 
-    def T1s(self):
+    def T1s(self) -> Dict[int, Optional[float]]:
         """
         Get a dictionary of T1s (in seconds) from the specs, keyed by qubit index.
 
         :return: A dictionary of T1s, in seconds.
-        :rtype: Dict[int, float]
         """
         return {qs.id: qs.T1 for qs in self.qubits_specs}
 
-    def T2s(self):
+    def T2s(self) -> Dict[int, Optional[float]]:
         """
         Get a dictionary of T2s (in seconds) from the specs, keyed by qubit index.
 
         :return: A dictionary of T2s, in seconds.
-        :rtype: Dict[int, float]
         """
         return {qs.id: qs.T2 for qs in self.qubits_specs}
 
-    def fBellStates(self):
+    def fBellStates(self) -> Dict[Tuple[int, ...], Optional[float]]:
         """
         Get a dictionary of two-qubit Bell state fidelities (normalized to unity)
         from the specs, keyed by targets (qubit-qubit pairs).
 
         :return: A dictionary of Bell state fidelities, normalized to unity.
-        :rtype: Dict[tuple(int, int), float]
         """
         warnings.warn(
             DeprecationWarning(
@@ -153,73 +151,66 @@ def fBellStates(self):
         )
         return {tuple(es.targets): es.fBellState for es in self.edges_specs}
 
-    def fCZs(self):
+    def fCZs(self) -> Dict[Tuple[int, ...], Optional[float]]:
         """
         Get a dictionary of CZ fidelities (normalized to unity) from the specs,
         keyed by targets (qubit-qubit pairs).
 
         :return: A dictionary of CZ fidelities, normalized to unity.
-        :rtype: Dict[tuple(int, int), float]
         """
         return {tuple(es.targets): es.fCZ for es in self.edges_specs}
 
-    def fISWAPs(self):
+    def fISWAPs(self) -> Dict[Tuple[int, ...], Optional[float]]:
         """
         Get a dictionary of ISWAP fidelities (normalized to unity) from the specs,
         keyed by targets (qubit-qubit pairs).
 
         :return: A dictionary of ISWAP fidelities, normalized to unity.
-        :rtype: Dict[tuple(int, int), float]
         """
         return {tuple(es.targets): es.fISWAP for es in self.edges_specs}
 
-    def fISWAP_std_errs(self):
+    def fISWAP_std_errs(self) -> Dict[Tuple[int, ...], Optional[float]]:
         """
         Get a dictionary of the standard errors of the ISWAP fidelities from the specs,
         keyed by targets (qubit-qubit pairs).
 
         :return: A dictionary of ISWAP fidelities, normalized to unity.
-        :rtype: Dict[tuple(int, int), float]
         """
         return {tuple(es.targets): es.fISWAP_std_err for es in self.edges_specs}
 
-    def fXYs(self):
+    def fXYs(self) -> Dict[Tuple[int, ...], Optional[float]]:
         """
         Get a dictionary of XY(pi) fidelities (normalized to unity) from the specs,
         keyed by targets (qubit-qubit pairs).
 
         :return: A dictionary of XY/2 fidelities, normalized to unity.
-        :rtype: Dict[tuple(int, int), float]
         """
         return {tuple(es.targets): es.fXY for es in self.edges_specs}
 
-    def fXY_std_errs(self):
+    def fXY_std_errs(self) -> Dict[Tuple[int, ...], Optional[float]]:
         """
         Get a dictionary of the standard errors of the XY fidelities from the specs,
         keyed by targets (qubit-qubit pairs).
 
         :return: A dictionary of XY fidelities, normalized to unity.
-        :rtype: Dict[tuple(int, int), float]
         """
         return {tuple(es.targets): es.fXY_std_err for es in self.edges_specs}
 
-    def fCZ_std_errs(self):
+    def fCZ_std_errs(self) -> Dict[Tuple[int, ...], Optional[float]]:
         """
         Get a dictionary of the standard errors of the CZ fidelities from the specs,
         keyed by targets (qubit-qubit pairs).
 
         :return: A dictionary of CZ fidelities, normalized to unity.
-        :rtype: Dict[tuple(int, int), float]
         """
         return {tuple(es.targets): es.fCZ_std_err for es in self.edges_specs}
 
-    def fCPHASEs(self):
+    def fCPHASEs(self) -> Dict[Tuple[int, ...], Optional[float]]:
         """
         Get a dictionary of CPHASE fidelities (normalized to unity) from the specs,
         keyed by targets (qubit-qubit pairs).
 
         :return: A dictionary of CPHASE fidelities, normalized to unity.
-        :rtype: Dict[tuple(int, int), float]
         """
         warnings.warn(
             DeprecationWarning(
@@ -229,7 +220,7 @@ def fCPHASEs(self):
         )
         return {tuple(es.targets): es.fCPHASE for es in self.edges_specs}
 
-    def to_dict(self):
+    def to_dict(self) -> Dict[str, Any]:
         """
         Create a JSON-serializable representation of the device Specs.
 
@@ -270,7 +261,6 @@ def to_dict(self):
             }
 
         :return: A dctionary representation of self.
-        :rtype: Dict[str, Any]
         """
         return {
             "1Q": {
@@ -303,13 +293,12 @@ def to_dict(self):
         }
 
     @staticmethod
-    def from_dict(d):
+    def from_dict(d: Dict[str, Any]) -> "Specs":
         """
         Re-create the Specs from a dictionary representation.
 
-        :param Dict[str, Any] d: The dictionary representation.
+        :param d: The dictionary representation.
         :return: The restored Specs.
-        :rtype: Specs
         """
         return Specs(
             qubits_specs=sorted(
@@ -332,7 +321,7 @@ def from_dict(d):
             edges_specs=sorted(
                 [
                     EdgeSpecs(
-                        targets=[int(q) for q in e.split("-")],
+                        targets=tuple(int(q) for q in e.split("-")),
                         fBellState=especs.get("fBellState"),
                         fCZ=especs.get("fCZ"),
                         fCZ_std_err=especs.get("fCZ_std_err"),
@@ -350,7 +339,7 @@ def from_dict(d):
         )
 
 
-def specs_from_graph(graph: nx.Graph):
+def specs_from_graph(graph: nx.Graph) -> Specs:
     """
     Generate a Specs object from a NetworkX graph with placeholder values for the actual specs.
 


Test patch:
diff --git a/pyquil/device/tests/test_device.py b/pyquil/device/tests/test_device.py
--- a/pyquil/device/tests/test_device.py
+++ b/pyquil/device/tests/test_device.py
@@ -55,10 +55,10 @@ def test_isa(isa_dict):
             Qubit(id=3, type="Xhalves", dead=True),
         ],
         edges=[
-            Edge(targets=[0, 1], type="CZ", dead=False),
-            Edge(targets=[0, 2], type="CPHASE", dead=False),
-            Edge(targets=[0, 3], type="CZ", dead=True),
-            Edge(targets=[1, 2], type="ISWAP", dead=False),
+            Edge(targets=(0, 1), type="CZ", dead=False),
+            Edge(targets=(0, 2), type="CPHASE", dead=False),
+            Edge(targets=(0, 3), type="CZ", dead=True),
+            Edge(targets=(1, 2), type="ISWAP", dead=False),
         ],
     )
     assert isa == ISA.from_dict(isa.to_dict())
@@ -115,7 +115,7 @@ def test_specs(specs_dict):
         ],
         edges_specs=[
             EdgeSpecs(
-                targets=[0, 1],
+                targets=(0, 1),
                 fBellState=0.90,
                 fCZ=0.89,
                 fCZ_std_err=0.01,
@@ -127,7 +127,7 @@ def test_specs(specs_dict):
                 fCPHASE_std_err=None,
             ),
             EdgeSpecs(
-                targets=[0, 2],
+                targets=(0, 2),
                 fBellState=0.92,
                 fCZ=0.91,
                 fCZ_std_err=0.20,
@@ -139,7 +139,7 @@ def test_specs(specs_dict):
                 fCPHASE_std_err=None,
             ),
             EdgeSpecs(
-                targets=[0, 3],
+                targets=(0, 3),
                 fBellState=0.89,
                 fCZ=0.88,
                 fCZ_std_err=0.03,
@@ -151,7 +151,7 @@ def test_specs(specs_dict):
                 fCPHASE_std_err=None,
             ),
             EdgeSpecs(
-                targets=[1, 2],
+                targets=(1, 2),
                 fBellState=0.91,
                 fCZ=0.90,
                 fCZ_std_err=0.12,
diff --git a/pyquil/tests/test_quantum_computer.py b/pyquil/tests/test_quantum_computer.py
--- a/pyquil/tests/test_quantum_computer.py
+++ b/pyquil/tests/test_quantum_computer.py
@@ -194,8 +194,8 @@ def test_device_stuff():
     assert nx.is_isomorphic(qc.qubit_topology(), topo)
 
     isa = qc.get_isa(twoq_type="CPHASE")
-    assert sorted(isa.edges)[0].type == "CPHASE"
-    assert sorted(isa.edges)[0].targets == [0, 4]
+    assert isa.edges[0].type == "CPHASE"
+    assert isa.edges[0].targets == (0, 4)
 
 
 def test_run(forest):