Repository: Qiskit/qiskit

Issue:
PassManager.run doesnt preserve input signature for length 1 list
### Environment

- **Qiskit Terra version**: 0.23.2
- **Python version**: 3.9
- **Operating system**: MacOS


### What is happening?

When calling `PassManager.run` directly, it flattens a list of 1 circuit to a quantum circuit rather than returning a list. This makes it difficult to write code that uses the pass manager directly where you want to assume the input and output is always a list of circuits, since currently you have to check the type of the output and convert back to a list for length 1 inputs.

### How can we reproduce the issue?

```python
from qiskit import QuantumCircuit
from qiskit.transpiler import PassManager

qc =  QuantumCircuit(1)
pm = PassManager([])
pm.run([qc])

<qiskit.circuit.quantumcircuit.QuantumCircuit at 0x105edf3d0>
```
a `QuantumCircuit` is returned instead of a list containing the single transpiled circuit.

### What should happen?

If the input to `PassManager.run` is a list, the return type should also be a list, regardless of how many entries in the list.

### Any suggestions?

This is probably a simple fix in `PassManager.run`
PassManager.run doesnt preserve input signature for length 1 list
### Environment

- **Qiskit Terra version**: 0.23.2
- **Python version**: 3.9
- **Operating system**: MacOS


### What is happening?

When calling `PassManager.run` directly, it flattens a list of 1 circuit to a quantum circuit rather than returning a list. This makes it difficult to write code that uses the pass manager directly where you want to assume the input and output is always a list of circuits, since currently you have to check the type of the output and convert back to a list for length 1 inputs.

### How can we reproduce the issue?

```python
from qiskit import QuantumCircuit
from qiskit.transpiler import PassManager

qc =  QuantumCircuit(1)
pm = PassManager([])
pm.run([qc])

<qiskit.circuit.quantumcircuit.QuantumCircuit at 0x105edf3d0>
```
a `QuantumCircuit` is returned instead of a list containing the single transpiled circuit.

### What should happen?

If the input to `PassManager.run` is a list, the return type should also be a list, regardless of how many entries in the list.

### Any suggestions?

This is probably a simple fix in `PassManager.run`
PassManager.run doesnt preserve input signature for length 1 list
### Environment

- **Qiskit Terra version**: 0.23.2
- **Python version**: 3.9
- **Operating system**: MacOS


### What is happening?

When calling `PassManager.run` directly, it flattens a list of 1 circuit to a quantum circuit rather than returning a list. This makes it difficult to write code that uses the pass manager directly where you want to assume the input and output is always a list of circuits, since currently you have to check the type of the output and convert back to a list for length 1 inputs.

### How can we reproduce the issue?

```python
from qiskit import QuantumCircuit
from qiskit.transpiler import PassManager

qc =  QuantumCircuit(1)
pm = PassManager([])
pm.run([qc])

<qiskit.circuit.quantumcircuit.QuantumCircuit at 0x105edf3d0>
```
a `QuantumCircuit` is returned instead of a list containing the single transpiled circuit.

### What should happen?

If the input to `PassManager.run` is a list, the return type should also be a list, regardless of how many entries in the list.

### Any suggestions?

This is probably a simple fix in `PassManager.run`


Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py
--- a/qiskit/transpiler/passmanager.py
+++ b/qiskit/transpiler/passmanager.py
@@ -14,7 +14,7 @@
 
 import io
 import re
-from typing import Union, List, Tuple, Callable, Dict, Any, Optional, Iterator, Iterable
+from typing import Union, List, Tuple, Callable, Dict, Any, Optional, Iterator, Iterable, TypeVar
 
 import dill
 
@@ -25,6 +25,9 @@
 from .runningpassmanager import RunningPassManager, FlowController
 
 
+_CircuitsT = TypeVar("_CircuitsT", bound=Union[List[QuantumCircuit], QuantumCircuit])
+
+
 class PassManager:
     """Manager for a set of Passes and their scheduling during transpilation."""
 
