From my experience, most of the time you can restructure your code to avoid the need for inserting gates in the middle of a circuit.
That said, if you already know the insertion points at the time of circuit creation but you don't know the gates to be inserted, you can add placeholders at these places and replace them later with whatever gates you want.
from qiskit import QuantumCircuit
from qiskit.circuit import Instruction
from typing import Union
class Placeholder(Instruction):
def __init__(self, num_qubits, label):
self.name = "placeholder"
super().__init__(self.name, num_qubits, 0, [], label = label)
def inverse(self):
return Placeholder(self.name, self.num_qubits)
# This function accepts a QuantumCircuit in addition to single instructions
# Note: you can modify it to accept a dictionary of label-instruction pairs.
def replace(self, placeholder_label, instruction: Union[Instruction, QuantumCircuit]):
if isinstance(instruction, QuantumCircuit):
instruction = instruction.to_gate()
self._data = [(instruction, _inst[1], _inst[2]) if _inst[0].name == 'placeholder' and _inst[0].label == placeholder_label else _inst for _inst in self._data]
QuantumCircuit.replace = replace
As an example, let's create a new circuit with some placeholders
circ = QuantumCircuit(2)
circ.h(0)
circ.x(1)
circ.cx(0, 1)
circ.append(Placeholder(2, 'First'), [0, 1])
circ.cx(1, 0)
circ.append(Placeholder(1, 'Second'), [0])
circ.measure_all()
circ.draw('mpl')

Now, let's replace these placeholders with actual gates:
from qiskit.circuit.library import HGate, SwapGate
circ.replace('First', SwapGate())
circ.replace('Second', HGate())
circ.draw('mpl')
The final circuit should look like
