diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs
new file mode 100644
index 000000000000..86df610a7547
--- /dev/null
+++ b/crates/circuit/src/bit.rs
@@ -0,0 +1,77 @@
+// This code is part of Qiskit.
+//
+// (C) Copyright IBM 2025
+//
+// This code is licensed under the Apache License, Version 2.0. You may
+// obtain a copy of this license in the LICENSE.txt file in the root directory
+// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+//
+// Any modifications or derivative works of this code must retain this
+// copyright notice, and modified files need to carry a notice indicating
+// that they have been altered from the originals.
+
+use std::fmt::Debug;
+
+/// Keeps information about where a bit is located within the circuit.
+///
+/// This information includes whether the bit was added by a register,
+/// which register it belongs to and where it is located within it.
+#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
+pub struct BitInfo {
+    added_by_reg: bool,
+    registers: Vec<BitLocation>,
+}
+
+impl BitInfo {
+    pub fn new(orig_reg: Option<(u32, u32)>) -> Self {
+        // If the instance was added by a register, add it and prefil its locator
+        if let Some((reg_idx, idx)) = orig_reg {
+            Self {
+                added_by_reg: true,
+                registers: vec![BitLocation::new(reg_idx, idx)],
+            }
+        } else {
+            Self {
+                added_by_reg: false,
+                registers: vec![],
+            }
+        }
+    }
+
+    /// Add a register to the bit instance
+    pub fn add_register(&mut self, register: u32, index: u32) {
+        self.registers.push(BitLocation(register, index))
+    }
+
+    /// Returns a list with all the [BitLocation] instances
+    pub fn get_registers(&self) -> &[BitLocation] {
+        &self.registers
+    }
+
+    /// Returns the index of the original register if any exists
+    pub fn orig_register_index(&self) -> Option<&BitLocation> {
+        if self.added_by_reg {
+            Some(&self.registers[0])
+        } else {
+            None
+        }
+    }
+}
+
+/// Keeps information about where a qubit is located within a register.
+#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Hash)]
+pub struct BitLocation(u32, u32);
+
+impl BitLocation {
+    pub fn new(register_idx: u32, index: u32) -> Self {
+        Self(register_idx, index)
+    }
+
+    pub fn register_index(&self) -> u32 {
+        self.0
+    }
+
+    pub fn index(&self) -> u32 {
+        self.1
+    }
+}
diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs
index bf9667e44214..e21d6049adc6 100644
--- a/crates/circuit/src/bit_data.rs
+++ b/crates/circuit/src/bit_data.rs
@@ -10,13 +10,19 @@
 // copyright notice, and modified files need to carry a notice indicating
 // that they have been altered from the originals.
 
-use crate::BitType;
+use crate::bit::{BitInfo, BitLocation};
+use crate::circuit_data::CircuitError;
+use crate::imports::{CLASSICAL_REGISTER, QUANTUM_REGISTER, REGISTER};
+use crate::register::{Register, RegisterAsKey};
+use crate::{BitType, ToPyBit};
 use hashbrown::HashMap;
 use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError};
 use pyo3::prelude::*;
-use pyo3::types::PyList;
+use pyo3::types::{PyDict, PyList};
+use std::borrow::Cow;
 use std::fmt::Debug;
 use std::hash::{Hash, Hasher};
+use std::sync::{OnceLock, RwLock};
 
 /// Private wrapper for Python-side Bit instances that implements
 /// [Hash] and [Eq], allowing them to be used in Rust hash-based
@@ -29,7 +35,7 @@ use std::hash::{Hash, Hasher};
 /// it call `repr()` on both sides, which has a significant
 /// performance advantage.
 #[derive(Clone, Debug)]
-struct BitAsKey {
+pub(crate) struct BitAsKey {
     /// Python's `hash()` of the wrapped instance.
     hash: isize,
     /// The wrapped instance.
@@ -230,3 +236,712 @@ where
         self.bits.clear();
     }
 }