@@ -183,10 +186,10 @@ def _normalize_passes(
 
     def run(
         self,
-        circuits: Union[QuantumCircuit, List[QuantumCircuit]],
-        output_name: str = None,
-        callback: Callable = None,
-    ) -> Union[QuantumCircuit, List[QuantumCircuit]]:
+        circuits: _CircuitsT,
+        output_name: Optional[str] = None,
+        callback: Optional[Callable] = None,
+    ) -> _CircuitsT:
         """Run all the passes on the specified ``circuits``.
 
         Args:
@@ -227,7 +230,7 @@ def callback_func(**kwargs):
         if isinstance(circuits, QuantumCircuit):
             return self._run_single_circuit(circuits, output_name, callback)
         if len(circuits) == 1:
-            return self._run_single_circuit(circuits[0], output_name, callback)
+            return [self._run_single_circuit(circuits[0], output_name, callback)]
         return self._run_several_circuits(circuits, output_name, callback)
 
     def _create_running_passmanager(self) -> RunningPassManager:
@@ -244,7 +247,10 @@ def _in_parallel(circuit, pm_dill=None) -> QuantumCircuit:
         return result
 
     def _run_several_circuits(
-        self, circuits: List[QuantumCircuit], output_name: str = None, callback: Callable = None
+        self,
+        circuits: List[QuantumCircuit],
+        output_name: Optional[str] = None,
+        callback: Optional[Callable] = None,
     ) -> List[QuantumCircuit]:
         """Run all the passes on the specified ``circuits``.
 
@@ -266,7 +272,10 @@ def _run_several_circuits(
         )
 
     def _run_single_circuit(
-        self, circuit: QuantumCircuit, output_name: str = None, callback: Callable = None
+        self,
+        circuit: QuantumCircuit,
+        output_name: Optional[str] = None,
+        callback: Optional[Callable] = None,
     ) -> QuantumCircuit:
         """Run all the passes on a ``circuit``.
 
@@ -520,10 +529,10 @@ def passes(self) -> List[Dict[str, BasePass]]:
 
     def run(
         self,
-        circuits: Union[QuantumCircuit, List[QuantumCircuit]],
-        output_name: str = None,
-        callback: Callable = None,
-    ) -> Union[QuantumCircuit, List[QuantumCircuit]]:
+        circuits: _CircuitsT,
+        output_name: Optional[str] = None,
+        callback: Optional[Callable] = None,
+    ) -> _CircuitsT:
         self._update_passmanager()
         return super().run(circuits, output_name, callback)
 


Test patch:
diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py
--- a/test/python/compiler/test_transpiler.py
+++ b/test/python/compiler/test_transpiler.py
@@ -384,10 +384,27 @@ def test_transpile_bell(self):
         circuits = transpile(qc, backend)
         self.assertIsInstance(circuits, QuantumCircuit)
 
+    def test_transpile_one(self):
+        """Test transpile a single circuit.
+
+        Check that the top-level `transpile` function returns
+        a single circuit."""
+        backend = BasicAer.get_backend("qasm_simulator")
+
+        qubit_reg = QuantumRegister(2)
+        clbit_reg = ClassicalRegister(2)
+        qc = QuantumCircuit(qubit_reg, clbit_reg, name="bell")
+        qc.h(qubit_reg[0])
+        qc.cx(qubit_reg[0], qubit_reg[1])
+        qc.measure(qubit_reg, clbit_reg)
+
+        circuit = transpile(qc, backend)
+        self.assertIsInstance(circuit, QuantumCircuit)
+
     def test_transpile_two(self):
-        """Test transpile to circuits.
+        """Test transpile two circuits.
 
-        If all correct some should exists.
+        Check that the transpiler returns a list of two circuits.
         """
         backend = BasicAer.get_backend("qasm_simulator")
 
@@ -402,12 +419,19 @@ def test_transpile_two(self):
         qc_extra = QuantumCircuit(qubit_reg, qubit_reg2, clbit_reg, clbit_reg2, name="extra")
         qc_extra.measure(qubit_reg, clbit_reg)
         circuits = transpile([qc, qc_extra], backend)
