Repository: Qiskit/qiskit

Issue:
`QuantumCircuit.copy(name=?)` allows for any object to be used as name
### Environment

- **Qiskit Terra version**: 0.43.1 meta package, terra 0.24.1
- **Python version**: 3.10
- **Operating system**: docker continuumio/miniconda3

### What is happening?

Creating a circuit via copy allows to give an invalid name (not a string), that is then checked only when the circuit is transpiled.
In this case the `copy()` api raises no error when passing a circuit as name (see API: [copy](https://qiskit.org/documentation/stubs/qiskit.circuit.QuantumCircuit.copy.html))

### How can we reproduce the issue?

Run this python script:

```python
from qiskit import QuantumCircuit
from qiskit.compiler import transpile
qc_output = QuantumCircuit(2, 2)
input_circuit = QuantumCircuit(2, 2)
input_circuit.measure([0, 1], [0, 1])
qc_output = qc_output.compose(input_circuit, qubits=range(2))
final_circuit = qc_output.copy(qc_output)
final_circuit.measure([0, 1], [0, 1])
print(final_circuit.draw())

print("Circuit name: ", final_circuit.name)

transpile(final_circuit, optimization_level=0)
```
Produces this output and error:
```bash
     ┌─┐   ┌─┐
q_0: ┤M├───┤M├───
     └╥┘┌─┐└╥┘┌─┐
q_1: ─╫─┤M├─╫─┤M├
      ║ └╥┘ ║ └╥┘
c: 2/═╩══╩══╩══╩═
      0  1  0  1
Circuit name:       ┌─┐
q_0: ┤M├───
     └╥┘┌─┐
q_1: ─╫─┤M├
      ║ └╥┘
c: 2/═╩══╩═
      0  1
Traceback (most recent call last):
  File "myfile.py", line 13, in <module>                       transpile(final_circuit, optimization_level=0)
  File ".../qiskit/compiler/transpiler.py", line 380, in transpile                    _serial_transpile_circuit(
  File ".../qiskit/compiler/transpiler.py", line 462, in _serial_transpile_circuit    result = pass_manager.run(circuit, callback=callback, output_name=outpu
t_name)                                                                      File ".../qiskit/transpiler/passmanager.py", line 537, in run                       return super().run(circuits, output_name, callback)
  File ".../qiskit/transpiler/passmanager.py", line 231, in run                       return self._run_single_circuit(circuits, output_name, callback)
  File ".../qiskit/transpiler/passmanager.py", line 292, in _run_single_circuit       result = running_passmanager.run(circuit, output_name=output_name, call
back=callback)                                                               File ".../qiskit/transpiler/runningpassmanager.py", line 127, in run                circuit = dag_to_circuit(dag, copy_operations=False)
  File ".../qiskit/converters/dag_to_circuit.py", line 58, in dag_to_circuit          circuit = QuantumCircuit(
  File ".../qiskit/circuit/quantumcircuit.py", line 260, in __init__                  raise CircuitError(
qiskit.circuit.exceptions.CircuitError: 'The circuit name should be a strin
g (or None to auto-generate a name).'
```

### What should happen?

I would expect the `copy` method to check the name of the circuit, and raise an error if it is not a string, whereas now it is allowed without any check (see [here](https://github.com/Qiskit/qiskit-terra/blob/802a735ebea547d0d96339c2de4a10f04b0ab8a6/qiskit/circuit/quantumcircuit.py#L2231)).

Even this is allowed:
```python
...
class AnyObject(object):
    def __init__(self, field):
        self.field = field
new_circuit = qc_output.copy(name=AnyObject("new_circuit"))
print(new_circuit.name)
```
Outputting:
```
...
<__main__.AnyObject object at 0x7f7c98809ab0>
```

### Any suggestions?

I think the `copy` method should check the name of the circuit, and raise an error if it is not a string, so that the error is more precise on the line of the `copy()` call rather than the `transpile()`.



Discussion:
This is somewhat a regular part of Python programming; the language doesn't enforce type checking.  You'll _always_ be able to put badly typed objects into Python classes, since the language fundamentally doesn't have access control.

That said, in this particular case, we _do_ do the manual check in `QuantumCircuit.__init__`, it's not very costly to do, and doesn't have greater performance implications, so it's reasonable that we could do the check in `QuantumCircuit.copy` as well.
@jakelishman @MattePalte 
I would like to solve this issue and contribute to Qiskit. Can you please assign me?
Thank you
running tox -elint-incr on my repository gives the following error:
`ERROR: sympy is imported via qiskit.circuit.quantumcircuit`

But sympy is not being imported. **Why is this error showing up?**

And by the way, I have fixed this issue and am in the testing phase. Will make a pull request once the above error gets solved. 

EDIT - I think it is showing up because qiskit/circuit/parameter.py is importing sympy 
@Abhiraj-Shrotriya 
For various reasons, Sympy is optionally imported. Tox flags that as an error, but this is not the case in the CI tests. If this is the only error you are facing, you should pass the CI tests.
Updating my branch to the latest version of Qiskit:main breaks my Pull Request. The CI tests ( which of course take time) need to rerun. Some of these re-tests fail which passed in the original run. 

Will not updating my branch affect merging process?

You don't need to update your branch to `main` unless there are merge conflicts.  We'll handle that automatically during the final merge window.

Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -2180,7 +2180,7 @@ def copy(self, name: str | None = None) -> "QuantumCircuit":
         """Copy the circuit.
 
         Args:
-          name (str): name to be given to the copied circuit. If None, then the name stays the same
+          name (str): name to be given to the copied circuit. If None, then the name stays the same.
 
         Returns:
           QuantumCircuit: a deepcopy of the current circuit, with the specified name
@@ -2222,6 +2222,10 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit":
         Returns:
             QuantumCircuit: An empty copy of self.
         """
+        if not (name is None or isinstance(name, str)):
+            raise TypeError(
+                f"invalid name for a circuit: '{name}'. The name must be a string or 'None'."
+            )
         cpy = copy.copy(self)
         # copy registers correctly, in copy.copy they are only copied via reference
         cpy.qregs = self.qregs.copy()


Test patch:
diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py
--- a/test/python/circuit/test_circuit_operations.py
+++ b/test/python/circuit/test_circuit_operations.py
@@ -389,6 +389,22 @@ def test_copy_empty_like_circuit(self):
         copied = qc.copy_empty_like("copy")
         self.assertEqual(copied.name, "copy")
 
+    def test_circuit_copy_rejects_invalid_types(self):
+        """Test copy method rejects argument with type other than 'string' and 'None' type."""
+        qc = QuantumCircuit(1, 1)
+        qc.h(0)
+
+        with self.assertRaises(TypeError):
+            qc.copy([1, "2", 3])
+
+    def test_circuit_copy_empty_like_rejects_invalid_types(self):
+        """Test copy_empty_like method rejects argument with type other than 'string' and 'None' type."""
+        qc = QuantumCircuit(1, 1)
+        qc.h(0)
+
+        with self.assertRaises(TypeError):
+            qc.copy_empty_like(123)
+
     def test_clear_circuit(self):
         """Test clear method deletes instructions in circuit."""
         qr = QuantumRegister(2)