+
+#[derive(Debug)]
+pub struct NewBitData<T: From<BitType>, R: Register + Hash + Eq> {
+    /// The public field name (i.e. `qubits` or `clbits`).
+    description: String,
+    /// Registered Python bits.
+    bits: Vec<OnceLock<PyObject>>,
+    /// Maps Python bits to native type, should be modifiable upon
+    /// retrieval.
+    indices: RwLock<HashMap<BitAsKey, T>>,
+    /// Maps Register keys to indices
+    reg_keys: HashMap<RegisterAsKey, u32>,
+    /// Mapping between bit index and its register info
+    bit_info: Vec<BitInfo>,
+    /// Registers in the circuit
+    registry: Vec<R>,
+    /// Registers in Python
+    registers: Vec<OnceLock<PyObject>>,
+    /// Cached Python bits
+    cached_py_bits: OnceLock<Py<PyList>>,
+    /// Cached Python registers
+    cached_py_regs: OnceLock<Py<PyList>>,
+}
+
+impl<T, R> NewBitData<T, R>
+where
+    T: From<BitType> + Copy + Debug + ToPyBit,
+    R: Register<Bit = T>
+        + Hash
+        + Eq
+        + From<(usize, Option<String>)>
+        + for<'a> From<Cow<'a, [T]>>
+        + for<'a> From<(Cow<'a, [T]>, Option<String>)>,
+    BitType: From<T>,
+{
+    pub fn new(description: String) -> Self {
+        NewBitData {
+            description,
+            bits: Vec::new(),
+            indices: HashMap::new().into(),
+            bit_info: Vec::new(),
+            registry: Vec::new(),
+            registers: Vec::new(),
+            cached_py_bits: OnceLock::new(),
+            cached_py_regs: OnceLock::new(),
+            reg_keys: HashMap::new(),
+        }
+    }
+
+    pub fn with_capacity(description: String, bit_capacity: usize, reg_capacity: usize) -> Self {
+        NewBitData {
+            description,
+            bits: Vec::with_capacity(bit_capacity),
+            indices: HashMap::with_capacity(bit_capacity).into(),
+            bit_info: Vec::with_capacity(bit_capacity),
+            registry: Vec::with_capacity(reg_capacity),
+            registers: Vec::with_capacity(reg_capacity),
+            cached_py_bits: OnceLock::new(),
+            cached_py_regs: OnceLock::new(),
+            reg_keys: HashMap::with_capacity(reg_capacity),
+        }
+    }
+
+    /// Gets the number of bits.
+    pub fn len(&self) -> usize {
+        self.bits.len()
+    }
+
+    /// Gets the number of registers.
+    pub fn len_regs(&self) -> usize {
+        self.registry.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.bits.is_empty()
+    }
+
+    /// Adds a register onto the [BitData] of the circuit.
+    ///
+    /// _**Note:** If providing the ``bits`` argument, the bits must already exist in the circuit._
+    pub fn add_register(
+        &mut self,
+        name: Option<String>,
+        size: Option<usize>,
+        bits: Option<Cow<'_, [T]>>,
+    ) -> Option<u32> {
+        let idx = self.registry.len().try_into().unwrap_or_else(|_| {
+            panic!(
+                "The {} registry in this circuit has reached its maximum capacity.",
+                self.description
+            )
+        });
+        match (size, bits) {
+            (None, None) => panic!("You should at least provide either a size or the bit indices."),
+            (None, Some(bits)) => {
+                if bits.is_empty() {
+                    return None;
+                }
+                let reg: R = (bits, name).into();
+                if self.reg_keys.contains_key(reg.as_key()) {
+                    return Some(self.reg_keys[reg.as_key()]);
+                }
+                // Add register info cancel if any qubit is duplicated
+                for (bit_idx, bit) in reg.bits().enumerate() {
+                    let bit_info = &mut self.bit_info[BitType::from(bit) as usize];
+                    bit_info.add_register(
+                        idx,
+                        bit_idx.try_into().unwrap_or_else(|_| {
+                            panic!(
+                                "The current register exceeds its capacity limit. Number of {} : {}",
+                                self.description,
+                                reg.len()
+                            )
+                        }),
+                    );
+                }
+                self.reg_keys.insert(reg.as_key().clone(), idx);
+                self.registry.push(reg);
+                self.registers.push(OnceLock::new());
+                Some(idx)
+            }
+            (Some(size), None) => {
+                if size < 1 {
+                    return None;
+                }
+                // Check if the register already exists
+                let reg: R = (size, name).into();
+                if let Some(index) = self.reg_keys.get(reg.as_key()) {
+                    return Some(*index);
+                }
+                let bits: Vec<T> = (0..size)
+                    .map(|bit| {
+                        self.add_bit_inner(Some((
+                            idx,
+                            bit.try_into().unwrap_or_else(|_| {
+                                panic!(
+                                    "The current register exceeds its capacity limit. Number of {} : {}",
+                                    self.description,
+                                    size
+                                )
+                            }),
+                        )))
+                    })
+                    .collect();
+                let reg: R = (Cow::from(bits), Some(reg.name().to_string())).into();
+                let idx = self.registry.len().try_into().unwrap_or_else(|_| {
+                    panic!(
+                        "The {} registry in this circuit has reached its maximum capacity.",
+                        self.description
+                    )
+                });
+                self.reg_keys.insert(reg.as_key().clone(), idx);
+                self.registry.push(reg);
+                self.registers.push(OnceLock::new());
+                Some(idx)
+            }
+            (Some(_), Some(_)) => {
+                panic!("You should only provide either a size or the bit indices, not both.")
+            }
+        }
+    }
+
+    /// Adds a bit index into the circuit's [BitData].
+    ///
+    /// _**Note:** You cannot add bits to registers once they are added._
+    pub fn add_bit(&mut self) -> T {
+        self.add_bit_inner(None)
+    }
+
+    fn add_bit_inner(&mut self, reg: Option<(u32, u32)>) -> T {
+        let idx: BitType = self.bits.len().try_into().unwrap_or_else(|_| {
+            panic!(
+                "The number of {} in the circuit has exceeded the maximum capacity",
+                self.description
+            )
+        });
+        self.bit_info.push(BitInfo::new(reg));
+        self.bits.push(OnceLock::new());
+        idx.into()
+    }
+
+    /// Retrieves the register info of a bit. Will panic if the index is out of range.
+    pub fn get_bit_info(&self, index: T) -> &[BitLocation] {
+        self.bit_info[BitType::from(index) as usize].get_registers()
+    }
+
+    /// Retrieves a register by its index within the circuit
+    #[inline]
+    pub fn get_register(&self, index: u32) -> Option<&R> {
+        self.registry.get(index as usize)
+    }
+
+    /// Returns a slice of the registers in the circuit
+    pub fn registers(&self) -> &[R] {
+        &self.registry
+    }
+
+    #[inline]
+    pub fn get_register_by_key(&self, key: &RegisterAsKey) -> Option<&R> {
+        self.reg_keys
+            .get(key)
+            .and_then(|idx| self.get_register(*idx))
+    }
+
+    /// Checks if a register is in the circuit
+    #[inline]
+    pub fn contains_register(&self, reg: &R) -> bool {
+        self.contains_register_by_key(reg.as_key())
+    }
+
+    #[inline]
+    pub fn contains_register_by_key(&self, reg: &RegisterAsKey) -> bool {
+        self.reg_keys.contains_key(reg)
+    }
+}
+
+// PyMethods
+impl<T, R> NewBitData<T, R>
+where
+    T: From<BitType> + Copy + Debug + ToPyBit,
+    R: Register<Bit = T>
+        + Hash
+        + Eq
+        + From<(usize, Option<String>)>
+        + for<'a> From<Cow<'a, [T]>>
+        + for<'a> From<(Cow<'a, [T]>, Option<String>)>,
+    BitType: From<T>,
+{
+    /// Finds the native bit index of the given Python bit.
+    #[inline]
+    pub fn py_find_bit(&self, bit: &Bound<PyAny>) -> PyResult<Option<T>> {
+        self.indices
+            .try_read()
+            .map(|op| op.get(&BitAsKey::new(bit)).copied())
+            .map_err(|_| {
+                PyRuntimeError::new_err(format!(
+                    "Could not map {}. Error accessing index mapping.",
+                    &bit
+                ))
+            })
+    }
+
+    /// Gets a reference to the cached Python list, with the bits maintained by
+    /// this instance.
+    #[inline]
+    pub fn py_cached_bits(&self, py: Python) -> PyResult<&Py<PyList>> {
+        let res = self.cached_py_bits.get_or_init(|| {
+            PyList::new(
+                py,
+                (0..self.len()).map(|idx| self.py_get_bit(py, (idx as u32).into()).unwrap()),
+            )
+            .unwrap()
+            .into()
+        });
+
+        // If the length is different from the stored bits, append to cache
+        // Indices are guaranteed to follow
+        let res_as_bound = res.bind(py);
+        if res_as_bound.len() < self.len() {
+            let current_length = res_as_bound.len();
+            for index in current_length.checked_sub(1).unwrap_or_default()..self.len() {
+                res_as_bound.append(self.py_get_bit(py, (index as u32).into())?)?
+            }
+        }
+        Ok(res)
+    }
+
+    /// Gets a reference to the cached Python list, with the registers maintained by
+    /// this instance.
+    #[inline]
+    pub fn py_cached_regs(&self, py: Python) -> PyResult<&Py<PyList>> {
+        // Initialize the list with all the currently available registers
+        let res = self.cached_py_regs.get_or_init(|| {
+            PyList::new(
+                py,
+                (0..self.len_regs()).map(|idx| self.py_get_register(py, idx as u32).unwrap()),
+            )
+            .unwrap()
+            .into()
+        });
+
+        // If the length is different from the stored registers, rebuild cache
+        let res_as_bound = res.bind(py);
+        if res_as_bound.len() < self.len_regs() {
+            let current_length = res_as_bound.len();
+            for index in (current_length - 1)..self.len_regs() {
+                res_as_bound.append(self.py_get_register(py, index as u32)?)?
+            }
+        }
+        Ok(res)
+    }
+
+    /// Gets a reference to the underlying vector of Python bits.
+    #[inline]
+    pub fn py_bits(&self, py: Python) -> PyResult<Vec<&PyObject>> {
+        (0..self.len())
+            .map(|idx| {
+                self.py_get_bit(py, (idx as u32).into())
+                    .map(|bit| bit.unwrap())
+            })
+            .collect::<PyResult<_>>()
+    }
+
+    /// Gets the location of a bit within the circuit
+    pub fn py_get_bit_location(
+        &mut self,
+        bit: &Bound<PyAny>,
+    ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> {
+        let py = bit.py();
+        let index = self.py_find_bit(bit)?.ok_or(PyKeyError::new_err(format!(
+            "The provided {} is not part of this circuit",
+            self.description
+        )))?;
+        Ok((
+            index.into(),
+            self.get_bit_info(index)
+                .iter()
+                .map(|info| -> PyResult<(&PyObject, u32)> {
+                    Ok((
+                        self.py_get_register(py, info.register_index())?.unwrap(),
+                        info.index(),
+                    ))
+                })
+                .collect::<PyResult<Vec<_>>>()?,
+        ))
+    }
+
+    /// Gets a reference to the underlying vector of Python registers.
+    #[inline]
+    pub fn py_registers(&self, py: Python) -> PyResult<Vec<&PyObject>> {
+        (0..self.len_regs() as u32)
+            .map(|idx| self.py_get_register(py, idx).map(|reg| reg.unwrap()))
+            .collect::<PyResult<_>>()
+    }
+
+    /// Map the provided Python bits to their native indices.
+    /// An error is returned if any bit is not registered.
+    pub fn py_map_bits<'py>(
+        &mut self,
+        bits: impl IntoIterator<Item = Bound<'py, PyAny>>,
+    ) -> PyResult<impl Iterator<Item = T>> {
+        let v: Result<Vec<_>, _> = bits
+            .into_iter()
+            .map(|b| {
+                self.py_find_bit(&b)?.ok_or_else(|| {
+                    PyKeyError::new_err(format!("Bit {:?} has not been added to this circuit.", b))
+                })
+            })
+            .collect();
+        v.map(|x| x.into_iter())
+    }
+
+    /// Map the provided native indices to the corresponding Python
+    /// bit instances.
+    /// Panics if any of the indices are out of range.
+    pub fn py_map_indices(
+        &self,
+        py: Python,
+        bits: &[T],
+    ) -> PyResult<impl ExactSizeIterator<Item = &Py<PyAny>>> {
+        let v: Vec<_> = bits
+            .iter()
+            .map(|i| -> PyResult<&PyObject> { Ok(self.py_get_bit(py, *i)?.unwrap()) })
+            .collect::<PyResult<_>>()?;
+        Ok(v.into_iter())
+    }
+
+    /// Gets the Python bit corresponding to the given native
+    /// bit index.
+    #[inline]
+    pub fn py_get_bit(&self, py: Python, index: T) -> PyResult<Option<&PyObject>> {
+        let index_as_usize = BitType::from(index) as usize;
+        // First check if the cell is in range if not, return none
+        if self.bits.get(index_as_usize).is_none() {
+            Ok(None)
+        }
+        // If the bit has an assigned register, check if it has been initialized.
+        else if let Some(bit_info) = self.bit_info[index_as_usize].orig_register_index() {
+            // If it is not initalized and has a register, initialize the original register
+            // and retrieve it from there the first time
+            if self.bits[index_as_usize].get().is_none() {
+                // A register index is guaranteed to exist in the instance of `BitData`.
+                let py_reg = self.py_get_register(py, bit_info.register_index())?;
+                let res = py_reg.unwrap().bind(py).get_item(bit_info.index())?;
+                self.indices
+                    .try_write()
+                    .map(|mut indices| indices.insert(BitAsKey::new(&res), index))
+                    .map_err(|err| PyRuntimeError::new_err(format!("{:?}", err)))?;
+                self.bits[index_as_usize].set(res.into()).map_err(|_| {
+                    PyRuntimeError::new_err(format!(
+                        "Error while initializing Python bit at index {} in these circuit's {}",
+                        BitType::from(index),
+                        &self.description
+                    ))
+                })?;
+            }
+            // If it is initialized, just retrieve.
+            Ok(self.bits[index_as_usize].get())
+        } else if let Some(bit) = self.bits[index_as_usize].get() {
+            Ok(Some(bit))
+        } else {
+            let new_bit = T::to_py_bit(py)?;
+            // Try and write the bit index into `BitData`.
+            self.indices
+                .try_write()
+                .map(|mut indices| indices.insert(BitAsKey::new(new_bit.bind(py)), index))
+                .map_err(|err| PyRuntimeError::new_err(format!("{:?}", err)))?;
+            self.bits[index_as_usize].set(new_bit).map_err(|_| {
+                PyRuntimeError::new_err(format!(
+                    "Error while initializing Python bit at index {} in these circuit's {}",
+                    BitType::from(index),
+                    &self.description
+                ))
+            })?;
+            Ok(self.bits[index_as_usize].get())
+        }
+    }
+
+    /// Retrieves a register instance from Python based on the rust description.
+    pub fn py_get_register(&self, py: Python, index: u32) -> PyResult<Option<&PyObject>> {
+        let index_as_usize = index as usize;
+        // First check if the cell is in range if not, return none
+        if self.registers.get(index_as_usize).is_none() {
+            Ok(None)
+        } else if self.registers[index_as_usize].get().is_none() {
+            let register = &self.registry[index as usize];
+            // Decide the register type based on its key
+            let reg_as_key = register.as_key();
+            let reg_type = match reg_as_key {
+                RegisterAsKey::Register(_) => REGISTER.get_bound(py),
+                RegisterAsKey::Quantum(_) => QUANTUM_REGISTER.get_bound(py),
+                RegisterAsKey::Classical(_) => CLASSICAL_REGISTER.get_bound(py),
+            };
+            // Check if all indices have been initialized from this register, if such is the case
+            // Treat the rest of indices as old `Bits``
+            if register.bits().all(|bit| {
+                self.bit_info[BitType::from(bit) as usize]
+                    .orig_register_index()
+                    .is_some_and(|idx| idx.register_index() == index)
+            }) {
+                let reg = reg_type.call1((register.len(), register.name()))?;
+                self.registers[index_as_usize]
+                    .set(reg.into())
+                    .map_err(|_| PyRuntimeError::new_err("Could not set the OnceCell correctly"))?;
+                Ok(self.registers[index_as_usize].get())
+            } else {
+                let bits: Vec<PyObject> = register
+                    .bits()
+                    .map(|bit| -> PyResult<PyObject> {
+                        if let Some(bit_obj) = self.bits[BitType::from(bit) as usize].get() {
+                            Ok(bit_obj.clone_ref(py))
+                        } else {
+                            T::to_py_bit(py)
+                        }
+                    })
+                    .collect::<PyResult<_>>()?;
+
+                // Extract kwargs
+                let kwargs = PyDict::new(py);
+                kwargs.set_item("name", register.name())?;
+                kwargs.set_item("bits", bits)?;
+
+                // Create register and assign to OnceCell
+                let reg = reg_type.call((), Some(&kwargs))?;
+                self.registers[index_as_usize]
+                    .set(reg.into())
+                    .map_err(|_| PyRuntimeError::new_err("Could not set the OnceCell correctly"))?;
+                Ok(self.registers[index_as_usize].get())
+            }
+        } else {
+            Ok(self.registers[index_as_usize].get())
+        }
+    }
+
+    /// Adds a new Python bit.
+    ///
+    /// _**Note:** If this Bit has register information, it will not be reflected unless
+    /// the Register is also added._
+    pub fn py_add_bit(&mut self, bit: &Bound<PyAny>, strict: bool) -> PyResult<T> {
+        let py: Python<'_> = bit.py();
+
+        if self.bits.len() != self.py_cached_bits(py)?.bind(bit.py()).len() {
+            return Err(PyRuntimeError::new_err(
+            format!("This circuit's {} list has become out of sync with the circuit data. Did something modify it?", self.description)
+            ));
+        }
+
+        let idx: BitType = self.bits.len().try_into().map_err(|_| {
+            PyRuntimeError::new_err(format!(
+                "The number of {} in the circuit has exceeded the maximum capacity",
+                self.description
+            ))
+        })?;
+        if self
+            .indices
+            .try_write()
+            .map(|mut res| res.try_insert(BitAsKey::new(bit), idx.into()).is_ok())
+            .is_ok_and(|res| res)
+        {
+            // Append to cache before bits to avoid rebuilding cache.
+            self.py_cached_bits(py)?.bind(py).append(bit)?;
+            self.bit_info.push(BitInfo::new(None));
+            self.bits.push(bit.clone().unbind().into());
+            Ok(idx.into())
+        } else if strict {
+            return Err(PyValueError::new_err(format!(
+                "Existing bit {:?} cannot be re-added in strict mode.",
+                bit
+            )));
+        } else {
+            return self.py_find_bit(bit).map(|opt| opt.unwrap());
+        }
+    }
+
+    /// Adds new register from Python.
+    pub fn py_add_register(&mut self, register: &Bound<PyAny>) -> PyResult<u32> {
+        let py = register.py();
+        if self.registers.len() != self.py_cached_regs(py)?.bind(py).len() {
+            return Err(PyRuntimeError::new_err(
+            format!("This circuit's {} register list has become out of sync with the circuit data. Did something modify it?", self.description)
+            ));
+        }
+        let key: RegisterAsKey = register.extract()?;
+        if self.reg_keys.contains_key(&key) {
+            return Err(CircuitError::new_err(format!(
+                "A {} register of name {} already exists in the circuit",
+                &self.description,
+                key.name()
+            )));
+        }
+
+        let idx: u32 = self.registers.len().try_into().map_err(|_| {
+            PyRuntimeError::new_err(format!(
+                "The number of {} registers in the circuit has exceeded the maximum capacity",
+                self.description
+            ))
+        })?;
+
+        let bits: Vec<T> = register
+            .try_iter()?
+            .enumerate()
+            .map(|(bit_index, bit)| -> PyResult<T> {
+                let bit_index: u32 = bit_index.try_into().map_err(|_| {
+                    CircuitError::new_err(format!(
+                        "The current register exceeds its capacity limit. Number of {} : {}",
+                        self.description,
+                        key.size()
+                    ))
+                })?;
+                let bit = bit?;
+                let index = if let Some(index) = self.py_find_bit(&bit)? {
+                    let bit_info = &mut self.bit_info[BitType::from(index) as usize];
+                    bit_info.add_register(idx, bit_index);
+                    index
+                } else {
+                    let index = self.py_add_bit(&bit, true)?;
+                    self.bit_info[BitType::from(index) as usize] =
+                        BitInfo::new(Some((idx, bit_index)));
+                    index
+                };
+                Ok(index)
+            })
+            .collect::<PyResult<_>>()?;
+
+        // Create the native register
+        let name: String = key.name().to_string();
+        let reg: R = (Cow::from(bits), Some(name)).into();
+
+        // Append to cache before registers to avoid rebuilding cache.
+        self.py_cached_regs(py)?.bind(py).append(register)?;
+        self.reg_keys.insert(reg.as_key().clone(), idx);
+        self.registry.push(reg);
+        self.registers.push(register.clone().unbind().into());
+        Ok(idx)
+    }
+
+    /// Works as a setter for Python registers when the circuit needs to discard old data.
+    /// This method discards the current registers and the data associated with them from its
+    /// respective bits.
+    pub fn py_set_registers(&mut self, other: &Bound<PyList>) -> PyResult<()> {
+        // First invalidate everything related to registers
+        // This is done to ensure we regenerate the lost information
+        // Clear all information bits may have on registers
+        self.bit_info = (0..self.len()).map(|_| BitInfo::new(None)).collect();
+
+        self.reg_keys.clear();
+        self.registers.clear();
+        self.registry.clear();
+        self.cached_py_regs.take();
+
+        // Re-assign
+        for reg in other.iter() {
+            self.py_add_register(&reg)?;
+        }
+
+        Ok(())
+    }
+
+    pub fn py_remove_bit_indices<I>(&mut self, py: Python, indices: I) -> PyResult<()>
+    where
+        I: IntoIterator<Item = T>,
+    {
+        let mut indices_sorted: Vec<usize> = indices
+            .into_iter()
+            .map(|i| <BitType as From<T>>::from(i) as usize)
+            .collect();
+        indices_sorted.sort();
+
+        for index in indices_sorted.into_iter().rev() {
+            self.cached_py_bits.take();
+            let bit = self.py_get_bit(py, (index as BitType).into())?.unwrap();
+            self.indices
+                .try_write()
+                .map(|mut op| op.remove(&BitAsKey::new(bit.bind(py))))
+                .map_err(|_| {
+                    PyRuntimeError::new_err("Could not remove bit from cache".to_string())
+                })?;
+            self.bits.remove(index);
+            self.bit_info.remove(index);
+        }
+        // Update indices.
+        for i in 0..self.bits.len() {
+            let bit = self.py_get_bit(py, (i as BitType).into())?.unwrap();
+            self.indices
+                .try_write()
+                .map(|mut op| op.insert(BitAsKey::new(bit.bind(py)), (i as BitType).into()))
+                .map_err(|_| {
+                    PyRuntimeError::new_err("Could not re-map bit in cache".to_string())
+                })?;
+        }
+        Ok(())
+    }
+
+    pub(crate) fn py_bits_raw(&self) -> &[OnceLock<PyObject>] {
+        &self.bits
+    }
+
+    pub(crate) fn py_bits_cached_raw(&self) -> Option<&Py<PyList>> {
+        self.cached_py_bits.get()
+    }
+
+    pub(crate) fn py_regs_raw(&self) -> &[OnceLock<PyObject>] {
+        &self.bits
+    }
+
+    pub(crate) fn py_regs_cached_raw(&self) -> Option<&Py<PyList>> {
+        self.cached_py_bits.get()
+    }
+
+    /// Called during Python garbage collection, only!.
+    /// Note: INVALIDATES THIS INSTANCE.
+    pub fn dispose(&mut self) -> PyResult<()> {
+        self.indices
+            .try_write()
+            .map(|mut op| op.clear())
+            .map_err(|err| PyRuntimeError::new_err(format!("{:?}", err)))?;
+        self.bits.clear();
+        self.registers.clear();
+        self.bit_info.clear();
+        self.registry.clear();
+        Ok(())
+    }
+
+    /// To convert [BitData] into [NewBitData]. If the structure the original comes from contains register
+    /// info. Make sure to add it manually after.
+    pub fn from_bit_data(py: Python, bit_data: &BitData<T>) -> Self {
+        Self {
+            description: bit_data.description.clone(),
+            bits: bit_data
+                .bits
+                .iter()
+                .map(|bit| bit.clone_ref(py).into())
+                .collect(),
+            indices: bit_data.indices.clone().into(),
+            reg_keys: HashMap::new(),
+            bit_info: (0..bit_data.len()).map(|_| BitInfo::new(None)).collect(),
+            registry: Vec::new(),
+            registers: Vec::new(),
+            cached_py_bits: OnceLock::new(),
+            cached_py_regs: OnceLock::new(),
+        }
+    }
+}
+
+// Custom implementation of Clone due to RWLock usage.
+impl<T: Clone, R: Clone> Clone for NewBitData<T, R>
+where
+    T: From<u32> + Copy,
+    R: Register + Hash + Eq,
+{
+    fn clone(&self) -> Self {
+        Self {
+            description: self.description.clone(),
+            bits: self.bits.clone(),
+            indices: self
+                .indices
+                .try_read()
+                .map(|indices| indices.clone())
+                .unwrap_or_default()
+                .into(),
+            reg_keys: self.reg_keys.clone(),
+            bit_info: self.bit_info.clone(),
+            registry: self.registry.clone(),
+            registers: self.registers.clone(),
+            cached_py_bits: self.cached_py_bits.clone(),
+            cached_py_regs: self.cached_py_regs.clone(),
+        }
+    }
+}
diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs
index be36aed73f6b..ce470364382e 100644
--- a/crates/circuit/src/circuit_data.rs
+++ b/crates/circuit/src/circuit_data.rs
@@ -10,19 +10,22 @@
 // copyright notice, and modified files need to carry a notice indicating
 // that they have been altered from the originals.
 
+use std::borrow::Cow;
 #[cfg(feature = "cache_pygates")]
 use std::sync::OnceLock;
 
-use crate::bit_data::BitData;
+use crate::bit::BitLocation;
+use crate::bit_data::NewBitData;
 use crate::circuit_instruction::{
     CircuitInstruction, ExtraInstructionAttributes, OperationFromPython,
 };
 use crate::dag_circuit::add_global_phase;
-use crate::imports::{ANNOTATED_OPERATION, CLBIT, QUANTUM_CIRCUIT, QUBIT};
+use crate::imports::{ANNOTATED_OPERATION, QUANTUM_CIRCUIT};
 use crate::interner::{Interned, Interner};
 use crate::operations::{Operation, OperationRef, Param, StandardGate};
 use crate::packed_instruction::{PackedInstruction, PackedOperation};
 use crate::parameter_table::{ParameterTable, ParameterTableError, ParameterUse, ParameterUuid};
+use crate::register::{ClassicalRegister, QuantumRegister};
 use crate::slice::{PySequenceIndex, SequenceIndex};
 use crate::{Clbit, Qubit};
 
@@ -102,9 +105,9 @@ pub struct CircuitData {
     /// The cache used to intern instruction bits.
     cargs_interner: Interner<[Clbit]>,
     /// Qubits registered in the circuit.
-    qubits: BitData<Qubit>,
+    qubits: NewBitData<Qubit, QuantumRegister>,
     /// Clbits registered in the circuit.
-    clbits: BitData<Clbit>,
+    clbits: NewBitData<Clbit, ClassicalRegister>,
     param_table: ParameterTable,
     #[pyo3(get)]
     global_phase: Param,
@@ -114,7 +117,7 @@ pub struct CircuitData {
 impl CircuitData {
     #[new]
     #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0, global_phase=Param::Float(0.0)))]
-    pub fn new(
+    pub fn py_new(
         py: Python<'_>,
         qubits: Option<&Bound<PyAny>>,
         clbits: Option<&Bound<PyAny>>,
@@ -126,20 +129,20 @@ impl CircuitData {
             data: Vec::new(),
             qargs_interner: Interner::new(),
             cargs_interner: Interner::new(),
-            qubits: BitData::new(py, "qubits".to_string()),
-            clbits: BitData::new(py, "clbits".to_string()),
+            qubits: NewBitData::new("qubits".to_string()),
+            clbits: NewBitData::new("clbits".to_string()),
             param_table: ParameterTable::new(),
             global_phase: Param::Float(0.),
         };
         self_.set_global_phase(py, global_phase)?;
         if let Some(qubits) = qubits {
             for bit in qubits.try_iter()? {
-                self_.add_qubit(py, &bit?, true)?;
+                self_.py_add_qubit(&bit?, true)?;
             }
         }
         if let Some(clbits) = clbits {
             for bit in clbits.try_iter()? {
-                self_.add_clbit(py, &bit?, true)?;
+                self_.py_add_clbit(&bit?, true)?;
             }
         }
         if let Some(data) = data {
@@ -149,19 +152,70 @@ impl CircuitData {
         Ok(self_)
     }
 
-    pub fn __reduce__(self_: &Bound<CircuitData>, py: Python<'_>) -> PyResult<PyObject> {
+    pub fn __reduce__(self_: &Bound<CircuitData>) -> PyResult<PyObject> {
+        let py = self_.py();
         let ty: Bound<PyType> = self_.get_type();
         let args = {
             let self_ = self_.borrow();
             (
-                self_.qubits.cached().clone_ref(py),
-                self_.clbits.cached().clone_ref(py),
+                self_.qubits.py_cached_bits(py)?.clone_ref(py),
+                self_.clbits.py_cached_bits(py)?.clone_ref(py),
                 None::<()>,
                 self_.data.len(),
                 self_.global_phase.clone(),
             )
         };
-        (ty, args, None::<()>, self_.try_iter()?).into_py_any(py)
+        let state = {
+            let borrowed = self_.borrow();
+            (
+                borrowed.qubits.py_cached_regs(py)?.clone_ref(py),
+                borrowed.clbits.py_cached_regs(py)?.clone_ref(py),
+            )
+        };
+        (ty, args, state, self_.try_iter()?).into_py_any(py)
+    }
+
+    pub fn __setstate__(
+        self_: &Bound<CircuitData>,
+        state: (Bound<PyList>, Bound<PyList>),
+    ) -> PyResult<()> {
+        let mut borrowed_mut = self_.borrow_mut();
+        for qreg in state.0.iter() {
+            borrowed_mut.py_add_qreg(&qreg)?;
+        }
+        for creg in state.1.iter() {
+            borrowed_mut.py_add_creg(&creg)?;
+        }
+        Ok(())
+    }
+
+    /// Returns the a list of registered :class:`.QuantumRegisters` instances.
+    ///
+    /// .. warning::
+    ///
+    ///     Do not modify this list yourself.  It will invalidate the :class:`CircuitData` data
+    ///     structures.
+    ///
+    /// Returns:
+    ///     dict(:class:`.QuantumRegister`): The current sequence of registered qubits.
+    #[getter("qregs")]
+    pub fn py_qregs(&self, py: Python<'_>) -> PyResult<&Py<PyList>> {
+        self.qubits.py_cached_regs(py)
+    }
+
+    /// Setter for registers, in case of forced updates.
+    #[setter("qregs")]
+    pub fn py_qreg_set(&mut self, other: &Bound<PyList>) -> PyResult<()> {
+        self.qubits.py_set_registers(other)
+    }
+
+    /// Gets the location of the bit inside of the circuit
+    #[pyo3(name = "get_qubit_location")]
+    pub fn py_get_qubit_location(
+        &mut self,
+        bit: &Bound<PyAny>,
+    ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> {
+        self.qubits.py_get_bit_location(bit)
     }
 
     /// Returns the current sequence of registered :class:`.Qubit` instances as a list.
@@ -174,8 +228,8 @@ impl CircuitData {
     /// Returns:
     ///     list(:class:`.Qubit`): The current sequence of registered qubits.
     #[getter("qubits")]
-    pub fn py_qubits(&self, py: Python<'_>) -> Py<PyList> {
-        self.qubits.cached().clone_ref(py)
+    pub fn py_qubits(&self, py: Python<'_>) -> PyResult<&Py<PyList>> {
+        self.qubits.py_cached_bits(py)
     }
 
     /// Return the number of qubits. This is equivalent to the length of the list returned by
@@ -188,8 +242,36 @@ impl CircuitData {
         self.qubits.len()
     }
 
-    /// Returns the current sequence of registered :class:`.Clbit`
-    /// instances as a list.
+    /// Returns the a list of registered :class:`.ClassicalRegisters` instances.
+    ///
+    /// .. warning::
+    ///
+    ///     Do not modify this list yourself.  It will invalidate the :class:`CircuitData` data
+    ///     structures.
+    ///
+    /// Returns:
+    ///     dict(:class:`.ClassicalRegister`): The current sequence of registered qubits.
+    #[getter("cregs")]
+    pub fn py_cregs(&self, py: Python<'_>) -> PyResult<&Py<PyList>> {
+        self.clbits.py_cached_regs(py)
+    }
+
+    /// Setter for registers, in case of forced updates.
+    #[setter("cregs")]
+    pub fn py_creg_set(&mut self, other: &Bound<PyList>) -> PyResult<()> {
+        self.clbits.py_set_registers(other)
+    }
+
+    /// Gets the location of the bit inside of the circuit
+    #[pyo3(name = "get_clbit_location")]
+    pub fn py_get_clbit_location(
+        &mut self,
+        bit: &Bound<PyAny>,
+    ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> {
+        self.clbits.py_get_bit_location(bit)
+    }
+
+    /// Returns the current sequence of registered :class:`.Clbit` instances as a list.
     ///
     /// .. warning::
     ///
@@ -199,8 +281,8 @@ impl CircuitData {
     /// Returns:
     ///     list(:class:`.Clbit`): The current sequence of registered clbits.
     #[getter("clbits")]
-    pub fn py_clbits(&self, py: Python<'_>) -> Py<PyList> {
-        self.clbits.cached().clone_ref(py)
+    pub fn py_clbits(&self, py: Python<'_>) -> PyResult<&Py<PyList>> {
+        self.clbits.py_cached_bits(py)
     }
 
     /// Return the number of clbits. This is equivalent to the length of the list returned by
@@ -257,9 +339,19 @@ impl CircuitData {
     /// Raises:
     ///     ValueError: The specified ``bit`` is already present and flag ``strict``
     ///         was provided.
-    #[pyo3(signature = (bit, *, strict=true))]
-    pub fn add_qubit(&mut self, py: Python, bit: &Bound<PyAny>, strict: bool) -> PyResult<()> {
-        self.qubits.add(py, bit, strict)?;
+    #[pyo3(name="add_qubit", signature = (bit, *, strict=true))]
+    pub fn py_add_qubit(&mut self, bit: &Bound<PyAny>, strict: bool) -> PyResult<()> {
+        self.qubits.py_add_bit(bit, strict)?;
+        Ok(())
+    }
+
+    /// Registers a :class:`.QuantumRegister` instance.
+    ///
+    /// Args:
+    ///     bit (:class:`.QuantumRegister`): The register to add.
+    #[pyo3(name="add_qreg", signature = (register, *,))]
+    pub fn py_add_qreg(&mut self, register: &Bound<PyAny>) -> PyResult<()> {
+        self.qubits.py_add_register(register)?;
         Ok(())
     }
 
@@ -272,9 +364,19 @@ impl CircuitData {
     /// Raises:
     ///     ValueError: The specified ``bit`` is already present and flag ``strict``
     ///         was provided.
-    #[pyo3(signature = (bit, *, strict=true))]
-    pub fn add_clbit(&mut self, py: Python, bit: &Bound<PyAny>, strict: bool) -> PyResult<()> {
-        self.clbits.add(py, bit, strict)?;
+    #[pyo3(name="add_clbit", signature = (bit, *, strict=true))]
+    pub fn py_add_clbit(&mut self, bit: &Bound<PyAny>, strict: bool) -> PyResult<()> {
+        self.clbits.py_add_bit(bit, strict)?;
+        Ok(())
+    }
+
+    /// Registers a :class:`.ClassicalRegister` instance.
+    ///
+    /// Args:
+    ///     bit (:class:`.ClassicalRegister`): The register to add.
+    #[pyo3(name="add_creg", signature = (register, *,))]
+    pub fn py_add_creg(&mut self, register: &Bound<PyAny>) -> PyResult<()> {
+        self.clbits.py_add_register(register)?;
         Ok(())
     }
 
@@ -284,18 +386,8 @@ impl CircuitData {
     ///     CircuitData: The shallow copy.
     #[pyo3(signature = (copy_instructions=true, deepcopy=false))]
     pub fn copy(&self, py: Python<'_>, copy_instructions: bool, deepcopy: bool) -> PyResult<Self> {
-        let mut res = CircuitData::new(
-            py,
-            Some(self.qubits.cached().bind(py)),
-            Some(self.clbits.cached().bind(py)),
-            None,
-            self.data.len(),
-            self.global_phase.clone(),
-        )?;
-        res.qargs_interner = self.qargs_interner.clone();
-        res.cargs_interner = self.cargs_interner.clone();
+        let mut res = self.copy_empty_like(py, Some(self.data().len()))?;
         res.param_table.clone_from(&self.param_table);
-
         if deepcopy {
             let memo = PyDict::new(py);
             for inst in &self.data {
@@ -327,6 +419,32 @@ impl CircuitData {
         Ok(res)
     }
 
+    /// Performs an empty-like shallow copy.
+    ///
+    /// Returns:
+    ///     CircuitData: The shallow copy.
+    #[pyo3(signature = (reserve = None,))]
+    pub fn copy_empty_like(&self, py: Python<'_>, reserve: Option<usize>) -> PyResult<Self> {
+        let mut res = CircuitData::py_new(
+            py,
+            Some(self.qubits.py_cached_bits(py)?.bind(py)),
+            Some(self.clbits.py_cached_bits(py)?.bind(py)),
+            None,
+            reserve.unwrap_or_default(),
+            self.global_phase.clone(),
+        )?;
+        res.qargs_interner = self.qargs_interner.clone();
+        res.cargs_interner = self.cargs_interner.clone();
+
+        for qreg in self.py_qregs(py)?.bind(py).iter() {
+            res.py_add_qreg(&qreg)?;
+        }
+        for creg in self.py_cregs(py)?.bind(py).iter() {
+            res.py_add_creg(&creg)?;
+        }
+        Ok(res)
+    }
+
     /// Reserves capacity for at least ``additional`` more
     /// :class:`.CircuitInstruction` instances to be added to this container.
     ///
@@ -347,10 +465,10 @@ impl CircuitData {
         let clbits = PySet::empty(py)?;
         for inst in self.data.iter() {
             for b in self.qargs_interner.get(inst.qubits) {
-                qubits.add(self.qubits.get(*b).unwrap().clone_ref(py))?;
+                qubits.add(self.qubits.py_get_bit(py, *b)?.unwrap().clone_ref(py))?;
             }
             for b in self.cargs_interner.get(inst.clbits) {
-                clbits.add(self.clbits.get(*b).unwrap().clone_ref(py))?;
+                clbits.add(self.clbits.py_get_bit(py, *b)?.unwrap().clone_ref(py))?;
             }
         }
 
@@ -466,14 +584,28 @@ impl CircuitData {
     ///             CircuitInstruction(XGate(), [qr[1]], []),
     ///             CircuitInstruction(XGate(), [qr[0]], []),
     ///         ])
-    #[pyo3(signature = (qubits=None, clbits=None))]
+    #[pyo3(signature = (qubits=None, clbits=None, qregs=None, cregs=None))]
     pub fn replace_bits(
         &mut self,
         py: Python<'_>,
         qubits: Option<&Bound<PyAny>>,
         clbits: Option<&Bound<PyAny>>,
+        qregs: Option<&Bound<PyAny>>,
+        cregs: Option<&Bound<PyAny>>,
     ) -> PyResult<()> {
-        let mut temp = CircuitData::new(py, qubits, clbits, None, 0, self.global_phase.clone())?;
+        let mut temp = CircuitData::py_new(py, qubits, clbits, None, 0, self.global_phase.clone())?;
+        // Add qregs if provided.
+        if let Some(qregs) = qregs {
+            for qreg in qregs.try_iter()? {
+                temp.py_add_qreg(&qreg?)?;
+            }
+        }
+        // Add cregs if provided.
+        if let Some(cregs) = cregs {
+            for creg in cregs.try_iter()? {
+                temp.py_add_creg(&creg?)?;
+            }
+        }
         if qubits.is_some() {
             if temp.num_qubits() < self.num_qubits() {
                 return Err(PyValueError::new_err(format!(
@@ -504,16 +636,16 @@ impl CircuitData {
     // Note: we also rely on this to make us iterable!
     pub fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult<PyObject> {
         // Get a single item, assuming the index is validated as in bounds.
-        let get_single = |index: usize| {
+        let get_single = |index: usize| -> PyObject {
             let inst = &self.data[index];
             let qubits = self.qargs_interner.get(inst.qubits);
             let clbits = self.cargs_interner.get(inst.clbits);
             CircuitInstruction {
                 operation: inst.op.clone(),
-                qubits: PyTuple::new(py, self.qubits.map_indices(qubits))
+                qubits: PyTuple::new(py, self.qubits.py_map_indices(py, qubits).unwrap())
                     .unwrap()
                     .unbind(),
-                clbits: PyTuple::new(py, self.clbits.map_indices(clbits))
+                clbits: PyTuple::new(py, self.clbits.py_map_indices(py, clbits).unwrap())
                     .unwrap()
                     .unbind(),
                 params: inst.params_view().iter().cloned().collect(),
@@ -663,7 +795,7 @@ impl CircuitData {
 
     pub fn extend(&mut self, py: Python<'_>, itr: &Bound<PyAny>) -> PyResult<()> {
         if let Ok(other) = itr.downcast::<CircuitData>() {
-            let other = other.borrow();
+            let other = other.borrow_mut();
             // Fast path to avoid unnecessary construction of CircuitInstruction instances.
             self.data.reserve(other.data.len());
             for inst in other.data.iter() {
@@ -674,7 +806,7 @@ impl CircuitData {
                     .map(|b| {
                         Ok(self
                             .qubits
-                            .find(other.qubits.get(*b).unwrap().bind(py))
+                            .py_find_bit(other.qubits.py_get_bit(py, *b)?.unwrap().bind(py))?
                             .unwrap())
                     })
                     .collect::<PyResult<Vec<Qubit>>>()?;
@@ -685,7 +817,7 @@ impl CircuitData {
                     .map(|b| {
                         Ok(self
                             .clbits
-                            .find(other.clbits.get(*b).unwrap().bind(py))
+                            .py_find_bit(other.clbits.py_get_bit(py, *b)?.unwrap().bind(py))?
                             .unwrap())
                     })
                     .collect::<PyResult<Vec<Clbit>>>()?;
@@ -824,26 +956,51 @@ impl CircuitData {
     }
 
     fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
-        for bit in self.qubits.bits().iter().chain(self.clbits.bits().iter()) {
+        for bit in self
+            .qubits
+            .py_bits_raw()
+            .iter()
+            .chain(self.clbits.py_bits_raw().iter())
+            .filter_map(|cell| cell.get())
+        {
             visit.call(bit)?;
         }
-
+        for register in self
+            .qubits
+            .py_regs_raw()
+            .iter()
+            .chain(self.clbits.py_regs_raw().iter())
+            .filter_map(|cell| cell.get())
+        {
+            visit.call(register)?;
+        }
         // Note:
         //   There's no need to visit the native Rust data
         //   structures used for internal tracking: the only Python
         //   references they contain are to the bits in these lists!
-        visit.call(self.qubits.cached())?;
-        visit.call(self.clbits.cached())?;
+        if let Some(bits) = self.qubits.py_bits_cached_raw() {
+            visit.call(bits)?;
+        }
+        if let Some(registers) = self.qubits.py_regs_cached_raw() {
+            visit.call(registers)?;
+        }
+        if let Some(bits) = self.clbits.py_bits_cached_raw() {
+            visit.call(bits)?;
+        }
+        if let Some(registers) = self.clbits.py_regs_cached_raw() {
+            visit.call(registers)?;
+        }
         self.param_table.py_gc_traverse(&visit)?;
         Ok(())
     }
 
-    fn __clear__(&mut self) {
+    fn __clear__(&mut self) -> PyResult<()> {
         // Clear anything that could have a reference cycle.
         self.data.clear();
-        self.qubits.dispose();
-        self.clbits.dispose();
+        self.qubits.dispose()?;
+        self.clbits.dispose()?;
         self.param_table.clear();
+        Ok(())
     }
 
     /// Set the global phase of the circuit.
@@ -897,6 +1054,141 @@ impl CircuitData {
 }
 
 impl CircuitData {
+    /// Rust native constructor for [CircuitData]. Builds a new instance without
+    /// any python initialization.
+    pub fn new(
+        num_qubits: u32,
+        num_clbits: u32,
+        global_phase: Param,
+        add_qreg: bool,
+        add_creg: bool,
+    ) -> Self {
+        let mut data = Self {
+            data: vec![],
+            qargs_interner: Interner::new(),
+            cargs_interner: Interner::new(),
+            qubits: NewBitData::with_capacity(
+                "qubits".to_owned(),
+                num_qubits
+                    .try_into()
+                    .expect("The number of qubits provided exceeds the limit for a circuit."),
+                if add_qreg { 1 } else { 0 },
+            ),
+            clbits: NewBitData::with_capacity(
+                "clbits".to_owned(),
+                num_clbits
+                    .try_into()
+                    .expect("The number of clbits provided exceeds the limit for a circuit."),
+                if add_creg { 1 } else { 0 },
+            ),
+            param_table: ParameterTable::new(),
+            global_phase: Param::Float(0.),
+        };
+
+        // Set the global phase using internal setter.
+        data._set_global_phase_float(global_phase);
+        // Add all the bits into a register
+        if add_qreg {
+            data.add_qreg(
+                Some("q".to_string()),
+                Some(
+                    num_qubits
+                        .try_into()
+                        .expect("The number of qubits provided exceeds the limit for a circuit."),
+                ),
+                None,
+            );
+        } else {
+            (0..num_qubits).for_each(|_| {
+                data.add_qubit();
+            });
+        }
+        // Add all the bits into a register
+        if add_creg {
+            data.add_creg(
+                Some("c".to_string()),
+                Some(
+                    num_clbits
+                        .try_into()
+                        .expect("The number of clbits provided exceeds the limit for a circuit."),
+                ),
+                None,
+            );
+        } else {
+            (0..num_clbits).for_each(|_| {
+                data.add_clbit();
+            });
+        }
+        data
+    }
+
+    /// Adds a generic qubit to a circuit
+    pub fn add_qubit(&mut self) -> Qubit {
+        self.qubits.add_bit()
+    }
+
+    /// Get qubit location in the circuit
+    pub fn get_qubit_location(&self, qubit: Qubit) -> &[BitLocation] {
+        self.qubits.get_bit_info(qubit)
+    }
+
+    /// Adds either a generic register with new bits, or uses existing bit indices.
+    pub fn add_qreg(
+        &mut self,
+        name: Option<String>,
+        num_qubits: Option<usize>,
+        bits: Option<Cow<'_, [Qubit]>>,
+    ) -> Option<u32> {
+        self.qubits.add_register(name, num_qubits, bits)
+    }
+
+    /// Returns an iterator with all the QuantumRegisters in the circuit
+    pub fn qregs(&self) -> &[QuantumRegister] {
+        self.qubits.registers()
+    }
+
+    /// Adds a generic clbit to a circuit
+    pub fn add_clbit(&mut self) -> Clbit {
+        self.clbits.add_bit()
+    }
+
+    /// Adds either a generic register with new bits, or uses existing bit indices.
+    pub fn add_creg(
+        &mut self,
+        name: Option<String>,
+        num_qubits: Option<usize>,
+        bits: Option<Cow<'_, [Clbit]>>,
+    ) -> Option<u32> {
+        self.clbits.add_register(name, num_qubits, bits)
+    }
+
+    /// Returns an iterator with all the QuantumRegisters in the circuit
+    pub fn cregs(&self) -> &[ClassicalRegister] {
+        self.clbits.registers()
+    }
+
+    /// Get qubit location in the circuit
+    pub fn get_clbit_location(&self, clbit: Clbit) -> &[BitLocation] {
+        self.clbits.get_bit_info(clbit)
+    }
+
+    /// Set the global phase of the circuit using a float, without needing a
+    /// `py` token.
+    ///
+    /// _**Note:** for development purposes only. Should be removed after
+    /// [#13278](https://github.com/Qiskit/qiskit/pull/13278)._
+    fn _set_global_phase_float(&mut self, angle: Param) {
+        match angle {
+            Param::Float(angle) => {
+                self.global_phase = Param::Float(angle.rem_euclid(2. * std::f64::consts::PI));
+            }
+            _ => panic!(
+                "Could not set the parameter {:?}. Parameter was not a float.",
+                &angle
+            ),
+        }
+    }
+
     /// An alternate constructor to build a new `CircuitData` from an iterator
     /// of packed operations. This can be used to build a circuit from a sequence
     /// of `PackedOperation` without needing to involve Python.
@@ -989,8 +1281,8 @@ impl CircuitData {
     /// * global_phase: The global phase value to use for the new circuit.
     pub fn from_packed_instructions<I>(
         py: Python,
-        qubits: BitData<Qubit>,
-        clbits: BitData<Clbit>,
+        qubits: NewBitData<Qubit, QuantumRegister>,
+        clbits: NewBitData<Clbit, ClassicalRegister>,
         qargs_interner: Interner<[Qubit]>,
         cargs_interner: Interner<[Clbit]>,
         instructions: I,
@@ -1086,8 +1378,8 @@ impl CircuitData {
             data: Vec::with_capacity(instruction_capacity),
             qargs_interner: Interner::new(),
             cargs_interner: Interner::new(),
-            qubits: BitData::new(py, "qubits".to_string()),
-            clbits: BitData::new(py, "clbits".to_string()),
+            qubits: NewBitData::with_capacity("qubits".to_string(), num_qubits as usize, 0),
+            clbits: NewBitData::with_capacity("clbits".to_string(), num_clbits as usize, 0),
             param_table: ParameterTable::new(),
             global_phase: Param::Float(0.0),
         };
@@ -1097,17 +1389,13 @@ impl CircuitData {
         res.set_global_phase(py, global_phase)?;
 
         if num_qubits > 0 {
-            let qubit_cls = QUBIT.get_bound(py);
             for _i in 0..num_qubits {
-                let bit = qubit_cls.call0()?;
-                res.add_qubit(py, &bit, true)?;
+                res.add_qubit();
             }
         }
         if num_clbits > 0 {
-            let clbit_cls = CLBIT.get_bound(py);
             for _i in 0..num_clbits {
-                let bit = clbit_cls.call0()?;
-                res.add_clbit(py, &bit, true)?;
+                res.add_clbit();
             }
         }
         Ok(res)
@@ -1215,10 +1503,10 @@ impl CircuitData {
     fn pack(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult<PackedInstruction> {
         let qubits = self
             .qargs_interner
-            .insert_owned(self.qubits.map_bits(inst.qubits.bind(py))?.collect());
+            .insert_owned(self.qubits.py_map_bits(inst.qubits.bind(py))?.collect());
         let clbits = self
             .cargs_interner
-            .insert_owned(self.clbits.map_bits(inst.clbits.bind(py))?.collect());
+            .insert_owned(self.clbits.py_map_bits(inst.clbits.bind(py))?.collect());
         Ok(PackedInstruction {
             op: inst.operation.clone(),
             qubits,
@@ -1295,15 +1583,27 @@ impl CircuitData {
     }
 
     /// Returns an immutable view of the Qubits registered in the circuit
-    pub fn qubits(&self) -> &BitData<Qubit> {
+    pub fn qubits(&self) -> &NewBitData<Qubit, QuantumRegister> {
         &self.qubits
     }
 
     /// Returns an immutable view of the Classical bits registered in the circuit
-    pub fn clbits(&self) -> &BitData<Clbit> {
+    pub fn clbits(&self) -> &NewBitData<Clbit, ClassicalRegister> {
         &self.clbits
     }
 
+    // TODO: Remove
+    /// Returns a mutable view of the Qubits registered in the circuit
+    pub fn qubits_mut(&mut self) -> &mut NewBitData<Qubit, QuantumRegister> {
+        &mut self.qubits
+    }
+
+    // TODO: Remove
+    /// Returns a mutable view of the Classical bits registered in the circuit
+    pub fn clbits_mut(&mut self) -> &mut NewBitData<Clbit, ClassicalRegister> {
+        &mut self.clbits
+    }
+
     /// Unpacks from interned value to `[Qubit]`
     pub fn get_qargs(&self, index: Interned<[Qubit]>) -> &[Qubit] {
         self.qargs_interner().get(index)
@@ -1595,3 +1895,265 @@ impl<'py> FromPyObject<'py> for AssignParam {
         Ok(Self(Param::extract_no_coerce(ob)?))
     }
 }
+
+#[cfg(test)]
+mod test {
+    use crate::register::Register;
+
+    use super::*;
+
+    #[test]
+    fn test_circuit_construction() {
+        let num_qubits = 4;
+        let num_clbits = 3;
+        let circuit_data = CircuitData::new(num_qubits, num_clbits, Param::Float(0.0), true, true);
+
+        // Expected qregs
+        let example_qreg = QuantumRegister::new(Some(4), Some("q".to_owned()), None);
+        let expected_qregs: Vec<QuantumRegister> = vec![example_qreg];
+
+        assert_eq!(circuit_data.qregs(), &expected_qregs);
+
+        // Expected cregs
+        let example_creg = ClassicalRegister::new(Some(3), Some("c".to_owned()), None);
+        let expected_cregs: Vec<ClassicalRegister> = vec![example_creg];
+        assert_eq!(circuit_data.cregs(), &expected_cregs)
+    }
+
+    #[test]
+    fn test_circuit_construction_no_regs() {
+        let num_qubits = 4;
+        let num_clbits = 3;
+        let circuit_data =
+            CircuitData::new(num_qubits, num_clbits, Param::Float(0.0), false, false);
+
+        // Register lists should be empty
+        assert!(
+            circuit_data.qregs().is_empty(),
+            "There are quantum registers in the circuit!"
+        );
+        assert!(
+            circuit_data.cregs().is_empty(),
+            "There are classical registers in the circuit!"
+        );
+
+        for qubit in 0..num_qubits {
+            assert!(
+                circuit_data.qubits().get_bit_info(qubit.into()).is_empty(),
+                "A qubit seems to have a register assigned, even when none exist yet!"
+            )
+        }
+
+        for clbit in 0..num_clbits {
+            assert!(circuit_data.clbits().get_bit_info(clbit.into()).is_empty())
+        }
+    }
+
+    #[test]
+    fn test_circuit_bit_multiple_registers() {
+        let mut circuit: CircuitData = CircuitData::new(0, 0, 0.0.into(), false, false);
+        assert_eq!(
+            circuit.num_qubits(),
+            0,
+            "The circuit says it contains bits, even when they don't exist!"
+        );
+
+        circuit.add_qreg(None, Some(3), None);
+        assert_eq!(
+            circuit.num_qubits(),
+            3,
+            "The qubits were not properly added!"
+        );
+        assert_eq!(
+            circuit.qregs().len(),
+            1,
+            "The qreg was either not added or mishandled!"
+        );
+
+        circuit.add_qreg(None, None, Some(vec![2.into()].into()));
+        assert_eq!(circuit.num_qubits(), 3, "The number of qubits changed!");
+        assert_eq!(
+            circuit.qregs().len(),
+            2,
+            "The qreg was either not added or mishandled!"
+        );
+
+        let locations: &[BitLocation] = circuit.get_qubit_location(2.into());
+        assert_eq!(
+            locations.len(),
+            2,
+            "The new register was not assigned to the bit"
+        );
+        assert_eq!(
+            locations.first(),
+            Some(&BitLocation::new(0, 2)),
+            "Incorrect register assigned as original."
+        );
+        assert_eq!(
+            locations.last(),
+            Some(&BitLocation::new(1, 0)),
+            "Incorrect register assigned as secondary."
+        );
+
+        // Check if the registers contain the index in question
+        for reg in circuit.qregs() {
+            assert!(reg.contains(2.into()))
+        }
+    }
+
+    #[test]
+    fn test_duplicate_q_register() {
+        // The circuit will be initialized by default with qregs ['q'] and cregs ['c']
+        // Should not add repeated instances of registers with the same hash value.
+        //
+        let mut circ = CircuitData::new(3, 0, 0.0.into(), true, false);
+
+        // Should not panic
+        circ.add_qreg(
+            Some("q".to_string()),
+            None,
+            Some((1..3).map(Qubit).collect::<Vec<_>>().into()),
+        );
+
+        assert_eq!(
+            circ.get_qubit_location(2.into()),
+            &[BitLocation::new(0, 2), BitLocation::new(1, 1)]
+        );
+
+        // When trying to add a register with the same name and length, return the original instance.
+        assert_eq!(circ.add_qreg(Some("q".to_string()), Some(3), None), Some(0));
+
+        // When trying to add a register with the same name and length, return the original instance.
+        assert_eq!(circ.add_qreg(Some("q".to_string()), Some(2), None), Some(1));
+    }
+
+    #[test]
+    fn test_duplicate_c_register() {
+        // The circuit will be initialized by default with qregs ['q'] and cregs ['c']
+        // Should not add repeated instances of registers with the same hash value.
+        //
+        let mut circ = CircuitData::new(0, 3, 0.0.into(), false, true);
+
+        // Should not panic
+        circ.add_creg(
+            Some("c".to_string()),
+            None,
+            Some((1..3).map(Clbit).collect::<Vec<_>>().into()),
+        );
+
+        assert_eq!(
+            circ.get_clbit_location(2.into()),
+            &[BitLocation::new(0, 2), BitLocation::new(1, 1)]
+        );
+
+        // When trying to add a register with the same name and length, return the original instance.
+        assert_eq!(circ.add_creg(Some("c".to_string()), Some(3), None), Some(0));
+
+        // When trying to add a register with the same name and length, return the original instance.
+        assert_eq!(circ.add_creg(Some("c".to_string()), Some(2), None), Some(1));
+    }
+}
+
+#[cfg(all(test, not(miri)))]
+// #[cfg(all(test))]
+mod pytest {
+    use pyo3::PyTypeInfo;
+
+    use super::*;
+
+    // Test Rust native circuit construction when accessed through Python, without
+    // adding registers to the circuit.
+    #[test]
+    fn test_circuit_construction_py_no_regs() {
+        let num_qubits = 4;
+        let num_clbits = 3;
+        let circuit_data =
+            CircuitData::new(num_qubits, num_clbits, Param::Float(0.0), false, false);
+        let result = Python::with_gil(|py| -> PyResult<bool> {
+            let quantum_circuit = QUANTUM_CIRCUIT.get_bound(py).clone();
+
+            let converted_circuit =
+                quantum_circuit.call_method1("_from_circuit_data", (circuit_data,))?;
+
+            let converted_qregs = converted_circuit.getattr("qregs")?;
+            assert!(converted_qregs.is_instance(&PyList::type_object(py))?);
+            assert!(
+                converted_qregs.downcast::<PyList>()?.len() == 0,
+                "The quantum registers list returned a non-empty value"
+            );
+
+            let converted_qubits = converted_circuit.getattr("qubits")?;
+            assert!(converted_qubits.is_instance(&PyList::type_object(py))?);
+            assert!(
+                converted_qubits.downcast::<PyList>()?.len() == (num_qubits as usize),
+                "The qubits has the wrong length"
+            );
+
+            let converted_qregs = converted_circuit.getattr("qregs")?;
+            assert!(converted_qregs.is_instance(&PyList::type_object(py))?);
+            assert!(
+                converted_qregs.downcast::<PyList>()?.len() == 0,
+                "The classical registers list returned a non-empty value"
+            );
+
+            let converted_clbits = converted_circuit.getattr("clbits")?;
+            assert!(converted_clbits.is_instance(&PyList::type_object(py))?);
+            assert!(
+                converted_clbits.downcast::<PyList>()?.len() == (num_clbits as usize),
+                "The clbits has the wrong length"
+            );
+
+            Ok(true)
+        })
+        .is_ok_and(|res| res);
+        assert!(result);
+    }
+
+    // Test Rust native circuit construction when accessed through Python.
+    #[test]
+    fn test_circuit_construction() {
+        let num_qubits = 4;
+        let num_clbits = 3;
+        let circuit_data = CircuitData::new(num_qubits, num_clbits, Param::Float(0.0), true, true);
+        let result = Python::with_gil(|py| -> PyResult<bool> {
+            let quantum_circuit = QUANTUM_CIRCUIT.get_bound(py).clone();
+
+            let converted_circuit = quantum_circuit.call_method1(
+                "_from_circuit_data",
+                (circuit_data.clone().into_pyobject(py)?,),
+            )?;
+            let expected_circuit = quantum_circuit.call((num_qubits, num_clbits), None)?;
+
+            let converted_qregs = converted_circuit.getattr("qregs")?;
+            let expected_qregs = expected_circuit.getattr("qregs")?;
+
+            assert!(converted_qregs.eq(expected_qregs)?);
+
+            let converted_cregs = converted_circuit.getattr("cregs")?;
+            let expected_cregs = expected_circuit.getattr("cregs")?;
+
+            assert!(converted_cregs.eq(expected_cregs)?);
+
+            let converted_qubits = converted_circuit.getattr("qubits")?;
+            let expected_qubits = expected_circuit.getattr("qubits")?;
+            assert!(converted_qubits.eq(&expected_qubits)?);
+
+            let converted_clbits = converted_circuit.getattr("clbits")?;
+            let expected_clbits = expected_circuit.getattr("clbits")?;
+            assert!(converted_clbits.eq(&expected_clbits)?);
+
+            let converted_global_phase = converted_circuit.getattr("global_phase")?;
+            let expected_global_phase = expected_circuit.getattr("global_phase")?;
+
+            assert!(converted_global_phase.eq(&expected_global_phase)?);
+
+            // TODO: Figure out why this fails
+            // println!("{:?}", expected_circuit.eq(&converted_circuit));
+
+            // Return true due to being unable to extract `CircuitData` from python
+            // due to conflics between cargo and python binaries.
+            Ok(true)
+        });
+        assert!(result.is_ok_and(|result| result))
+    }
+}
diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs
index b9b7b433eec8..32f45ba2e416 100644
--- a/crates/circuit/src/converters.rs
+++ b/crates/circuit/src/converters.rs
@@ -15,14 +15,11 @@ use std::sync::OnceLock;
 
 use hashbrown::HashMap;
 use pyo3::prelude::*;
-use pyo3::{
-    intern,
-    types::{PyDict, PyList},
-};
+use pyo3::{intern, types::PyDict};
 
-use crate::circuit_data::CircuitData;
 use crate::dag_circuit::{DAGCircuit, NodeType};
 use crate::packed_instruction::PackedInstruction;
+use crate::{bit_data::NewBitData, circuit_data::CircuitData};
 
 /// An extractable representation of a QuantumCircuit reserved only for
 /// conversion purposes.
@@ -32,8 +29,6 @@ pub struct QuantumCircuitData<'py> {
     pub name: Option<Bound<'py, PyAny>>,
     pub calibrations: Option<HashMap<String, Py<PyDict>>>,
     pub metadata: Option<Bound<'py, PyAny>>,
-    pub qregs: Option<Bound<'py, PyList>>,
-    pub cregs: Option<Bound<'py, PyList>>,
     pub input_vars: Vec<Bound<'py, PyAny>>,
     pub captured_vars: Vec<Bound<'py, PyAny>>,
     pub declared_vars: Vec<Bound<'py, PyAny>>,
@@ -52,14 +47,6 @@ impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> {
                 .extract()
                 .ok(),
             metadata: ob.getattr(intern!(py, "metadata")).ok(),
-            qregs: ob
-                .getattr(intern!(py, "qregs"))
-                .map(|ob| ob.downcast_into())?
-                .ok(),
-            cregs: ob
-                .getattr(intern!(py, "cregs"))
-                .map(|ob| ob.downcast_into())?
-                .ok(),
             input_vars: ob
                 .call_method0(intern!(py, "iter_input_vars"))?
                 .try_iter()?
@@ -99,10 +86,10 @@ pub fn dag_to_circuit(
     dag: &DAGCircuit,
     copy_operations: bool,
 ) -> PyResult<CircuitData> {
-    CircuitData::from_packed_instructions(
+    let mut circuit = CircuitData::from_packed_instructions(
         py,
-        dag.qubits().clone(),
-        dag.clbits().clone(),
+        NewBitData::from_bit_data(py, dag.qubits()),
+        NewBitData::from_bit_data(py, dag.clbits()),
         dag.qargs_interner().clone(),
         dag.cargs_interner().clone(),
         dag.topological_op_nodes()?.map(|node_index| {
@@ -133,7 +120,15 @@ pub fn dag_to_circuit(
             }
         }),
         dag.get_global_phase(),
-    )
+    )?;
+    // Manually add qregs and cregs
+    for reg in dag.qregs.bind(py).values() {
+        circuit.py_add_qreg(&reg)?;
+    }
+    for reg in dag.cregs.bind(py).values() {
+        circuit.py_add_creg(&reg)?;
+    }
+    Ok(circuit)
 }
 
 pub fn converters(m: &Bound<PyModule>) -> PyResult<()> {
diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs
index 42b334e88207..90b7beab1903 100644
--- a/crates/circuit/src/dag_circuit.rs
+++ b/crates/circuit/src/dag_circuit.rs
@@ -193,9 +193,9 @@ pub struct DAGCircuit {
     dag: StableDiGraph<NodeType, Wire>,
 
     #[pyo3(get)]
-    qregs: Py<PyDict>,
+    pub qregs: Py<PyDict>,
     #[pyo3(get)]
-    cregs: Py<PyDict>,
+    pub cregs: Py<PyDict>,
 
     /// The cache used to intern instruction qargs.
     pub qargs_interner: Interner<[Qubit]>,
@@ -6729,7 +6729,7 @@ impl DAGCircuit {
                             &qubit
                         )));
                     }
-                    let qubit_index = qc_data.qubits().find(&qubit).unwrap();
+                    let qubit_index = qc_data.qubits().py_find_bit(&qubit)?.unwrap();
                     ordered_vec[qubit_index.index()] = new_dag.add_qubit_unchecked(py, &qubit)?;
                     Ok(())
                 })?;
@@ -6741,7 +6741,7 @@ impl DAGCircuit {
         } else {
             qc_data
                 .qubits()
-                .bits()
+                .py_bits(py)?
                 .iter()
                 .try_for_each(|qubit| -> PyResult<_> {
                     new_dag.add_qubit_unchecked(py, qubit.bind(py))?;
@@ -6762,7 +6762,7 @@ impl DAGCircuit {
                             &clbit
                         )));
                     };
-                    let clbit_index = qc_data.clbits().find(&clbit).unwrap();
+                    let clbit_index = qc_data.clbits().py_find_bit(&clbit)?.unwrap();
                     ordered_vec[clbit_index.index()] = new_dag.add_clbit_unchecked(py, &clbit)?;
                     Ok(())
                 })?;
@@ -6774,7 +6774,7 @@ impl DAGCircuit {
         } else {
             qc_data
                 .clbits()
-                .bits()
+                .py_bits(py)?
                 .iter()
                 .try_for_each(|clbit| -> PyResult<()> {
                     new_dag.add_clbit_unchecked(py, clbit.bind(py))?;
@@ -6797,16 +6797,12 @@ impl DAGCircuit {
         }
 
         // Add all the registers
-        if let Some(qregs) = qc.qregs {
-            for qreg in qregs.iter() {
-                new_dag.add_qreg(py, &qreg)?;
-            }
+        for qreg in qc_data.py_qregs(py)?.bind(py).iter() {
+            new_dag.add_qreg(py, &qreg)?;
         }
 
-        if let Some(cregs) = qc.cregs {
-            for creg in cregs.iter() {
-                new_dag.add_creg(py, &creg)?;
-            }
+        for creg in qc_data.py_cregs(py)?.bind(py).iter() {
+            new_dag.add_creg(py, &creg)?;
         }
 
         new_dag.try_extend(
@@ -6841,8 +6837,6 @@ impl DAGCircuit {
             name: None,
             calibrations: None,
             metadata: None,
-            qregs: None,
-            cregs: None,
             input_vars: Vec::new(),
             captured_vars: Vec::new(),
             declared_vars: Vec::new(),
diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs
index a591dfb1569e..933a715e54d1 100644
--- a/crates/circuit/src/imports.rs
+++ b/crates/circuit/src/imports.rs
@@ -66,10 +66,12 @@ pub static CONTROL_FLOW_OP: ImportOnceCell =
     ImportOnceCell::new("qiskit.circuit.controlflow", "ControlFlowOp");
 pub static QUBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumregister", "Qubit");
 pub static CLBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classicalregister", "Clbit");
+pub static BIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.bit", "Bit");
 pub static QUANTUM_REGISTER: ImportOnceCell =
     ImportOnceCell::new("qiskit.circuit.quantumregister", "QuantumRegister");
 pub static CLASSICAL_REGISTER: ImportOnceCell =
     ImportOnceCell::new("qiskit.circuit.classicalregister", "ClassicalRegister");
+pub static REGISTER: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.register", "Register");
 pub static PARAMETER_EXPRESSION: ImportOnceCell =
     ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression");
 pub static PARAMETER_VECTOR: ImportOnceCell =
diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs
index 5138438dec33..69e8cbf290b1 100644
--- a/crates/circuit/src/lib.rs
+++ b/crates/circuit/src/lib.rs
@@ -10,6 +10,7 @@
 // copyright notice, and modified files need to carry a notice indicating
 // that they have been altered from the originals.
 
+pub mod bit;
 pub mod bit_data;
 pub mod circuit_data;
 pub mod circuit_instruction;
@@ -24,11 +25,13 @@ pub mod interner;
 pub mod operations;
 pub mod packed_instruction;
 pub mod parameter_table;
+pub mod register;
 pub mod slice;
 pub mod util;
 
 mod rustworkx_core_vnext;
 
+use imports::{CLBIT, QUBIT};
 use pyo3::prelude::*;
 use pyo3::types::{PySequence, PyTuple};
 
@@ -122,6 +125,27 @@ impl From<Clbit> for BitType {
     }
 }
 
+/// **For development purposes only.** This ensures we convert to the correct Bit
+/// type in Python since [BitData] does not know what its types are inherently.
+pub trait ToPyBit {
+    /// Creates an empty bit from a rust bit instance of the correct type.
+    ///
+    /// _**Note:** Should only be used when dealing with fully opaque bits._
+    fn to_py_bit(py: Python) -> PyResult<PyObject>;
+}
+
+impl ToPyBit for Qubit {
+    fn to_py_bit(py: Python) -> PyResult<PyObject> {
+        QUBIT.get_bound(py).call0().map(|bit| bit.into())
+    }
+}
+
+impl ToPyBit for Clbit {
+    fn to_py_bit(py: Python) -> PyResult<PyObject> {
+        CLBIT.get_bound(py).call0().map(|bit| bit.into())
+    }
+}
+
 /// Implement `IntoPyObject` for the reference to a struct or enum declared as `#[pyclass]` that is
 /// also `Copy`.
 ///
diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs
new file mode 100644
index 000000000000..e86d1eed8783
--- /dev/null
+++ b/crates/circuit/src/register.rs
@@ -0,0 +1,247 @@
+// This code is part of Qiskit.
+//
+// (C) Copyright IBM 2025
+//
+// This code is licensed under the Apache License, Version 2.0. You may
+// obtain a copy of this license in the LICENSE.txt file in the root directory
+// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+//
+// Any modifications or derivative works of this code must retain this
+// copyright notice, and modified files need to carry a notice indicating
+// that they have been altered from the originals.
+
+use indexmap::IndexSet;
+use pyo3::{exceptions::PyTypeError, intern, types::PyAnyMethods, FromPyObject};
+use std::{
+    borrow::Cow,
+    hash::{Hash, Hasher},
+    ops::Index,
+    sync::Mutex,
+};
+
+use crate::{
+    imports::{CLASSICAL_REGISTER, QUANTUM_REGISTER, REGISTER},
+    Clbit, Qubit,
+};
+
+/// This represents the hash value of a Register according to the register's
+/// name and number of qubits.
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum RegisterAsKey {
+    Register((String, u32)),
+    Quantum((String, u32)),
+    Classical((String, u32)),
+}
+
+impl RegisterAsKey {
+    #[inline]
+    pub fn reduce(&self) -> (u32, &str) {
+        match self {
+            RegisterAsKey::Register(key) => (key.1, key.0.as_str()),
+            RegisterAsKey::Quantum(key) => (key.1, key.0.as_str()),
+            RegisterAsKey::Classical(key) => (key.1, key.0.as_str()),
+        }
+    }
+
+    #[inline]
+    pub fn name(&self) -> &str {
+        match self {
+            RegisterAsKey::Register(key) => key.0.as_str(),
+            RegisterAsKey::Quantum(key) => key.0.as_str(),
+            RegisterAsKey::Classical(key) => key.0.as_str(),
+        }
+    }
+
+    #[inline]
+    pub fn size(&self) -> u32 {
+        match self {
+            RegisterAsKey::Register(key) => key.1,
+            RegisterAsKey::Quantum(key) => key.1,
+            RegisterAsKey::Classical(key) => key.1,
+        }
+    }
+
+    #[inline]
+    pub fn type_identifier(&self) -> &str {
+        match self {
+            RegisterAsKey::Register(_) => "Register",
+            RegisterAsKey::Quantum(_) => "QuantumRegister",
+            RegisterAsKey::Classical(_) => "ClassicalRegister",
+        }
+    }
+}
+
+impl<'py> FromPyObject<'py> for RegisterAsKey {
+    fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
+        if ob.is_instance(REGISTER.get_bound(ob.py()))? {
+            let (name, num_qubits) = (
+                ob.getattr(intern!(ob.py(), "name"))?.extract()?,
+                ob.len()? as u32,
+            );
+            if ob.is_instance(CLASSICAL_REGISTER.get_bound(ob.py()))? {
+                return Ok(RegisterAsKey::Classical((name, num_qubits)));
+            } else if ob.is_instance(QUANTUM_REGISTER.get_bound(ob.py()))? {
+                return Ok(RegisterAsKey::Quantum((name, num_qubits)));
+            } else {
+                return Ok(RegisterAsKey::Register((name, num_qubits)));
+            }
+        }
+        Err(PyTypeError::new_err(
+            "The provided argument was not a register.",
+        ))
+    }
+}
+/// Described the desired behavior of a Register.
+pub trait Register {
+    /// The type of bit stored by the [Register]
+    type Bit;
+
+    /// Returns the size of the [Register].
+    fn len(&self) -> usize;
+    /// Checks if the [Register] is empty.
+    fn is_empty(&self) -> bool;
+    /// Returns the name of the [Register].
+    fn name(&self) -> &str;
+    /// Checks if a bit exists within the [Register].
+    fn contains(&self, bit: Self::Bit) -> bool;
+    /// Finds the local index of a certain bit within [Register].
+    fn find_index(&self, bit: Self::Bit) -> Option<u32>;
+    /// Return an iterator over all the bits in the register
+    fn bits(&self) -> impl ExactSizeIterator<Item = Self::Bit>;
+    /// Returns the register as a Key
+    fn as_key(&self) -> &RegisterAsKey;
+}
+
+macro_rules! create_register {
+    ($name:ident, $bit:ty, $counter:ident, $prefix:literal, $key:expr) => {
+        static $counter: Mutex<u32> = Mutex::new(0);
+
+        #[derive(Debug, Clone, Eq)]
+        pub struct $name {
+            register: IndexSet<<$name as Register>::Bit>,
+            key: RegisterAsKey,
+        }
+
+        impl $name {
+            pub fn new(
+                size: Option<usize>,
+                name: Option<String>,
+                bits: Option<Cow<'_, [$bit]>>,
+            ) -> Self {
+                let register: IndexSet<<$name as Register>::Bit> = if let Some(size) = size {
+                    (0..size).map(|bit| <$bit>::new(bit)).collect()
+                } else if let Some(bits) = bits {
+                    match bits {
+                        Cow::Borrowed(borrowed) => borrowed.iter().copied().collect(),
+                        Cow::Owned(owned) => owned.into_iter().collect(),
+                    }
+                } else {
+                    panic!("You should only provide either a size or the bit indices, not both.")
+                };
+                let name = if let Some(name) = name {
+                    name
+                } else {
+                    let count = if let Ok(ref mut count) = $counter.try_lock() {
+                        let curr = **count;
+                        **count += 1;
+                        curr
+                    } else {
+                        panic!("Could not access register counter.")
+                    };
+                    format!("{}{}", $prefix, count)
+                };
+                let length: u32 = register.len().try_into().unwrap();
+                Self {
+                    register,
+                    key: $key((name, length)),
+                }
+            }
+        }
+
+        impl Register for $name {
+            type Bit = $bit;
+
+            fn len(&self) -> usize {
+                self.register.len()
+            }
+
+            fn is_empty(&self) -> bool {
+                self.register.is_empty()
+            }
+
+            fn name(&self) -> &str {
+                self.key.name()
+            }
+
+            fn contains(&self, bit: Self::Bit) -> bool {
+                self.register.contains(&bit)
+            }
+
+            fn find_index(&self, bit: Self::Bit) -> Option<u32> {
+                self.register.get_index_of(&bit).map(|idx| idx as u32)
+            }
+
+            fn bits(&self) -> impl ExactSizeIterator<Item = Self::Bit> {
+                self.register.iter().copied()
+            }
+
+            fn as_key(&self) -> &RegisterAsKey {
+                &self.key
+            }
+        }
+
+        impl Hash for $name {
+            fn hash<H: Hasher>(&self, state: &mut H) {
+                (self.key).hash(state);
+            }
+        }
+
+        impl PartialEq for $name {
+            fn eq(&self, other: &Self) -> bool {
+                self.register.len() == other.register.len() && self.key == other.key
+            }
+        }
+
+        impl Index<usize> for $name {
+            type Output = $bit;
+
+            fn index(&self, index: usize) -> &Self::Output {
+                self.register.index(index)
+            }
+        }
+
+        impl From<(usize, Option<String>)> for $name {
+            fn from(value: (usize, Option<String>)) -> Self {
+                Self::new(Some(value.0), value.1, None)
+            }
+        }
+
+        impl From<Cow<'_, [$bit]>> for $name {
+            fn from(value: Cow<'_, [$bit]>) -> Self {
+                Self::new(None, None, Some(value))
+            }
+        }
+
+        impl From<(Cow<'_, [$bit]>, Option<String>)> for $name {
+            fn from(value: (Cow<'_, [$bit]>, Option<String>)) -> Self {
+                Self::new(None, value.1, Some(value.0))
+            }
+        }
+    };
+}
+
+create_register!(
+    QuantumRegister,
+    Qubit,
+    QREG_COUNTER,
+    "qr",
+    RegisterAsKey::Quantum
+);
+
+create_register!(
+    ClassicalRegister,
+    Clbit,
+    CREG_COUNTER,
+    "cr",
+    RegisterAsKey::Classical
+);
diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py
index 8e4276c16229..c32c030c3938 100644
--- a/qiskit/circuit/library/blueprintcircuit.py
+++ b/qiskit/circuit/library/blueprintcircuit.py
@@ -69,14 +69,23 @@ def _build(self) -> None:
 
     def _invalidate(self) -> None:
         """Invalidate the current circuit build."""
+        # Take out the registers before invalidating
+        qregs = self._data.qregs
+        cregs = self._data.cregs
         self._data = CircuitData(self._data.qubits, self._data.clbits)
+        for qreg in qregs:
+            self._data.add_qreg(qreg)
+        for creg in cregs:
+            self._data.add_creg(creg)
         self.global_phase = 0
         self._is_built = False
 
     @property
     def qregs(self):
         """A list of the quantum registers associated with the circuit."""
-        return self._qregs
+        if not self._is_initialized:
+            return self._qregs
+        return super().qregs
 
     @qregs.setter
     def qregs(self, qregs):
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
index 7ab1bae5d8cd..2af597188371 100644
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -1096,13 +1096,6 @@ def __init__(
             "qiskit.circuit.controlflow.builder.ControlFlowBuilderBlock"
         ] = []
 
-        self.qregs: list[QuantumRegister] = []
-        """A list of the :class:`QuantumRegister`\\ s in this circuit.  You should not mutate
-        this."""
-        self.cregs: list[ClassicalRegister] = []
-        """A list of the :class:`ClassicalRegister`\\ s in this circuit.  You should not mutate
-        this."""
-
         # Dict mapping Qubit or Clbit instances to tuple comprised of 0) the
         # corresponding index in circuit.{qubits,clbits} and 1) a list of
         # Register-int pairs for each Register containing the Bit and its index
@@ -1174,26 +1167,18 @@ def _from_circuit_data(
         if data.num_qubits > 0:
             if add_regs:
                 qr = QuantumRegister(name="q", bits=data.qubits)
-                out.qregs = [qr]
-                out._qubit_indices = {
-                    bit: BitLocations(index, [(qr, index)]) for index, bit in enumerate(data.qubits)
-                }
-            else:
-                out._qubit_indices = {
-                    bit: BitLocations(index, []) for index, bit in enumerate(data.qubits)
-                }
+                data.qregs = [qr]
 
+            out._qubit_indices = {
+                bit: BitLocations(*data.get_qubit_location(bit)) for bit in data.qubits
+            }
         if data.num_clbits > 0:
             if add_regs:
                 cr = ClassicalRegister(name="c", bits=data.clbits)
-                out.cregs = [cr]
-                out._clbit_indices = {
-                    bit: BitLocations(index, [(cr, index)]) for index, bit in enumerate(data.clbits)
-                }
-            else:
-                out._clbit_indices = {
-                    bit: BitLocations(index, []) for index, bit in enumerate(data.clbits)
-                }
+                data.cregs = [cr]
+            out._clbit_indices = {
+                bit: BitLocations(*data.get_clbit_location(bit)) for bit in data.clbits
+            }
 
         out._data = data
 
@@ -1445,6 +1430,8 @@ def __deepcopy__(self, memo=None):
         result._data.replace_bits(
             qubits=_copy.deepcopy(self._data.qubits, memo),
             clbits=_copy.deepcopy(self._data.clbits, memo),
+            qregs=_copy.deepcopy(self._data.qregs, memo),
+            cregs=_copy.deepcopy(self._data.cregs, memo),
         )
         return result
 
@@ -2237,6 +2224,30 @@ def clbits(self) -> list[Clbit]:
         this."""
         return self._data.clbits
 
+    @property
+    def qregs(self) -> list[QuantumRegister]:
+        """A list of :class:`Qubit`\\ s in the order that they were added.  You should not mutate
+        this."""
+        return self._data.qregs
+
+    @qregs.setter
+    def qregs(self, other: list[QuantumRegister]):
+        self._data.qregs = other
+        for qubit in self.qubits:
+            self._qubit_indices[qubit] = BitLocations(*self._data.get_qubit_location(qubit))
+
+    @property
+    def cregs(self) -> list[ClassicalRegister]:
+        """A list of :class:`Clbit`\\ s in the order that they were added.  You should not mutate
+        this."""
+        return self._data.cregs
+
+    @cregs.setter
+    def cregs(self, other: list[ClassicalRegister]):
+        self._data.cregs = other
+        for clbit in self.clbits:
+            self._clbit_indices[clbit] = BitLocations(*self._data.get_clbit_location(clbit))
+
     @property
     def ancillas(self) -> list[AncillaQubit]:
         """A list of :class:`AncillaQubit`\\ s in the order that they were added.  You should not
@@ -3075,16 +3086,10 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None:
                 self._add_qreg(register)
 
             elif isinstance(register, ClassicalRegister):
-                self.cregs.append(register)
+                self._data.add_creg(register)
 
-                for idx, bit in enumerate(register):
-                    if bit in self._clbit_indices:
-                        self._clbit_indices[bit].registers.append((register, idx))
-                    else:
-                        self._data.add_clbit(bit)
-                        self._clbit_indices[bit] = BitLocations(
-                            self._data.num_clbits - 1, [(register, idx)]
-                        )
+                for bit in register:
+                    self._clbit_indices[bit] = BitLocations(*self._data.get_clbit_location(bit))
 
             elif isinstance(register, list):
                 self.add_bits(register)
@@ -3092,14 +3097,10 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None:
                 raise CircuitError("expected a register")
 
     def _add_qreg(self, qreg: QuantumRegister) -> None:
-        self.qregs.append(qreg)
+        self._data.add_qreg(qreg)
 
-        for idx, bit in enumerate(qreg):
-            if bit in self._qubit_indices:
-                self._qubit_indices[bit].registers.append((qreg, idx))
-            else:
-                self._data.add_qubit(bit)
-                self._qubit_indices[bit] = BitLocations(self._data.num_qubits - 1, [(qreg, idx)])
+        for bit in qreg:
+            self._qubit_indices[bit] = BitLocations(*self._data.get_qubit_location(bit))
 
     def add_bits(self, bits: Iterable[Bit]) -> None:
         """Add Bits to the circuit."""
@@ -3114,10 +3115,10 @@ def add_bits(self, bits: Iterable[Bit]) -> None:
                 self._ancillas.append(bit)
             if isinstance(bit, Qubit):
                 self._data.add_qubit(bit)
-                self._qubit_indices[bit] = BitLocations(self._data.num_qubits - 1, [])
+                self._qubit_indices[bit] = BitLocations(*self._data.get_qubit_location(bit))
             elif isinstance(bit, Clbit):
                 self._data.add_clbit(bit)
-                self._clbit_indices[bit] = BitLocations(self._data.num_clbits - 1, [])
+                self._clbit_indices[bit] = BitLocations(*self._data.get_clbit_location(bit))
             else:
                 raise CircuitError(
                     "Expected an instance of Qubit, Clbit, or "
@@ -3177,8 +3178,12 @@ def find_bit(self, bit: Bit) -> BitLocations:
 
         try:
             if isinstance(bit, Qubit):
+                if bit not in self._qubit_indices:
+                    self._qubit_indices[bit] = BitLocations(*self._data.get_qubit_location(bit))
                 return self._qubit_indices[bit]
             elif isinstance(bit, Clbit):
+                if bit not in self._clbit_indices:
+                    self._clbit_indices[bit] = BitLocations(*self._data.get_clbit_location(bit))
                 return self._clbit_indices[bit]
             else:
                 raise CircuitError(f"Could not locate bit of unknown type: {type(bit)}")
@@ -3698,8 +3703,16 @@ def copy(self, name: str | None = None) -> typing.Self:
         Returns:
           QuantumCircuit: a deepcopy of the current circuit, with the specified name
         """
-        cpy = self.copy_empty_like(name)
+        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_metadata(self, cpy, "alike")
         cpy._data = self._data.copy()
+        if name is not None:
+            cpy.name = name
         return cpy
 
     def copy_empty_like(
@@ -3755,9 +3768,7 @@ def copy_empty_like(
 
         _copy_metadata(self, cpy, vars_mode)
 
-        cpy._data = CircuitData(
-            self._data.qubits, self._data.clbits, global_phase=self._data.global_phase
-        )
+        cpy._data = self._data.copy_empty_like()
 
         if name:
             cpy.name = name
@@ -4071,11 +4082,18 @@ def remove_final_measurements(self, inplace: bool = True) -> Optional["QuantumCi
         circ.cregs = []
         circ._clbit_indices = {}
 
+        # Save the old qregs
+        old_qregs = circ.qregs
+
         # Clear instruction info
         circ._data = CircuitData(
             qubits=circ._data.qubits, reserve=len(circ._data), global_phase=circ.global_phase
         )
 
+        # Re-add old registers
+        for qreg in old_qregs:
+            circ.add_register(qreg)
+
         # We must add the clbits first to preserve the original circuit
         # order. This way, add_register never adds clbits and just
         # creates registers that point to them.
diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py
index bd7d720a5dd0..b6c3cee06edf 100644
--- a/qiskit/converters/circuit_to_instruction.py
+++ b/qiskit/converters/circuit_to_instruction.py
@@ -146,9 +146,13 @@ def fix_condition(op):
     data.replace_bits(qubits=qreg, clbits=creg)
     data.map_nonstandard_ops(fix_condition)
 
-    qc = QuantumCircuit(*regs, name=out_instruction.name)
+    qc = QuantumCircuit(name=out_instruction.name)
     qc._data = data
 
+    # Re-add the registers.
+    for reg in regs:
+        qc.add_register(reg)
+
     if circuit.global_phase:
         qc.global_phase = circuit.global_phase
 
diff --git a/releasenotes/notes/add_rust_native_bits_and_registers-3691716ca96194c2.yaml b/releasenotes/notes/add_rust_native_bits_and_registers-3691716ca96194c2.yaml
new file mode 100644
index 000000000000..613bb4cadad1
--- /dev/null
+++ b/releasenotes/notes/add_rust_native_bits_and_registers-3691716ca96194c2.yaml
@@ -0,0 +1,13 @@
+---
+features_circuits:
+  - |
+    Added rust representation of the :class:`.QuantumRegister` and :class:`.ClassicalRegister`
+    classes.
+upgrade_circuits:
+  - |
+    Reformulate :class:`.CircuitData` to use its native representations of :class:`.Bit`, and
+    :class:`.Register` as the source of truth. With this there is no longer a need to initialize
+    a circuit from rust with a `py` token.
+  - |
+    Refactor :class:`.CircuitData`'s ``BitData`` to store registers and enables users to use
+    stores bits before initializing them with python counterparts.
diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py
index 1b78861c6c00..60951182be02 100644
--- a/test/python/circuit/test_gate_definitions.py
+++ b/test/python/circuit/test_gate_definitions.py
@@ -21,7 +21,6 @@
 from qiskit import QuantumCircuit, QuantumRegister
 from qiskit.quantum_info import Operator
 from qiskit.circuit import ParameterVector, Gate, ControlledGate
-from qiskit.circuit.quantumregister import Qubit
 from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate
 from qiskit.circuit.library import standard_gates
 from qiskit.circuit.library import (
@@ -453,8 +452,8 @@ def test_definition_parameters(self, gate_class):
         self.assertGreaterEqual(len(param_entry), 1)
         self.assertGreaterEqual(len(float_entry), 1)
 
-        param_qc = QuantumCircuit([Qubit() for _ in range(param_gate.num_qubits)])
-        float_qc = QuantumCircuit([Qubit() for _ in range(float_gate.num_qubits)])
+        param_qc = QuantumCircuit(param_gate.num_qubits)
+        float_qc = QuantumCircuit(float_gate.num_qubits)
 
         param_qc.append(param_gate, param_qc.qubits)
         float_qc.append(float_gate, float_qc.qubits)