-        self.assertIsInstance(circuits[0], QuantumCircuit)
-        self.assertIsInstance(circuits[1], QuantumCircuit)
+        self.assertIsInstance(circuits, list)
+        self.assertEqual(len(circuits), 2)
+
+        for circuit in circuits:
+            self.assertIsInstance(circuit, QuantumCircuit)
 
     def test_transpile_singleton(self):
         """Test transpile a single-element list with a circuit.
-        See https://github.com/Qiskit/qiskit-terra/issues/5260"""
+
+        Check that `transpile` returns a single-element list.
+
+        See https://github.com/Qiskit/qiskit-terra/issues/5260
+        """
         backend = BasicAer.get_backend("qasm_simulator")
 
         qubit_reg = QuantumRegister(2)
@@ -418,6 +442,7 @@ def test_transpile_singleton(self):
         qc.measure(qubit_reg, clbit_reg)
 
         circuits = transpile([qc], backend)
+        self.assertIsInstance(circuits, list)
         self.assertEqual(len(circuits), 1)
         self.assertIsInstance(circuits[0], QuantumCircuit)
 
diff --git a/test/python/transpiler/test_passmanager_run.py b/test/python/transpiler/test_passmanager_run.py
--- a/test/python/transpiler/test_passmanager_run.py
+++ b/test/python/transpiler/test_passmanager_run.py
@@ -17,13 +17,46 @@
 from qiskit.transpiler.preset_passmanagers import level_1_pass_manager
 from qiskit.test import QiskitTestCase
 from qiskit.providers.fake_provider import FakeMelbourne
-from qiskit.transpiler import Layout
+from qiskit.transpiler import Layout, PassManager
 from qiskit.transpiler.passmanager_config import PassManagerConfig
 
 
 class TestPassManagerRun(QiskitTestCase):
     """Test default_pass_manager.run(circuit(s))."""
 
+    def test_bare_pass_manager_single(self):
+        """Test that PassManager.run(circuit) returns a single circuit."""
+        qc = QuantumCircuit(1)
+        pm = PassManager([])
+        new_qc = pm.run(qc)
+        self.assertIsInstance(new_qc, QuantumCircuit)
+        self.assertEqual(qc, new_qc)  # pm has no passes
+
+    def test_bare_pass_manager_single_list(self):
+        """Test that PassManager.run([circuit]) returns a list with a single circuit."""
+        qc = QuantumCircuit(1)
+        pm = PassManager([])
+        result = pm.run([qc])
+        self.assertIsInstance(result, list)
+        self.assertEqual(len(result), 1)
+        self.assertIsInstance(result[0], QuantumCircuit)
+        self.assertEqual(result[0], qc)  # pm has no passes
+
+    def test_bare_pass_manager_multiple(self):
+        """Test that PassManager.run(circuits) returns a list of circuits."""
+        qc0 = QuantumCircuit(1)
+        qc1 = QuantumCircuit(2)
+
+        pm = PassManager([])
+        result = pm.run([qc0, qc1])
+
+        self.assertIsInstance(result, list)
+        self.assertEqual(len(result), 2)
+
+        for qc, new_qc in zip([qc0, qc1], result):
+            self.assertIsInstance(new_qc, QuantumCircuit)
+            self.assertEqual(new_qc, qc)  # pm has no passes
+
     def test_default_pass_manager_single(self):
         """Test default_pass_manager.run(circuit).
 
@@ -61,6 +94,7 @@ def test_default_pass_manager_single(self):
             )
         )
         new_circuit = pass_manager.run(circuit)
+        self.assertIsInstance(new_circuit, QuantumCircuit)
 
         bit_indices = {bit: idx for idx, bit in enumerate(new_circuit.qregs[0])}
 
@@ -110,8 +144,11 @@ def test_default_pass_manager_two(self):
             )
         )
         new_circuits = pass_manager.run([circuit1, circuit2])
+        self.assertIsInstance(new_circuits, list)
+        self.assertEqual(len(new_circuits), 2)
 
         for new_circuit in new_circuits:
+            self.assertIsInstance(new_circuit, QuantumCircuit)
             bit_indices = {bit: idx for idx, bit in enumerate(new_circuit.qregs[0])}
 
             for instruction in new_circuit.data: