Repository: Qiskit/qiskit-terra (python)

Issue:
Submitting a non-string label to instructions leads to a TypeError when a QuantumCircuit instance is drawn.
### Environment

- **Qiskit Terra version**: 0.19.2
- **Python version**: 3.8.8
- **Operating system**: Pop!_OS 21.10


### What is happening?

While applying an `X` gate to two qubits, I made a mistake in which instead of passing in the target qubit indices as a list like so, `qc.x([0, 1])`, I submitted them as separate parameters as in,  `qc.x(0, 1)`. This lead to the following exception when I later called `qc.draw`:

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/username/anaconda3/lib/python3.8/site-packages/qiskit/visualization/text.py", line 657, in __repr__
    return self.single_string()
  File "/home/username/anaconda3/lib/python3.8/site-packages/qiskit/visualization/text.py", line 665, in single_string
    return "\n".join(self.lines()).encode(self.encoding).decode(self.encoding)
  File "/home/username/anaconda3/lib/python3.8/site-packages/qiskit/visualization/text.py", line 709, in lines
    layers = self.build_layers()
  File "/home/username/anaconda3/lib/python3.8/site-packages/qiskit/visualization/text.py", line 1138, in build_layers
    layer, current_connections, connection_label = self._node_to_gate(node, layer)
  File "/home/username/anaconda3/lib/python3.8/site-packages/qiskit/visualization/text.py", line 1005, in _node_to_gate
    gate_text = gate_text + params
TypeError: unsupported operand type(s) for +: 'int' and 'str'
```
I quickly realized that I had failed to pass in the indices as a list, but was puzzled as to why I was getting this exception. I took a look at the [documentation](https://qiskit.org/documentation/stubs/qiskit.circuit.QuantumCircuit.x.html?highlight=x#qiskit.circuit.QuantumCircuit.x) corresponding to `x` and saw that the second index had been stored in the optional `label` parameter. This results in a `TypeError` being raised [here](https://github.com/Qiskit/qiskit-terra/blob/1ae663d80295dce1b6ff25cbf29c640bf2ff9952/qiskit/visualization/text.py#L1055) when the instruction is processed during a draw.

Note that a similar error arises, when one attempts to draw the circuit using Matplotlib, so this is not isolated to text based circuit displays:
```
Traceback (most recent call last):
  File "error.py", line 6, in <module>
    qc.draw(output='mpl')
  File "/home/username/anaconda3/lib/python3.8/site-packages/qiskit/circuit/quantumcircuit.py", line 1865, in draw
    return circuit_drawer(
  File "/home/username/anaconda3/lib/python3.8/site-packages/qiskit/visualization/circuit_visualization.py", line 284, in circuit_drawer
    image = _matplotlib_circuit_drawer(
  File "/home/username/anaconda3/lib/python3.8/site-packages/qiskit/visualization/circuit_visualization.py", line 678, in _matplotlib_circuit_drawer
    return qcd.draw(filename)
  File "/home/username/anaconda3/lib/python3.8/site-packages/qiskit/visualization/matplotlib.py", line 278, in draw
    self._get_layer_widths()
  File "/home/username/anaconda3/lib/python3.8/site-packages/qiskit/visualization/matplotlib.py", line 394, in _get_layer_widths
    and len(gate_text) < 3
TypeError: object of type 'int' has no len()
```

### How can we reproduce the issue?

This issue can be produced by executing the following code:
```
from qiskit import QuantumCircuit

qc = QuantumCircuit(2)
qc.x(0, 1)
print(qc)
```
Note that here I use a `print` instead of a `draw` to demonstrate that the same code is touched on either path.

### What should happen?

As a Qiskit user, I would expect an exception to be raised when I attempt to create an instruction while providing a label with an invalid type, rather than when I try to draw it. Currently, I receive an exception, but it's distanced from the root cause and isn't as clear or clean as a `ValueError` raised on instruction creation would be.

### Any suggestions?

I'm new to Qiskit, so forgive me if my suggestions are off base, but I did some sleuthing and determined a possible fix. Looking through the code, I found that all instructions have [Instruction](https://github.com/Qiskit/qiskit-terra/blob/1ae663d80295dce1b6ff25cbf29c640bf2ff9952/qiskit/circuit/instruction.py#L50) as their base class. If we were to add a type check to the [section](https://github.com/Qiskit/qiskit-terra/blob/1ae663d80295dce1b6ff25cbf29c640bf2ff9952/qiskit/circuit/instruction.py#L84) of `Instruction`'s constructor which sets the optional `label` when present, we could raise a `ValueError` that would notify the user of their mistake as soon as the instruction is created. I have a draft of such a fix working locally and would love to submit a pull request, if the group would like to assign this issue to me. Even if we don't go with my suggested fix, my willingness to help out remains.

Pull request description:
Ensuring instruction labels are strings when provided to the Instruction constructor.
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary

Fixes #7667 by adding a type check to the `Instruction` constructor which will raise a `TypeError` if the `label` parameter is provided, but not of type `str`. Per the associated issue, this guarantees that an informative exception is raised for labels of an invalid type at instruction creation time rather than when the label is later processed by `print` or `draw`.

### Details and comments

Note that this pull request also adds a test for the type check that already existed for the `label` setter, but which was not under test coverage. I thought it prudent to add this test so that if our type checking rules change for labels in the future, we immediately know via a test failure that a change needs to be applied in both locations.


Patch:
diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py
index d1bd8e2cf..ed7ff3f1d 100644
--- a/qiskit/circuit/instruction.py
+++ b/qiskit/circuit/instruction.py
@@ -69,6 +69,7 @@ class Instruction:
 
         Raises:
             CircuitError: when the register is not in the correct format.
+            TypeError: when the optional label is provided, but it is not a string.
         """
         if not isinstance(num_qubits, int) or not isinstance(num_clbits, int):
             raise CircuitError("num_qubits and num_clbits must be integer.")
@@ -86,6 +87,8 @@ class Instruction:
         #       already set is a temporary work around that can be removed after
         #       the next stable qiskit-aer release
         if not hasattr(self, "_label"):
+            if label is not None and not isinstance(label, str):
+                raise TypeError("label expects a string or None")
             self._label = label
         # tuple (ClassicalRegister, int), tuple (Clbit, bool) or tuple (Clbit, int)
         # when the instruction has a conditional ("if")
diff --git a/releasenotes/notes/type-check-instruction-labels-on-creation-8399dd8b5d72f272.yaml b/releasenotes/notes/type-check-instruction-labels-on-creation-8399dd8b5d72f272.yaml
new file mode 100644
index 000000000..a284e46c9
--- /dev/null
+++ b/releasenotes/notes/type-check-instruction-labels-on-creation-8399dd8b5d72f272.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+  - |
+    :class:`~.circuit.Instruction` labels are now type-checked on instruction creation.


Test patch:
diff --git a/test/python/circuit/test_extensions_standard.py b/test/python/circuit/test_extensions_standard.py
index 152b73e6b..a37d2a588 100644
--- a/test/python/circuit/test_extensions_standard.py
+++ b/test/python/circuit/test_extensions_standard.py
@@ -1455,7 +1455,7 @@ class TestStandardMethods(QiskitTestCase):
                 # gate_class is abstract
                 continue
             sig = signature(gate_class)
-            free_params = len(set(sig.parameters) - {"label"})
+            free_params = len(set(sig.parameters) - {"label", "ctrl_state"})
             try:
                 if gate_class == PauliGate:
                     # special case due to PauliGate using string parameters
@@ -1508,7 +1508,7 @@ class TestStandardMethods(QiskitTestCase):
                 # n_qubits argument is no longer supported.
                 free_params = 2
             else:
-                free_params = len(set(sig.parameters) - {"label"})
+                free_params = len(set(sig.parameters) - {"label", "ctrl_state"})
             try:
                 if gate_class == PauliGate:
                     # special case due to PauliGate using string parameters
diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py
index e346e77d4..ad2b7a99b 100644
--- a/test/python/circuit/test_instructions.py
+++ b/test/python/circuit/test_instructions.py
@@ -692,6 +692,19 @@ class TestInstructions(QiskitTestCase):
             for instruction in instruction_list:
                 self.assertIs(instruction.condition[0], sentinel_register)
 
+    def test_label_type_enforcement(self):
+        """Test instruction label type enforcement."""
+        with self.subTest("accepts string labels"):
+            instruction = Instruction("h", 1, 0, [], label="label")
+            self.assertEqual(instruction.label, "label")
+        with self.subTest("raises when a non-string label is provided to constructor"):
+            with self.assertRaisesRegex(TypeError, r"label expects a string or None"):
+                Instruction("h", 1, 0, [], label=0)
+        with self.subTest("raises when a non-string label is provided to setter"):
+            with self.assertRaisesRegex(TypeError, r"label expects a string or None"):
+                instruction = HGate()
+                instruction.label = 0
+
 
 if __name__ == "__main__":
     unittest.main()