Repository: rigetti/qcs-sdk-rust (language: python)

Issue:
pyo3_runtime.PanicException: `spawn_local` called from outside of a `task::LocalSet`
Received the above error while testing [PyExecutable::execute_on_qvm](https://github.com/rigetti/qcs-sdk-rust/blob/41c5b0112172077a46f61acd445b974fb725bce5/crates/python/src/executable.rs#L96) by adding the following test to [tests/test_api.py](https://github.com/rigetti/qcs-sdk-rust/blob/main/crates/python/tests/test_api.py):

```py
@pytest.mark.asyncio
async def test_translate_exe(native_bitflip_program: str):
    exe = qcs_sdk.Executable(native_bitflip_program)
    results = await exe.execute_on_qvm()
``` 
pyo3_runtime.PanicException: `spawn_local` called from outside of a `task::LocalSet`
Received the above error while testing [PyExecutable::execute_on_qvm](https://github.com/rigetti/qcs-sdk-rust/blob/41c5b0112172077a46f61acd445b974fb725bce5/crates/python/src/executable.rs#L96) by adding the following test to [tests/test_api.py](https://github.com/rigetti/qcs-sdk-rust/blob/main/crates/python/tests/test_api.py):

```py
@pytest.mark.asyncio
async def test_translate_exe(native_bitflip_program: str):
    exe = qcs_sdk.Executable(native_bitflip_program)
    results = await exe.execute_on_qvm()
``` 


Discussion / hints:
Related to https://github.com/rigetti/pyquil/issues/1473
Related to https://github.com/rigetti/pyquil/issues/1473

Fix patch:
diff --git a/.gitignore b/.gitignore
index 63a113838..00cc29e11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,5 @@ qcs-api/docs
 
 # Python artifacts
 **/__pycache__
+crates/python/.hypothesis
 crates/python/qcs_sdk/*.so
diff --git a/Cargo.lock b/Cargo.lock
index cd17bed41..b7a59fc2f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1471,6 +1471,7 @@ name = "qcs-sdk-python"
 version = "0.5.0-rc.7"
 dependencies = [
  "numpy",
+ "paste",
  "pyo3",
  "pyo3-asyncio",
  "pyo3-build-config",
diff --git a/crates/lib/examples/delayed_job_retrieval.rs b/crates/lib/examples/delayed_job_retrieval.rs
index aec7fc164..7447064b4 100644
--- a/crates/lib/examples/delayed_job_retrieval.rs
+++ b/crates/lib/examples/delayed_job_retrieval.rs
@@ -12,9 +12,9 @@ MEASURE 0 ro[0]
 async fn main() {
     let mut exe = Executable::from_quil(PROGRAM);
     let job_handle = exe
-        .submit_to_qpu("Aspen-11")
+        .submit_to_qpu("Aspen-M-3")
         .await
-        .expect("Program should be sucessfully submitted for execution");
+        .expect("Program should be successfully submitted for execution");
     // Do some other stuff
     let _data = exe
         .retrieve_results(job_handle)
diff --git a/crates/lib/src/executable.rs b/crates/lib/src/executable.rs
index 8f272ef1c..5adf8ef1a 100644
--- a/crates/lib/src/executable.rs
+++ b/crates/lib/src/executable.rs
@@ -690,7 +690,7 @@ mod describe_qpu_for_id {
             qpu::Execution::new(
                 "".into(),
                 shots,
-                "Aspen-11",
+                "Aspen-M-3",
                 exe.get_config().await.unwrap_or_default(),
                 exe.compile_with_quilc,
             )
@@ -700,7 +700,7 @@ mod describe_qpu_for_id {
         // Load config with no credentials to prevent creating a new Execution if it tries
         exe.config = Some(Arc::new(Configuration::default()));
 
-        assert!(exe.qpu_for_id("Aspen-11").await.is_ok());
+        assert!(exe.qpu_for_id("Aspen-M-3").await.is_ok());
     }
 
     #[tokio::test]
diff --git a/crates/python/Cargo.toml b/crates/python/Cargo.toml
index 569c05f35..e6e2d071d 100644
--- a/crates/python/Cargo.toml
+++ b/crates/python/Cargo.toml
@@ -28,6 +28,7 @@ serde_json.workspace = true
 tokio.workspace = true
 numpy.workspace = true
 rigetti-pyo3.workspace = true
+paste = "1.0.11"
 
 [build-dependencies]
 pyo3-build-config.workspace = true
diff --git a/crates/python/Makefile.toml b/crates/python/Makefile.toml
index f3a9cc1c2..015dea412 100644
--- a/crates/python/Makefile.toml
+++ b/crates/python/Makefile.toml
@@ -28,4 +28,4 @@ dependencies = [
     "pre-test-install-lib",
     "test",
     "post-test",
-]
\ No newline at end of file
+]
diff --git a/crates/python/qcs_sdk/__init__.pyi b/crates/python/qcs_sdk/__init__.pyi
index 8369f07dd..04821bc52 100644
--- a/crates/python/qcs_sdk/__init__.pyi
+++ b/crates/python/qcs_sdk/__init__.pyi
@@ -1,8 +1,26 @@
-from .api import *
+from .api import (
+    compile as compile,
+    compile_async as compile_async,
+    rewrite_arithmetic as rewrite_arithmetic,
+    build_patch_values as build_patch_values,
+    translate as translate,
+    translate_async as translate_async,
+    submit as submit,
+    submit_async as submit_async,
+    retrieve_results as retrieve_results,
+    retrieve_results_async as retrieve_results_async,
+    get_quilc_version as get_quilc_version,
+    get_quilc_version_async as get_quilc_version_async,
+    list_quantum_processors as list_quantum_processors,
+    list_quantum_processors_async as list_quantum_processors_async,
+)
 
-from .qpu.client import QcsClient as QcsClient
+from .qpu.client import (
+    QCSClient as QCSClient,
+)
 from .qpu.isa import (
     get_instruction_set_architecture as get_instruction_set_architecture,
+    get_instruction_set_architecture_async as get_instruction_set_architecture_async,
 )
 
 from ._execution_data import (
@@ -17,7 +35,7 @@ from ._executable import (
     Executable as Executable,
     ExeParameter as ExeParameter,
     JobHandle as JobHandle,
-    QcsExecutionError as QcsExecutionError,
+    QCSExecutionError as QCSExecutionError,
     Service as Service,
 )
 
diff --git a/crates/python/qcs_sdk/_executable.pyi b/crates/python/qcs_sdk/_executable.pyi
index 696f9f392..40811ba8b 100644
--- a/crates/python/qcs_sdk/_executable.pyi
+++ b/crates/python/qcs_sdk/_executable.pyi
@@ -8,7 +8,7 @@ from typing import Dict, List, Optional
 from .qpu.quilc import CompilerOpts
 from ._execution_data import ExecutionData
 
-class QcsExecutionError(RuntimeError):
+class QCSExecutionError(RuntimeError):
     """Error encounteted when executing programs."""
 
     ...
@@ -22,28 +22,58 @@ class Executable:
         compile_with_quilc: Optional[bool] = None,
         compiler_options: Optional[CompilerOpts] = None,
     ) -> "Executable": ...
-    async def execute_on_qvm(self) -> ExecutionData:
+    def execute_on_qvm(self) -> ExecutionData:
         """
         Execute on a QVM which must be available at the configured URL (default http://localhost:5000).
 
         Raises:
-            - ``QcsExecutionError``: If the job fails to execute.
+            - ``QCSExecutionError``: If the job fails to execute.
         """
         ...
-    async def execute_on_qpu(self, quantum_processor_id: str) -> ExecutionData:
+    async def execute_on_qvm_async(self) -> ExecutionData:
+        """
+        Async version of ``execute_on_qvm``.
+
+        Execute on a QVM which must be available at the configured URL (default http://localhost:5000).
+
+        Raises:
+            - ``QCSExecutionError``: If the job fails to execute.
+        """
+        ...
+    def execute_on_qpu(self, quantum_processor_id: str) -> ExecutionData:
         """
         Compile the program and execute it on a QPU, waiting for results.
 
         Raises:
-            - ``QcsExecutionError``: If the job fails to execute.
+            - ``QCSExecutionError``: If the job fails to execute.
         """
         ...
-    async def retrieve_results(self, job_handle: JobHandle) -> ExecutionData:
+    async def execute_on_qpu_async(self, quantum_processor_id: str) -> ExecutionData:
         """
+        Async version of ``execute_on_qvm``.
+
+        Compile the program and execute it on a QPU, waiting for results.
+
+        Raises:
+            - ``QCSExecutionError``: If the job fails to execute.
+        """
+        ...
+    def retrieve_results(self, job_handle: JobHandle) -> ExecutionData:
+        """
+        Wait for the results of a job to complete.
+
+        Raises:
+            - ``QCSExecutionError``: If there is a problem constructing job results.
+        """
+        ...
+    async def retrieve_results_async(self, job_handle: JobHandle) -> ExecutionData:
+        """
+        Async version of ``retrieve_results``.
+
         Wait for the results of a job to complete.
 
         Raises:
-            - ``QcsExecutionError``: If there is a problem constructing job results.
+            - ``QCSExecutionError``: If there is a problem constructing job results.
         """
         ...
 
@@ -95,6 +125,6 @@ class ExeParameter:
 
 class Service(Enum):
     Quilc = "Quilc"
-    Qvm = "Qvm"
-    Qcs = "Qcs"
-    Qpu = "Qpu"
+    QVM = "QVM"
+    QCS = "QCS"
+    QPU = "QPU"
diff --git a/crates/python/qcs_sdk/api.pyi b/crates/python/qcs_sdk/api.pyi
index f956603fb..2948fb1c8 100644
--- a/crates/python/qcs_sdk/api.pyi
+++ b/crates/python/qcs_sdk/api.pyi
@@ -5,7 +5,7 @@ from numbers import Number
 from typing import Dict, List, Optional
 
 from .qpu.isa import InstructionSetArchitecture
-from .qpu.client import QcsClient
+from .qpu.client import QCSClient
 
 RecalculationTable = List[str]
 Memory = Dict[str, List[float]]
@@ -31,17 +31,25 @@ class RewriteArithmeticError(RuntimeError):
 
     ...
 
-class DeviceIsaError(ValueError):
+class DeviceISAError(ValueError):
     """Error while building Instruction Set Architecture."""
 
     ...
 
-class QcsGetQuiltCalibrationsError(RuntimeError):
+class QCSListQuantumProcessorsError(RuntimeError):
+    """Error while listing available Quantum Processors."""
+    ...
+
+class QCSSubmitError(RuntimeError):
+    """Errors while submitting a program for execution."""
+    ...
+
+class QCSGetQuiltCalibrationsError(RuntimeError):
     """Error while fetching Quil-T calibrations."""
 
     ...
 
-class RewriteArithmeticResults:
+class RewriteArithmeticResult:
     """
     The result of a call to [`rewrite_arithmetic`] which provides the information necessary to later patch-in memory values to a compiled program.
     """
@@ -188,10 +196,10 @@ class QuiltCalibrations:
     @settings_timestamp.setter
     def settings_timestamp(self, value: Optional[str]): ...
 
-async def compile(
+def compile(
     quil: str,
     target_device: str,
-    client: Optional[QcsClient] = None,
+    client: Optional[QCSClient] = None,
     *,
     timeout: int = 30,
 ) -> str:
@@ -201,7 +209,7 @@ async def compile(
     Args:
         quil: A Quil program.
         target_device: A JSON encoded description of the Quantum Abstract Machine Architecture.
-        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
 
     Keyword Args:
         timeout: The number of seconds to wait before timing out. If set to None, there is no timeout (default: 30).
@@ -211,7 +219,37 @@ async def compile(
 
     Raises:
         - ``LoadError`` If there is an issue loading the QCS Client configuration.
-        - ``DeviceIsaError`` If the `target_device` is misconfigured.
+        - ``DeviceISAError`` If the `target_device` is misconfigured.
+        - ``CompilationError`` If the program could not compile.
+    """
+    ...
+
+async def compile_async(
+    quil: str,
+    target_device: str,
+    client: Optional[QCSClient] = None,
+    *,
+    timeout: int = 30,
+) -> str:
+    """
+    Async version of ``compile``.
+
+    Uses quilc to convert a quil program to native Quil.
+
+    Args:
+        quil: A Quil program.
+        target_device: A JSON encoded description of the Quantum Abstract Machine Architecture.
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+
+    Keyword Args:
+        timeout: The number of seconds to wait before timing out. If set to None, there is no timeout (default: 30).
+
+    Returns:
+        An ``Awaitable`` that resolves to the native Quil program.
+
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``DeviceISAError`` If the `target_device` is misconfigured.
         - ``CompilationError`` If the program could not compile.
     """
     ...
@@ -255,20 +293,46 @@ def build_patch_values(
     """
     ...
 
-async def translate(
+def translate(
+    native_quil: str,
+    num_shots: int,
+    quantum_processor_id: str,
+    client: Optional[QCSClient] = None,
+) -> TranslationResult:
+    """
+    Translates a native Quil program into an executable.
+
+    Args:
+        native_quil: A Quil program.
+        num_shots: The number of shots to perform.
+        quantum_processor_id: The ID of the quantum processor the executable will run on (e.g. "Aspen-M-2").
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+
+    Returns:
+        An Awaitable that resolves to a dictionary with the compiled program, memory descriptors, and readout sources (see `TranslationResult`).
+
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``TranslationError`` If the `native_quil` program could not be translated.
+    """
+    ...
+
+async def translate_async(
     native_quil: str,
     num_shots: int,
     quantum_processor_id: str,
-    client: Optional[QcsClient] = None,
+    client: Optional[QCSClient] = None,
 ) -> TranslationResult:
     """
+    Async version of ``translate``
+
     Translates a native Quil program into an executable.
 
     Args:
         native_quil: A Quil program.
         num_shots: The number of shots to perform.
         quantum_processor_id: The ID of the quantum processor the executable will run on (e.g. "Aspen-M-2").
-        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
 
     Returns:
         An Awaitable that resolves to a dictionary with the compiled program, memory descriptors, and readout sources (see `TranslationResult`).
@@ -279,11 +343,11 @@ async def translate(
     """
     ...
 
-async def submit(
+def submit(
     program: str,
     patch_values: Dict[str, List[float]],
     quantum_processor_id: str,
-    client: Optional[QcsClient] = None,
+    client: Optional[QCSClient] = None,
 ) -> str:
     """
     Submits an executable `program` to be run on the specified QPU.
@@ -292,7 +356,7 @@ async def submit(
         program: An executable program (see `translate`).
         patch_values: A mapping of symbols to their desired values (see `build_patch_values`).
         quantum_processor_id: The ID of the quantum processor to run the executable on.
-        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
 
     Returns:
         An Awaitable that resolves to the ID of the submitted job.
@@ -303,18 +367,44 @@ async def submit(
     """
     ...
 
-async def retrieve_results(
+async def submit_async(
+    program: str,
+    patch_values: Dict[str, List[float]],
+    quantum_processor_id: str,
+    client: Optional[QCSClient] = None,
+) -> str:
+    """
+    Async version of ``submit``.
+
+    Submits an executable `program` to be run on the specified QPU.
+
+    Args:
+        program: An executable program (see `translate`).
+        patch_values: A mapping of symbols to their desired values (see `build_patch_values`).
+        quantum_processor_id: The ID of the quantum processor to run the executable on.
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+
+    Returns:
+        An Awaitable that resolves to the ID of the submitted job.
+
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``ExecutionError`` If there was a problem during program execution.
+    """
+    ...
+
+def retrieve_results(
     job_id: str,
     quantum_processor_id: str,
-    client: Optional[QcsClient] = None,
+    client: Optional[QCSClient] = None,
 ) -> ExecutionResults:
     """
     Fetches results for the corresponding job ID.
 
     Args:
         job_id: The ID of the job to retrieve results for.
-        quantum_processor_id: The ID of the quanutum processor the job ran on.
-        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        quantum_processor_id: The ID of the quantum processor the job ran on.
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
 
     Returns:
         An Awaitable that resolves to a dictionary describing the results of the execution and its duration (see `ExecutionResults`).
@@ -325,14 +415,38 @@ async def retrieve_results(
     """
     ...
 
-async def get_quilc_version(
-    client: Optional[QcsClient] = None,
+async def retrieve_results_async(
+    job_id: str,
+    quantum_processor_id: str,
+    client: Optional[QCSClient] = None,
+) -> ExecutionResults:
+    """
+    Async version of ``retrieve_results``
+
+    Fetches results for the corresponding job ID.
+
+    Args:
+        job_id: The ID of the job to retrieve results for.
+        quantum_processor_id: The ID of the quantum processor the job ran on.
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+
+    Returns:
+        An Awaitable that resolves to a dictionary describing the results of the execution and its duration (see `ExecutionResults`).
+
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``ExecutionError`` If there was a problem fetching execution results.
+    """
+    ...
+
+def get_quilc_version(
+    client: Optional[QCSClient] = None,
 ) -> str:
     """
     Returns the version number of the running quilc server.
 
     Args:
-        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
 
     Raises:
         - ``LoadError`` If there is an issue loading the QCS Client configuration.
@@ -340,47 +454,87 @@ async def get_quilc_version(
     """
     ...
 
-async def list_quantum_processors(
-    client: Optional[QcsClient] = None,
+async def get_quilc_version_async(
+    client: Optional[QCSClient] = None,
+) -> str:
+    """
+    Async version of ``get_quilc_version``
+
+    Returns the version number of the running quilc server.
+
+    Args:
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``CompilationError`` If there is an issue fetching the version from the quilc compiler.
+    """
+    ...
+
+def list_quantum_processors(
+    client: Optional[QCSClient] = None,
+    timeout: Optional[float] = None,
+) -> List[str]:
+    """
+    Returns all available Quantum Processor IDs.
+
+    Args:
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        timeout: Maximum duration to wait for API calls to complete, in seconds.
+    """
+    ...
+
+async def list_quantum_processors_async(
+    client: Optional[QCSClient] = None,
     timeout: Optional[float] = None,
 ) -> List[str]:
     """
+    Async version of ``list_quantum_processors``
+
     Returns all available Quantum Processor IDs.
 
     Args:
-        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
         timeout: Maximum duration to wait for API calls to complete, in seconds.
     """
     ...
 
-async def get_quilt_calibrations(
-    client: Optional[QcsClient] = None,
+def get_quilt_calibrations(
+    quantum_processor_id: str,
+    client: Optional[QCSClient] = None,
     timeout: Optional[float] = None,
 ) -> QuiltCalibrations:
     """
     Retrieve the calibration data used for client-side Quil-T generation.
 
     Args:
-        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        quantum_processor_id: The ID of the quantum processor the job ran on.
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
         timeout: Maximum duration to wait for API calls to complete, in seconds.
 
     Raises:
         - ``LoadError`` If there is an issue loading the QCS Client configuration.
-        - ``QcsGetQuiltCalibrationsError`` If there was a problem fetching Quil-T calibrations.
+        - ``QCSGetQuiltCalibrationsError`` If there was a problem fetching Quil-T calibrations.
     """
     ...
 
-async def get_instruction_set_architecture(
-    quantum_processor_id: str, client: Optional[QcsClient]
-) -> InstructionSetArchitecture:
+async def get_quilt_calibrations_async(
+    quantum_processor_id: str,
+    client: Optional[QCSClient] = None,
+    timeout: Optional[float] = None,
+) -> QuiltCalibrations:
     """
-    Retrieve the InstructionSetArchitecture (ISA) for the given Quantum Processor ID.
+    Async version of `get_quilt_calibrations``
+
+    Retrieve the calibration data used for client-side Quil-T generation.
 
     Args:
-        quantum_processor_id: The ID of the quantum processor
-        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        quantum_processor_id: The ID of the quantum processor the job ran on.
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        timeout: Maximum duration to wait for API calls to complete, in seconds.
 
     Raises:
-        - ``IsaError`` if there was a problem fetching the ISA.
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``QCSGetQuiltCalibrationsError`` If there was a problem fetching Quil-T calibrations.
     """
     ...
diff --git a/crates/python/qcs_sdk/qpu/__init__.pyi b/crates/python/qcs_sdk/qpu/__init__.pyi
index b9da7d6f1..7b1675551 100644
--- a/crates/python/qcs_sdk/qpu/__init__.pyi
+++ b/crates/python/qcs_sdk/qpu/__init__.pyi
@@ -1,6 +1,6 @@
+from .isa import QCSISAError as QCSISAError
 from .result_data import (
     QPUResultData as QPUResultData,
     ReadoutValues as ReadoutValues,
 )
 
-class QcsIsaError(RuntimeError): ...
diff --git a/crates/python/qcs_sdk/qpu/client.pyi b/crates/python/qcs_sdk/qpu/client.pyi
index 87cbd762a..fb792f084 100644
--- a/crates/python/qcs_sdk/qpu/client.pyi
+++ b/crates/python/qcs_sdk/qpu/client.pyi
@@ -1,32 +1,45 @@
 from typing import Optional
 
-class QcsClient:
+class QCSClient:
     """
     Configuration for connecting and authenticating to QCS API resources.
     """
 
     def __new__(
         cls,
-        tokens: Optional[QcsClientTokens] = None,
+        tokens: Optional[QCSClientTokens] = None,
         api_url: Optional[str] = None,
-        auth_server: Optional[QcsClientAuthServer] = None,
+        auth_server: Optional[QCSClientAuthServer] = None,
         grpc_api_url: Optional[str] = None,
         quilc_url: Optional[str] = None,
         qvm_url: Optional[str] = None,
-    ) -> "QcsClient":
+    ) -> "QCSClient":
         """
         Construct a client from scratch.
 
-        Use ``QcsClient.load`` to construct an environment-based profile.
+        Use ``QCSClient.load`` to construct an environment-based profile.
         """
         ...
     @staticmethod
-    async def load(
+    def load(
         profile_name: Optional[str] = None,
         use_gateway: Optional[bool] = None,
-    ) -> "QcsClient":
+    ) -> "QCSClient":
         """
-        Load a QcsClient configuration using an environment-based configuration.
+        Create a ``QCSClient`` configuration using an environment-based configuration.
+
+        See for details: https://docs.rigetti.com/qcs/references/qcs-client-configuration#environment-variables-and-configuration-files
+        """
+        ...
+    @staticmethod
+    async def load_async(
+        profile_name: Optional[str] = None,
+        use_gateway: Optional[bool] = None,
+    ) -> "QCSClient":
+        """
+        Async version of ``QCSClient.load()``
+
+        Create a ``QCSClient`` configuration using an environment-based configuration.
 
         See for details: https://docs.rigetti.com/qcs/references/qcs-client-configuration#environment-variables-and-configuration-files
         """
@@ -48,7 +61,7 @@ class QcsClient:
         """URL to access the QVM."""
         ...
 
-class QcsClientAuthServer:
+class QCSClientAuthServer:
     """Authentication server configuration for the QCS API."""
 
     def __init__(self, client_id: str, issuer: str): ...
@@ -61,7 +74,7 @@ class QcsClientAuthServer:
     @issuer.setter
     def issuer(self, value: str): ...
 
-class QcsClientTokens:
+class QCSClientTokens:
     """Authentication tokens for the QCS API."""
 
     def __init__(self, bearer_access_token: str, refresh_token: str): ...
@@ -74,14 +87,17 @@ class QcsClientTokens:
     @refresh_token.setter
     def refresh_token(self, value: Optional[str]): ...
 
-class QcsGrpcClientError(RuntimeError):
+class QCSGrpcClientError(RuntimeError):
     """Error encountered while loading a QCS gRPC API client."""
 
-class QcsGrpcEndpointError(RuntimeError):
+class QCSGrpcEndpointError(RuntimeError):
     """Error when trying to resolve the QCS gRPC API endpoint."""
 
-class QcsGrpcError(RuntimeError):
+class QCSGrpcError(RuntimeError):
     """Error during QCS gRPC API requests."""
 
-class QcsLoadError(RuntimeError):
+class QCSLoadError(RuntimeError):
     """Error encountered while loading the QCS API client configuration."""
+
+class QCSConfigurationBuildError(RuntimeError):
+    """Error encountered while building the QCS API client configuration."""
\ No newline at end of file
diff --git a/crates/python/qcs_sdk/qpu/isa.pyi b/crates/python/qcs_sdk/qpu/isa.pyi
index 7ae54a0c8..8669ef4d4 100644
--- a/crates/python/qcs_sdk/qpu/isa.pyi
+++ b/crates/python/qcs_sdk/qpu/isa.pyi
@@ -1,21 +1,21 @@
 from enum import Enum
 from typing import List, Optional
 
-from .client import QcsClient
+from .client import QCSClient
 
-class QcsIsaSerializationError(ValueError):
-    """Raised when an `InstructionSetArchitecture` is not able to be serialized from a given json string."""
+class QCSISASerializationError(ValueError):
+    """Raised when an ``InstructionSetArchitecture`` is not able to be serialized from a given json string."""
     ...
 
 
-class QcsIsaError(RuntimeError):
-    """Raised when there is a problem fetching `InstructionSetArchitecture` from the QCS API."""
+class QCSISAError(RuntimeError):
+    """Raised when there is a problem fetching ``InstructionSetArchitecture`` from the QCS API."""
     ...
 
 
 class Family(Enum):
     """
-    The architecture family identifier of an `InstructionSetArchitecture`.
+    The architecture family identifier of an ``InstructionSetArchitecture``.
 
     Value "Full"  implies that each node is connected to every other (fully-connected architecture).
     """
@@ -30,9 +30,9 @@ class Node:
     """
     A logical node in the quantum processor's architecture.
 
-    The existence of a node in the ISA `Architecture` does not necessarily mean that a given 1Q
+    The existence of a node in the ISA ``Architecture`` does not necessarily mean that a given 1Q
     operation will be available on the node. This information is conveyed by the presence of the
-    specific `node_id` in instances of `Instruction`.
+    specific `node_id` in instances of ``Instruction``.
     """
 
     @property
@@ -50,9 +50,9 @@ class Edge:
     """
     A degree-two logical connection in the quantum processor's architecture.
 
-    The existence of an edge in the ISA `Architecture` does not necessarily mean that a given 2Q
+    The existence of an edge in the ISA ``Architecture`` does not necessarily mean that a given 2Q
     operation will be available on the edge. This information is conveyed by the presence of the
-    two `node_id` values in instances of `Instruction`.
+    two `node_id` values in instances of ``Instruction``.
 
     Note that edges are undirected in this model. Thus edge :math:`(a, b)` is equivalent to edge
     :math:`(b, a)`.
@@ -223,8 +223,8 @@ class Architecture:
     7 qubits wide and 12 tall. This architecture may also be presented in "landscape"
     orientation, which is a simple 90-degree clockwise rotation of the vertical orientation. 
 
-    Note that the operations that are actually available are defined entirely by `Operation`
-    instances. The presence of a node or edge in the `Architecture` model provides no guarantee
+    Note that the operations that are actually available are defined entirely by ``Operation``
+    instances. The presence of a node or edge in the ``Architecture`` model provides no guarantee
     that any 1Q or 2Q operation will be available to users writing QUIL programs.
     """
 
@@ -293,20 +293,45 @@ class InstructionSetArchitecture:
 
     @staticmethod
     def from_raw(value: str) -> "InstructionSetArchitecture":
-        """Deserialize an `InstructionSetArchitecture` from a json representation."""
+        """Deserialize an ``InstructionSetArchitecture`` from a json representation."""
         ...
     
     def json(self, pretty: bool = False) -> str:
-        """Serialize the `InstructionSetArchitecture` to a json string, optionally pretty-printed."""
+        """Serialize the ``InstructionSetArchitecture`` to a json string, optionally pretty-printed."""
         ...
 
+def get_instruction_set_architecture(
+    quantum_processor_id: str,
+    client: QCSClient = ...,
+) -> InstructionSetArchitecture:
+    """
+    Fetch the ``InstructionSetArchitecture`` (ISA) for a given `quantum_processor_id` from the QCS API.
+
+    Args:
+        quantum_processor_id: The ID of the quantum processor
+        client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+
+    Raises:
+        - ``QCSLoadError`` if there is an issue with the QCS API client configuration.
+        - ``QCSISAError`` if there is an issue fetching the ISA from the QCS API.
+    """
+    ...
 
-async def get_instruction_set_architecture(quantum_processor_id: str, client: QcsClient = ...) -> InstructionSetArchitecture:
+async def get_instruction_set_architecture_async(
+    quantum_processor_id: str,
+    client: QCSClient = ...,
+) -> InstructionSetArchitecture:
     """
-    Fetch the `InstructionSetArchitecture` (ISA) for a given `quantum_processor_id` from the QCS API.
+    Async verion of ``get_instruction_set_architecture``
+
+    Fetch the ``InstructionSetArchitecture`` (ISA) for a given `quantum_processor_id` from the QCS API.
+
+    Args:
+        quantum_processor_id: The ID of the quantum processor
+        client: The ``QCSClient`` to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
 
     Raises:
-        - `QcsLoadError` if there is an issue with the QCS API client configuration.
-        - `QcsIsaError` if there is an issue fetching the ISA from the QCS API.
+        - ``QCSLoadError`` if there is an issue with the QCS API client configuration.
+        - ``QCSISAError`` if there is an issue fetching the ISA from the QCS API.
     """
     ...
diff --git a/crates/python/qcs_sdk/qpu/quilc.pyi b/crates/python/qcs_sdk/qpu/quilc.pyi
index a8141c05b..c334d6dd7 100644
--- a/crates/python/qcs_sdk/qpu/quilc.pyi
+++ b/crates/python/qcs_sdk/qpu/quilc.pyi
@@ -1,6 +1,7 @@
 from typing import Optional
 
 DEFAULT_COMPILER_TIMEOUT: int
+"""Number of seconds to wait before timing out."""
 
 class QuilcError(RuntimeError): ...
 
diff --git a/crates/python/qcs_sdk/qpu/result_data.pyi b/crates/python/qcs_sdk/qpu/result_data.pyi
index 1c0d3b432..dd69d748f 100644
--- a/crates/python/qcs_sdk/qpu/result_data.pyi
+++ b/crates/python/qcs_sdk/qpu/result_data.pyi
@@ -1,3 +1,7 @@
+"""
+Do not import this file, it has no exports.
+It is only here to represent the structure of the rust source code 1:1
+"""
 from typing import Dict, List, Optional
 
 class ReadoutValues:
diff --git a/crates/python/src/api.rs b/crates/python/src/api.rs
index 34627e2ff..7e33e0565 100644
--- a/crates/python/src/api.rs
+++ b/crates/python/src/api.rs
@@ -10,8 +10,8 @@ use pyo3::{
 };
 use qcs::{
     api::{
-        get_quilt_calibrations, list_quantum_processors, ExecutionResult, ExecutionResults,
-        Register, RewriteArithmeticResult, TranslationResult,
+        self, ExecutionResult, ExecutionResults, Register, RewriteArithmeticResult,
+        TranslationResult,
     },
     qpu::quilc::{CompilerOpts, TargetDevice, DEFAULT_COMPILER_TIMEOUT},
 };
@@ -21,7 +21,10 @@ use rigetti_pyo3::{
     py_wrap_union_enum, wrap_error, ToPython, ToPythonError,
 };
 
-use crate::qpu::client::PyQcsClient;
+use crate::{
+    py_sync::{py_async, py_function_sync_async, py_sync},
+    qpu::client::PyQcsClient,
+};
 
 create_init_submodule! {
     classes: [
@@ -29,28 +32,36 @@ create_init_submodule! {
         PyExecutionResults,
         PyRegister,
         PyRewriteArithmeticResult,
-        PyTranslationResult
+        PyTranslationResult,
+        PyQuiltCalibrations
     ],
     errors: [
         ExecutionError,
         TranslationError,
         CompilationError,
         RewriteArithmeticError,
-        DeviceIsaError,
-        QcsListQuantumProcessorsError,
-        QcsSubmitError,
-        QcsGetQuiltCalibrationsError
+        DeviceISAError,
+        QCSListQuantumProcessorsError,
+        QCSSubmitError,
+        QCSGetQuiltCalibrationsError
     ],
     funcs: [
-        compile,
+        py_compile,
+        py_compile_async,
         rewrite_arithmetic,
-        translate,
-        submit,
-        retrieve_results,
         build_patch_values,
-        get_quilc_version,
+        py_translate,
+        py_translate_async,
+        py_submit,
+        py_submit_async,
+        py_retrieve_results,
+        py_retrieve_results_async,
+        py_get_quilc_version,
+        py_get_quilc_version_async,
         py_list_quantum_processors,
-        py_get_quilt_calibrations
+        py_list_quantum_processors_async,
+        py_get_quilt_calibrations,
+        py_get_quilt_calibrations_async
     ],
 }
 
@@ -98,10 +109,10 @@ create_exception!(qcs, ExecutionError, PyRuntimeError);
 create_exception!(qcs, TranslationError, PyRuntimeError);
 create_exception!(qcs, CompilationError, PyRuntimeError);
 create_exception!(qcs, RewriteArithmeticError, PyRuntimeError);
-create_exception!(qcs, DeviceIsaError, PyValueError);
+create_exception!(qcs, DeviceISAError, PyValueError);
 
 wrap_error!(SubmitError(qcs::api::SubmitError));
-py_wrap_error!(api, SubmitError, QcsSubmitError, PyRuntimeError);
+py_wrap_error!(api, SubmitError, QCSSubmitError, PyRuntimeError);
 
 wrap_error!(ListQuantumProcessorsError(
     qcs::api::ListQuantumProcessorsError
@@ -109,7 +120,7 @@ wrap_error!(ListQuantumProcessorsError(
 py_wrap_error!(
     api,
     ListQuantumProcessorsError,
-    QcsListQuantumProcessorsError,
+    QCSListQuantumProcessorsError,
     PyRuntimeError
 );
 
@@ -119,7 +130,7 @@ wrap_error!(GetQuiltCalibrationsError(
 py_wrap_error!(
     api,
     GetQuiltCalibrationsError,
-    QcsGetQuiltCalibrationsError,
+    QCSGetQuiltCalibrationsError,
     PyRuntimeError
 );
 
@@ -130,29 +141,53 @@ fn get_kwd<'a, T: FromPyObject<'a>>(kwds: Option<&'a PyDict>, key: &str) -> PyRe
         .map_or(Ok(None), PyAny::extract::<Option<T>>)
 }
 
-#[pyfunction(client = "None", kwds = "**")]
-pub fn compile<'a>(
-    py: Python<'a>,
+/// Because `PyDict` does not implement `Send`, we must build `CompilerOpts`
+/// before having python wrappers invoke the private `compile` function.
+fn get_compiler_opts(kwds: Option<&PyDict>) -> PyResult<CompilerOpts> {
+    let compiler_timeout = get_kwd(kwds, "timeout")?.or(Some(DEFAULT_COMPILER_TIMEOUT));
+    let protoquil: Option<bool> = get_kwd(kwds, "protoquil")?;
+    Ok(CompilerOpts::default()
+        .with_timeout(compiler_timeout)
+        .with_protoquil(protoquil))
+}
+
+async fn compile(
     quil: String,
     target_device: String,
     client: Option<PyQcsClient>,
-    kwds: Option<&PyDict>,
-) -> PyResult<&'a PyAny> {
+    options: CompilerOpts,
+) -> PyResult<String> {
     let target_device: TargetDevice =
-        serde_json::from_str(&target_device).map_err(|e| DeviceIsaError::new_err(e.to_string()))?;
+        serde_json::from_str(&target_device).map_err(|e| DeviceISAError::new_err(e.to_string()))?;
 
-    let compiler_timeout = get_kwd(kwds, "timeout")?.or(Some(DEFAULT_COMPILER_TIMEOUT));
-    let protoquil: Option<bool> = get_kwd(kwds, "protoquil")?;
+    let client = PyQcsClient::get_or_create_client(client).await?;
+    qcs::api::compile(&quil, target_device, &client, options)
+        .map_err(|e| CompilationError::new_err(e.to_string()))
+}
 
-    pyo3_asyncio::tokio::future_into_py(py, async move {
-        let client = PyQcsClient::get_or_create_client(client).await?;
-        let options = CompilerOpts::default()
-            .with_timeout(compiler_timeout)
-            .with_protoquil(protoquil);
-        let result = qcs::api::compile(&quil, target_device, &client, options)
-            .map_err(|e| CompilationError::new_err(e.to_string()))?;
-        Ok(result)
-    })
+#[pyfunction(client = "None", kwds = "**")]
+#[pyo3(name = "compile")]
+pub fn py_compile(
+    quil: String,
+    target_device: String,
+    client: Option<PyQcsClient>,
+    kwds: Option<&PyDict>,
+) -> PyResult<String> {
+    let options = get_compiler_opts(kwds)?;
+    py_sync!(compile(quil, target_device, client, options))
+}
+
+#[pyfunction(client = "None", kwds = "**")]
+#[pyo3(name = "compile_async")]
+pub fn py_compile_async<'py>(
+    py: Python<'py>,
+    quil: String,
+    target_device: String,
+    client: Option<PyQcsClient>,
+    kwds: Option<&'py PyDict>,
+) -> PyResult<&'py PyAny> {
+    let options = get_compiler_opts(kwds)?;
+    py_async!(py, compile(quil, target_device, client, options))
 }
 
 #[pyfunction]
@@ -185,100 +220,88 @@ pub fn build_patch_values(
         .to_python(py)
 }
 
-#[pyfunction(client = "None")]
-pub fn translate(
-    py: Python<'_>,
-    native_quil: String,
-    num_shots: u16,
-    quantum_processor_id: String,
-    client: Option<PyQcsClient>,
-) -> PyResult<&PyAny> {
-    pyo3_asyncio::tokio::future_into_py(py, async move {
+py_function_sync_async! {
+    #[pyfunction(client = "None")]
+    async fn translate(
+        native_quil: String,
+        num_shots: u16,
+        quantum_processor_id: String,
+        client: Option<PyQcsClient>,
+    ) -> PyResult<PyTranslationResult> {
         let client = PyQcsClient::get_or_create_client(client).await?;
-        let result = qcs::api::translate(&native_quil, num_shots, &quantum_processor_id, &client)
+        qcs::api::translate(&native_quil, num_shots, &quantum_processor_id, &client)
             .await
-            .map_err(|e| TranslationError::new_err(e.to_string()))?;
-        Python::with_gil(|py| PyTranslationResult::from(result).to_python(py))
-    })
+            .map(PyTranslationResult::from)
+            .map_err(|e| TranslationError::new_err(e.to_string()))
+    }
 }
 
-#[pyfunction(client = "None")]
-pub fn submit(
-    py: Python<'_>,
-    program: String,
-    patch_values: HashMap<String, Vec<f64>>,
-    quantum_processor_id: String,
-    client: Option<PyQcsClient>,
-) -> PyResult<&PyAny> {
-    pyo3_asyncio::tokio::future_into_py(py, async move {
+py_function_sync_async! {
+    #[pyfunction(client = "None")]
+    async fn submit(
+        program: String,
+        patch_values: HashMap<String, Vec<f64>>,
+        quantum_processor_id: String,
+        client: Option<PyQcsClient>,
+    ) -> PyResult<String> {
         let client = PyQcsClient::get_or_create_client(client).await?;
-        let job_id = qcs::api::submit(&program, patch_values, &quantum_processor_id, &client)
+        qcs::api::submit(&program, patch_values, &quantum_processor_id, &client)
             .await
-            .map_err(|e| ExecutionError::new_err(e.to_string()))?;
-        Ok(Python::with_gil(|_py| job_id))
-    })
+            .map_err(|e| ExecutionError::new_err(e.to_string()))
+    }
 }
 
-#[pyfunction(client = "None")]
-pub fn retrieve_results(
-    py: Python<'_>,
-    job_id: String,
-    quantum_processor_id: String,
-    client: Option<PyQcsClient>,
-) -> PyResult<&PyAny> {
-    pyo3_asyncio::tokio::future_into_py(py, async move {
+py_function_sync_async! {
+    #[pyfunction(client = "None")]
+    async fn retrieve_results(
+        job_id: String,
+        quantum_processor_id: String,
+        client: Option<PyQcsClient>,
+    ) -> PyResult<PyExecutionResults> {
         let client = PyQcsClient::get_or_create_client(client).await?;
-        let results = qcs::api::retrieve_results(&job_id, &quantum_processor_id, &client)
+        qcs::api::retrieve_results(&job_id, &quantum_processor_id, &client)
             .await
-            .map_err(|e| ExecutionError::new_err(e.to_string()))?;
-        Ok(PyExecutionResults::from(results))
-    })
+            .map(PyExecutionResults::from)
+            .map_err(|e| ExecutionError::new_err(e.to_string()))
+    }
 }
 
-#[pyfunction(client = "None")]
-pub fn get_quilc_version(py: Python<'_>, client: Option<PyQcsClient>) -> PyResult<&PyAny> {
-    pyo3_asyncio::tokio::future_into_py(py, async move {
+py_function_sync_async! {
+    #[pyfunction(client = "None")]
+    async fn get_quilc_version(client: Option<PyQcsClient>) -> PyResult<String> {
         let client = PyQcsClient::get_or_create_client(client).await?;
-        let version = qcs::api::get_quilc_version(&client)
-            .map_err(|e| CompilationError::new_err(e.to_string()))?;
-        Ok(version)
-    })
+        qcs::api::get_quilc_version(&client).map_err(|e| CompilationError::new_err(e.to_string()))
+    }
 }
 
-#[pyfunction(client = "None", timeout = "None")]
-#[pyo3(name = "list_quantum_processors")]
-pub fn py_list_quantum_processors(
-    py: Python<'_>,
-    client: Option<PyQcsClient>,
-    timeout: Option<f64>,
-) -> PyResult<&PyAny> {
-    pyo3_asyncio::tokio::future_into_py(py, async move {
+py_function_sync_async! {
+    #[pyfunction(client = "None", timeout = "None")]
+    async fn list_quantum_processors(
+        client: Option<PyQcsClient>,
+        timeout: Option<f64>,
+    ) -> PyResult<Vec<String>> {
         let client = PyQcsClient::get_or_create_client(client).await?;
         let timeout = timeout.map(Duration::from_secs_f64);
-        let names = list_quantum_processors(&client, timeout)
+        api::list_quantum_processors(&client, timeout)
             .await
             .map_err(ListQuantumProcessorsError::from)
-            .map_err(ListQuantumProcessorsError::to_py_err)?;
-        Ok(names)
-    })
+            .map_err(ListQuantumProcessorsError::to_py_err)
+    }
 }
 
-#[pyfunction(client = "None", timeout = "None")]
-#[pyo3(name = "get_quilt_calibrations")]
-pub fn py_get_quilt_calibrations(
-    py: Python<'_>,
-    quantum_processor_id: String,
-    client: Option<PyQcsClient>,
-    timeout: Option<f64>,
-) -> PyResult<&PyAny> {
-    pyo3_asyncio::tokio::future_into_py(py, async move {
+py_function_sync_async! {
+    #[pyfunction(client = "None", timeout = "None")]
+    async fn get_quilt_calibrations(
+        quantum_processor_id: String,
+        client: Option<PyQcsClient>,
+        timeout: Option<f64>,
+    ) -> PyResult<PyQuiltCalibrations> {
         let client = PyQcsClient::get_or_create_client(client).await?;
         let timeout = timeout.map(Duration::from_secs_f64);
-        let result = get_quilt_calibrations(&quantum_processor_id, &client, timeout)
+        api::get_quilt_calibrations(&quantum_processor_id, &client, timeout)
             .await
             .map(PyQuiltCalibrations::from)
             .map_err(GetQuiltCalibrationsError::from)
-            .map_err(GetQuiltCalibrationsError::to_py_err)?;
-        Ok(result)
-    })
+            .map_err(GetQuiltCalibrationsError::to_py_err)
+    }
 }
diff --git a/crates/python/src/executable.rs b/crates/python/src/executable.rs
index 5e44f7940..0a9e7529b 100644
--- a/crates/python/src/executable.rs
+++ b/crates/python/src/executable.rs
@@ -9,14 +9,18 @@ use rigetti_pyo3::{
 };
 use tokio::sync::Mutex;
 
-use crate::qpu::quilc::PyCompilerOpts;
+use crate::{
+    execution_data::PyExecutionData,
+    py_sync::{py_async, py_sync},
+    qpu::quilc::PyCompilerOpts,
+};
 
 wrap_error!(ExecutionError(Error));
 
 py_wrap_error!(
     executable,
     ExecutionError,
-    QcsExecutionError,
+    QCSExecutionError,
     PyRuntimeError
 );
 
@@ -49,6 +53,24 @@ impl PyParameter {
     }
 }
 
+/// Invoke a PyExecutable's inner Executable::method with given arguments,
+/// then mapped to `Future<Output = Result<PyExecutionData, ExecutionError>>`
+macro_rules! py_executable_data {
+    ($self: ident, $method: ident $(, $arg: expr)* $(,)?) => {{
+        let arc = $self.as_inner().clone();
+        async move {
+            arc.lock()
+                .await
+                .$method($($arg),*)
+                .await
+                .map(ExecutionData::from)
+                .map(PyExecutionData::from)
+                .map_err(ExecutionError::from)
+                .map_err(ExecutionError::to_py_err)
+        }
+    }};
+}
+
 #[pymethods]
 impl PyExecutable {
     #[new]
@@ -93,63 +115,59 @@ impl PyExecutable {
         Self::from(Arc::new(Mutex::new(exe)))
     }
 
-    pub fn execute_on_qvm<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> {
-        let arc = self.as_inner().clone();
-        pyo3_asyncio::tokio::local_future_into_py(py, async move {
-            arc.lock()
-                .await
-                .execute_on_qvm()
-                .await
-                .map(ExecutionData::from)
-                .map(|qvm| Python::with_gil(|py| qvm.to_python(py)))
-                .map_err(ExecutionError::from)
-                .map_err(ExecutionError::to_py_err)?
-        })
+    pub fn execute_on_qvm(&self) -> PyResult<PyExecutionData> {
+        py_sync!(py_executable_data!(self, execute_on_qvm))
+    }
+
+    pub fn execute_on_qvm_async<'py>(&'py self, py: Python<'py>) -> PyResult<&PyAny> {
+        py_async!(py, py_executable_data!(self, execute_on_qvm))
+    }
+
+    pub fn execute_on_qpu(&self, quantum_processor_id: String) -> PyResult<PyExecutionData> {
+        py_sync!(py_executable_data!(
+            self,
+            execute_on_qpu,
+            quantum_processor_id,
+        ))
     }
 
-    pub fn execute_on_qpu<'py>(
-        &self,
+    pub fn execute_on_qpu_async<'py>(
+        &'py self,
         py: Python<'py>,
         quantum_processor_id: String,
-    ) -> PyResult<&'py PyAny> {
-        let arc = self.as_inner().clone();
-        pyo3_asyncio::tokio::local_future_into_py(py, async move {
-            arc.lock()
-                .await
-                .execute_on_qpu(quantum_processor_id)
-                .await
-                .map(ExecutionData::from)
-                .map(|qpu| Python::with_gil(|py| qpu.to_python(py)))
-                .map_err(ExecutionError::from)
-                .map_err(ExecutionError::to_py_err)?
-        })
+    ) -> PyResult<&PyAny> {
+        py_async!(
+            py,
+            py_executable_data!(self, execute_on_qpu, quantum_processor_id)
+        )
     }
 
-    pub fn retrieve_results<'py>(
-        &mut self,
+    pub fn retrieve_results(&mut self, job_handle: PyJobHandle) -> PyResult<PyExecutionData> {
+        py_sync!(py_executable_data!(
+            self,
+            retrieve_results,
+            job_handle.into(),
+        ))
+    }
+
+    pub fn retrieve_results_async<'py>(
+        &'py mut self,
         py: Python<'py>,
         job_handle: PyJobHandle,
-    ) -> PyResult<&'py PyAny> {
-        let arc = self.as_inner().clone();
-        pyo3_asyncio::tokio::local_future_into_py(py, async move {
-            arc.lock()
-                .await
-                .retrieve_results(job_handle.into_inner())
-                .await
-                .map(ExecutionData::from)
-                .map(|qpu| Python::with_gil(|py| qpu.to_python(py)))
-                .map_err(ExecutionError::from)
-                .map_err(ExecutionError::to_py_err)?
-        })
+    ) -> PyResult<&PyAny> {
+        py_async!(
+            py,
+            py_executable_data!(self, retrieve_results, job_handle.into())
+        )
     }
 }
 
 py_wrap_simple_enum! {
     PyService(Service) as "Service" {
-        Quilc,
-        Qvm,
-        Qcs,
-        Qpu
+        Quilc as Quilc,
+        Qvm as QVM,
+        Qcs as QCS,
+        Qpu as QPU
     }
 }
 
diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs
index 5805aceec..dc877dc19 100644
--- a/crates/python/src/lib.rs
+++ b/crates/python/src/lib.rs
@@ -1,7 +1,7 @@
 use pyo3::prelude::*;
 use rigetti_pyo3::create_init_submodule;
 
-use executable::QcsExecutionError;
+use executable::QCSExecutionError;
 
 pub mod api;
 pub mod executable;
@@ -11,6 +11,8 @@ pub mod qpu;
 pub mod qvm;
 pub mod register_data;
 
+pub(crate) mod py_sync;
+
 create_init_submodule! {
     classes: [
         execution_data::PyExecutionData,
@@ -25,19 +27,26 @@ create_init_submodule! {
         qpu::client::PyQcsClient
     ],
     errors: [
-        QcsExecutionError,
+        QCSExecutionError,
         execution_data::PyRegisterMatrixConversionError
     ],
     funcs: [
-        api::compile,
+        api::py_compile,
+        api::py_compile_async,
         api::rewrite_arithmetic,
-        api::translate,
-        api::submit,
-        api::retrieve_results,
+        api::py_translate,
+        api::py_translate_async,
+        api::py_submit,
+        api::py_submit_async,
+        api::py_retrieve_results,
+        api::py_retrieve_results_async,
         api::build_patch_values,
-        api::get_quilc_version,
+        api::py_get_quilc_version,
+        api::py_get_quilc_version_async,
         api::py_list_quantum_processors,
-        qpu::isa::py_get_instruction_set_architecture
+        api::py_list_quantum_processors_async,
+        qpu::isa::py_get_instruction_set_architecture,
+        qpu::isa::py_get_instruction_set_architecture_async
     ],
     submodules: [
         "api": api::init_submodule,
diff --git a/crates/python/src/py_sync.rs b/crates/python/src/py_sync.rs
new file mode 100644
index 000000000..a62a2c4b0
--- /dev/null
+++ b/crates/python/src/py_sync.rs
@@ -0,0 +1,100 @@
+/// Spawn and block on a future using the pyo3 tokio runtime.
+/// Useful for returning a synchronous `PyResult`.
+///
+///
+/// When used like the following:
+/// ```rs
+/// async fn say_hello(name: String) -> String {
+///     format!("hello {name}")
+/// }
+///
+/// #[pyo3(name="say_hello")]
+/// pub fn py_say_hello(name: String) -> PyResult<String> {
+///     py_sync!(say_hello(name))
+/// }
+/// ```
+///
+/// Becomes the associated "synchronous" python call:
+/// ```py
+/// assert say_hello("Rigetti") == "hello Rigetti"
+/// ```
+macro_rules! py_sync {
+    ($body: expr) => {{
+        let runtime = ::pyo3_asyncio::tokio::get_runtime();
+        let handle = runtime.spawn($body);
+        runtime
+            .block_on(handle)
+            .map_err(|err| ::pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))?
+    }};
+}
+
+/// Convert a rust future into a Python awaitable using
+/// `pyo3_asyncio::tokio::future_into_py`
+macro_rules! py_async {
+    ($py: ident, $body: expr) => {
+        ::pyo3_asyncio::tokio::future_into_py($py, $body)
+    };
+}
+
+/// Given a single implementation of an async function,
+/// create that function as private and two pyfunctions
+/// named after it that can be used to invoke either
+/// blocking or async variants of the same function.
+///
+/// This macro cannot be used when lifetime specifiers are
+/// required, or the pyfunction bodies need additional
+/// parameter handling besides simply calling out to
+/// the underlying `py_async` or `py_sync` macros.
+///
+/// ```rs
+/// // ... becomes python package "things"
+/// create_init_submodule! {
+///     funcs: [
+///         py_do_thing,
+///         py_do_thing_async,
+///     ]
+/// }
+///
+/// py_function_sync_async! {
+///     #[args(timeout = "None")]
+///     async fn do_thing(timeout: Option<u64>) -> PyResult<String> {
+///         // ... sleep for timeout ...
+///         Ok(String::from("done"))
+///     }
+/// }
+/// ```
+///
+/// becomes in python:
+/// ```py
+/// from things import do_thing, do_thing_async
+/// assert do_thing() == "done"
+/// assert await do_thing_async() == "done"
+/// ```
+macro_rules! py_function_sync_async {
+    (
+        $(#[$meta: meta])+
+        async fn $name: ident($($arg: ident : $kind: ty),* $(,)?) $(-> $ret: ty)? $body: block
+    ) => {
+        async fn $name($($arg: $kind,)*) $(-> $ret)? {
+            $body
+        }
+
+        ::paste::paste! {
+        $(#[$meta])+
+        #[pyo3(name = $name "")]
+        pub fn [< py_ $name >]($($arg: $kind),*) $(-> $ret)? {
+            $crate::py_sync::py_sync!($name($($arg),*))
+        }
+
+        $(#[$meta])+
+        #[pyo3(name = $name "_async")]
+        pub fn [< py_ $name _async >](py: ::pyo3::Python<'_> $(, $arg: $kind)*) -> ::pyo3::PyResult<&::pyo3::PyAny> {
+            $crate::py_sync::py_async!(py, $name($($arg),*))
+        }
+        }
+    };
+}
+
+pub(crate) use py_async;
+pub(crate) use py_function_sync_async;
+pub(crate) use py_sync;
diff --git a/crates/python/src/qpu/client.rs b/crates/python/src/qpu/client.rs
index fa35acd9f..9c94e3d8c 100644
--- a/crates/python/src/qpu/client.rs
+++ b/crates/python/src/qpu/client.rs
@@ -1,4 +1,3 @@
-use pyo3_asyncio::tokio::future_into_py;
 use qcs_api_client_common::{
     configuration::{AuthServer, BuildError, ClientConfigurationBuilder, Tokens},
     ClientConfiguration,
@@ -14,6 +13,8 @@ use rigetti_pyo3::{
 
 use qcs::qpu::Qcs;
 
+use crate::py_sync::{py_async, py_sync};
+
 create_init_submodule! {
     classes: [
         PyQcsClient,
@@ -21,27 +22,28 @@ create_init_submodule! {
         PyQcsClientTokens
     ],
     errors: [
-        QcsGrpcClientError,
-        QcsGrpcEndpointError,
-        QcsGrpcError,
-        QcsLoadError
+        QCSGrpcClientError,
+        QCSGrpcEndpointError,
+        QCSGrpcError,
+        QCSLoadError,
+        QCSConfigurationBuildError
     ],
 }
 
 wrap_error! {
     LoadError(qcs::qpu::client::LoadError);
 }
-py_wrap_error!(client, LoadError, QcsLoadError, PyRuntimeError);
+py_wrap_error!(client, LoadError, QCSLoadError, PyRuntimeError);
 
 wrap_error! {
     GrpcError(qcs::qpu::client::GrpcError);
 }
-py_wrap_error!(client, GrpcError, QcsGrpcError, PyRuntimeError);
+py_wrap_error!(client, GrpcError, QCSGrpcError, PyRuntimeError);
 
 wrap_error! {
     GrpcClientError(qcs::qpu::client::GrpcClientError);
 }
-py_wrap_error!(client, GrpcClientError, QcsGrpcClientError, PyRuntimeError);
+py_wrap_error!(client, GrpcClientError, QCSGrpcClientError, PyRuntimeError);
 
 wrap_error! {
     GrpcEndpointError(qcs::qpu::client::GrpcEndpointError);
@@ -49,7 +51,7 @@ wrap_error! {
 py_wrap_error!(
     client,
     GrpcEndpointError,
-    QcsGrpcEndpointError,
+    QCSGrpcEndpointError,
     PyRuntimeError
 );
 
@@ -57,13 +59,13 @@ wrap_error!(ConfigurationBuildError(BuildError));
 py_wrap_error!(
     qcs,
     ConfigurationBuildError,
-    QcsConfigurationBuildError,
+    QCSConfigurationBuildError,
     PyRuntimeError
 );
 
 /// The fields on qcs_api_client_common::client::AuthServer are not public.
 #[pyclass]
-#[pyo3(name = "QcsClientAuthServer")]
+#[pyo3(name = "QCSClientAuthServer")]
 #[derive(FromPyObject)]
 pub struct PyQcsClientAuthServer {
     #[pyo3(get, set)]
@@ -95,7 +97,7 @@ impl PyQcsClientAuthServer {
 }
 
 py_wrap_data_struct! {
-    PyQcsClientTokens(Tokens) as "QcsClientTokens" {
+    PyQcsClientTokens(Tokens) as "QCSClientTokens" {
         bearer_access_token: Option<String> => Option<Py<PyString>>,
         refresh_token: Option<String> => Option<Py<PyString>>
     }
@@ -114,7 +116,7 @@ impl PyQcsClientTokens {
 }
 
 py_wrap_type! {
-    PyQcsClient(Qcs) as "QcsClient";
+    PyQcsClient(Qcs) as "QCSClient";
 }
 
 impl PyQcsClient {
@@ -127,6 +129,25 @@ impl PyQcsClient {
                 .map_err(LoadError::to_py_err)?,
         })
     }
+
+    async fn load(profile_name: Option<String>, use_gateway: Option<bool>) -> PyResult<Self> {
+        let config = match profile_name {
+            Some(profile_name) => ClientConfiguration::load_profile(profile_name).await,
+            None => ClientConfiguration::load_default().await,
+        };
+
+        let client = config
+            .map(Qcs::with_config)
+            .map_err(LoadError)
+            .map_err(ToPythonError::to_py_err)?;
+
+        let client = match use_gateway {
+            None => client,
+            Some(use_gateway) => client.with_use_gateway(use_gateway),
+        };
+
+        Ok(Self(client))
+    }
 }
 
 impl PartialEq for PyQcsClient {
@@ -185,29 +206,20 @@ impl PyQcsClient {
 
     #[staticmethod]
     #[args("/", profile_name = "None", use_gateway = "None")]
-    pub fn load(
+    #[pyo3(name = "load")]
+    pub fn py_load(profile_name: Option<String>, use_gateway: Option<bool>) -> PyResult<Self> {
+        py_sync!(Self::load(profile_name, use_gateway))
+    }
+
+    #[staticmethod]
+    #[args("/", profile_name = "None", use_gateway = "None")]
+    #[pyo3(name = "load_async")]
+    pub fn py_load_async(
         py: Python<'_>,
         profile_name: Option<String>,
         use_gateway: Option<bool>,
     ) -> PyResult<&PyAny> {
-        future_into_py(py, async move {
-            let config = match profile_name {
-                Some(profile_name) => ClientConfiguration::load_profile(profile_name).await,
-                None => ClientConfiguration::load_default().await,
-            };
-
-            let client = config
-                .map(Qcs::with_config)
-                .map_err(LoadError)
-                .map_err(ToPythonError::to_py_err)?;
-
-            let client = match use_gateway {
-                None => client,
-                Some(use_gateway) => client.with_use_gateway(use_gateway),
-            };
-
-            Ok(Self(client))
-        })
+        py_async!(py, Self::load(profile_name, use_gateway))
     }
 
     #[getter]
diff --git a/crates/python/src/qpu/isa.rs b/crates/python/src/qpu/isa.rs
index ffdb90314..abc970965 100644
--- a/crates/python/src/qpu/isa.rs
+++ b/crates/python/src/qpu/isa.rs
@@ -8,14 +8,14 @@ use rigetti_pyo3::{
         exceptions::{PyRuntimeError, PyValueError},
         prelude::*,
         types::{PyFloat, PyInt, PyList, PyString},
-        Py, Python,
+        Py,
     },
     wrap_error, ToPythonError,
 };
 
 use qcs::qpu::get_isa;
 
-use crate::qpu::client::PyQcsClient;
+use crate::{py_sync::py_function_sync_async, qpu::client::PyQcsClient};
 
 create_init_submodule! {
     classes: [
@@ -30,11 +30,12 @@ create_init_submodule! {
         PyInstructionSetArchitecture
     ],
     errors: [
-        QcsIsaSerializationError,
-        QcsIsaError
+        QCSISASerializationError,
+        QCSISAError
     ],
     funcs: [
-        py_get_instruction_set_architecture
+        py_get_instruction_set_architecture,
+        py_get_instruction_set_architecture_async
     ],
 }
 
@@ -42,12 +43,12 @@ wrap_error!(IsaSerializationError(serde_json::Error));
 py_wrap_error!(
     api,
     IsaSerializationError,
-    QcsIsaSerializationError,
+    QCSISASerializationError,
     PyValueError
 );
 
 wrap_error!(IsaError(qcs::qpu::IsaError));
-py_wrap_error!(api, IsaError, QcsIsaError, PyRuntimeError);
+py_wrap_error!(api, IsaError, QCSISAError, PyRuntimeError);
 
 py_wrap_simple_enum! {
     PyFamily(Family) as "Family" {
@@ -146,14 +147,12 @@ impl PyInstructionSetArchitecture {
     }
 }
 
-#[pyfunction(client = "None")]
-#[pyo3(name = "get_instruction_set_architecture")]
-pub(crate) fn py_get_instruction_set_architecture(
-    py: Python<'_>,
-    quantum_processor_id: String,
-    client: Option<PyQcsClient>,
-) -> PyResult<&PyAny> {
-    pyo3_asyncio::tokio::future_into_py(py, async move {
+py_function_sync_async! {
+    #[pyfunction(client = "None")]
+    async fn get_instruction_set_architecture(
+        quantum_processor_id: String,
+        client: Option<PyQcsClient>,
+    ) -> PyResult<PyInstructionSetArchitecture> {
         let client = PyQcsClient::get_or_create_client(client).await?;
 
         get_isa(&quantum_processor_id, &client)
@@ -161,5 +160,5 @@ pub(crate) fn py_get_instruction_set_architecture(
             .map(PyInstructionSetArchitecture::from)
             .map_err(IsaError::from)
             .map_err(IsaError::to_py_err)
-    })
+    }
 }
diff --git a/crates/python/src/qpu/mod.rs b/crates/python/src/qpu/mod.rs
index 5c2928b62..c4215f317 100644
--- a/crates/python/src/qpu/mod.rs
+++ b/crates/python/src/qpu/mod.rs
@@ -1,5 +1,4 @@
-use pyo3::exceptions::PyRuntimeError;
-use rigetti_pyo3::{create_init_submodule, py_wrap_error, wrap_error};
+use rigetti_pyo3::create_init_submodule;
 
 pub use result_data::{PyQpuResultData, PyReadoutValues};
 
@@ -8,18 +7,14 @@ pub mod isa;
 pub mod quilc;
 mod result_data;
 
+use isa::QCSISAError;
+
 create_init_submodule! {
     classes: [PyQpuResultData, PyReadoutValues],
-    errors: [QcsIsaError],
+    errors: [QCSISAError],
     submodules: [
         "client": client::init_submodule,
         "isa": isa::init_submodule,
         "quilc": quilc::init_submodule
     ],
 }
-
-wrap_error! {
-    IsaError(qcs::qpu::IsaError);
-}
-
-py_wrap_error!(qpu, IsaError, QcsIsaError, PyRuntimeError);


Test patch:
diff --git a/crates/python/tests/test_api.py b/crates/python/tests/test_api.py
index f8104b0f0..720a934fa 100644
--- a/crates/python/tests/test_api.py
+++ b/crates/python/tests/test_api.py
@@ -6,24 +6,34 @@
 import qcs_sdk
 
 
-@pytest.mark.asyncio
-async def test_compile(native_bitflip_program: str, device_2q: str):
-    await qcs_sdk.compile(native_bitflip_program, device_2q)
+def test_compile(native_bitflip_program: str, device_2q: str):
+    qcs_sdk.compile(native_bitflip_program, device_2q)
 
 
-@pytest.mark.asyncio
 @pytest.mark.skip
-async def test_translate(native_bitflip_program: str, device_2q: str):
-    await qcs_sdk.translate(native_bitflip_program, 1, "Aspen-11")
-
+def test_translate(native_bitflip_program: str, device_2q: str):
+    qcs_sdk.translate(native_bitflip_program, 1, "Aspen-M-3")
 
 @pytest.mark.asyncio
+async def test_execute_qvm(bell_program: str):
+    shots = 1234
+    executable = qcs_sdk.Executable(bell_program, shots=shots)
+
+    response = executable.execute_on_qvm()
+    data = response.result_data.to_qvm().memory.get("ro").to_i8()
+    assert data == [[1, 1] for _ in range(shots)]
+
+    response = await executable.execute_on_qvm_async()
+    data = response.result_data.to_qvm().memory.get("ro").to_i8()
+    assert data == [[1, 1] for _ in range(shots)]
+
+
 @pytest.mark.skip
-async def test_execute(bell_program: str, device_2q):
-    compiled_program = await qcs_sdk.compile(bell_program, device_2q)
-    translation_result = await qcs_sdk.translate(compiled_program, 1, "Aspen-11")
-    job_id = await qcs_sdk.submit(translation_result["program"], {}, "Aspen-11")
-    await qcs_sdk.retrieve_results(job_id, "Aspen-M-3")
+def test_execute(bell_program: str, device_2q):
+    compiled_program = qcs_sdk.compile(bell_program, device_2q)
+    translation_result = qcs_sdk.translate(compiled_program, 1, "Aspen-M-3")
+    job_id = qcs_sdk.submit(translation_result["program"], {}, "Aspen-M-3")
+    qcs_sdk.retrieve_results(job_id, "Aspen-M-3")
 
 
 def test_rewrite_arithmetic():
@@ -57,12 +67,16 @@ def test_exe_parameters():
 
 @pytest.mark.asyncio
 async def test_get_quilc_version():
-    version = await qcs_sdk.get_quilc_version()
+    version = qcs_sdk.get_quilc_version()
     assert re.match(r"^([0-9]+)\.([0-9]+)\.([0-9]+)$", version)
 
+    assert version == await qcs_sdk.get_quilc_version_async()
+
 
-@pytest.mark.asyncio
 @pytest.mark.skip
+@pytest.mark.asyncio
 async def test_list_quantum_processors():
-    qpus = await qcs_sdk.list_quantum_processors()
+    qpus = qcs_sdk.list_quantum_processors()
     assert isinstance(qpus, list)
+
+    assert qpus == await qcs_sdk.list_quantum_processors_async()
diff --git a/crates/python/tests/test_client.py b/crates/python/tests/test_client.py
index b9ac4fea2..919dd3a4b 100644
--- a/crates/python/tests/test_client.py
+++ b/crates/python/tests/test_client.py
@@ -1,15 +1,14 @@
 import pytest
 from urllib.parse import urlparse
 
-from qcs_sdk import QcsClient
-from qcs_sdk.qpu.client import QcsLoadError, QcsClientAuthServer, QcsClientTokens
+from qcs_sdk import QCSClient
+from qcs_sdk.qpu.client import QCSLoadError, QCSClientAuthServer, QCSClientTokens
 
 @pytest.fixture
 def default_client():
-    return QcsClient()
+    return QCSClient()
 
-@pytest.mark.asyncio
-async def test_client_has_url_from_env(default_client: QcsClient):
+def test_client_has_url_from_env(default_client: QCSClient):
     """The default client is configured with valid urls."""
     assert urlparse(default_client.api_url).geturl() != ""
     assert urlparse(default_client.grpc_api_url).geturl() != ""
@@ -18,37 +17,37 @@ async def test_client_has_url_from_env(default_client: QcsClient):
 
 
 @pytest.mark.asyncio
-async def test_client_empty_profile_is_default(default_client: QcsClient):
+async def test_client_empty_profile_is_default(default_client: QCSClient):
     """The profile "empty" is configured to be similar to a default client."""
-    client = await QcsClient.load(profile_name="empty")
+    client = QCSClient.load(profile_name="empty")
 
     assert client == default_client
 
+    assert client == await QCSClient.load_async(profile_name="empty")
 
-@pytest.mark.asyncio
-async def test_client_default_profile_is_not_empty(default_client: QcsClient):
+
+def test_client_default_profile_is_not_empty(default_client: QCSClient):
     """The "default" profile is configured to have a token, unlike the default client."""
-    client = await QcsClient.load()
+    client = QCSClient.load()
 
     assert client != default_client
 
 
-@pytest.mark.asyncio
-async def test_client_broken_raises():
+def test_client_broken_raises():
     """Using a profile with broken configuration should surface the underlying error."""
-    with pytest.raises(QcsLoadError, match=r"Expected auth server .* but it didn't exist"):
-        await QcsClient.load(profile_name="broken")
+    with pytest.raises(QCSLoadError, match=r"Expected auth server .* but it didn't exist"):
+        QCSClient.load(profile_name="broken")
 
 
 def test_client_auth_server_can_be_manually_defined():
     """Ensures that pyo3 usage is correct."""
-    auth_server = QcsClientAuthServer(client_id="foo", issuer="bar")
+    auth_server = QCSClientAuthServer(client_id="foo", issuer="bar")
     assert auth_server.client_id == "foo"
     assert auth_server.issuer == "bar"
 
 
 def test_client_tokens_can_be_manually_defined():
     """Ensures that pyo3 usage is correct."""
-    auth_server = QcsClientTokens(bearer_access_token="foo", refresh_token="bar")
+    auth_server = QCSClientTokens(bearer_access_token="foo", refresh_token="bar")
     assert auth_server.bearer_access_token == "foo"
     assert auth_server.refresh_token == "bar"
diff --git a/crates/python/tests/test_isa.py b/crates/python/tests/test_isa.py
index d7e3b6be9..5572ff7f9 100644
--- a/crates/python/tests/test_isa.py
+++ b/crates/python/tests/test_isa.py
@@ -3,16 +3,16 @@
 from os import path
 import json
 
-from qcs_sdk import get_instruction_set_architecture
+from qcs_sdk import get_instruction_set_architecture, get_instruction_set_architecture_async
 from qcs_sdk.qpu.isa import InstructionSetArchitecture, Family
 
 
-def ignore_nones(value):
+def _ignore_nones(value):
     """Recursively ignore `None` values, useful for comparing json serializations."""
     if isinstance(value, list):
-        return [ignore_nones(x) for x in value if x is not None]
+        return [_ignore_nones(x) for x in value if x is not None]
     elif isinstance(value, dict):
-        return {key: ignore_nones(val) for key, val in value.items() if val is not None}
+        return {key: _ignore_nones(val) for key, val in value.items() if val is not None}
     else:
         return value
 
@@ -33,12 +33,16 @@ def test_isa_from_aspen_m_3_json(aspen_m_3_json: str):
     node_ids = {node.node_id for node in isa.architecture.nodes}
     assert len(node_ids) == 80
 
-    assert json.loads(isa.json()) == ignore_nones(json.loads(aspen_m_3_json))
+    assert json.loads(isa.json()) == _ignore_nones(json.loads(aspen_m_3_json))
 
 
-@pytest.mark.asyncio
 @pytest.mark.skip
+@pytest.mark.asyncio
 async def test_get_isa_aspen_m_3():
-    isa = await get_instruction_set_architecture("Aspen-M-3")
+    isa = get_instruction_set_architecture("Aspen-M-3")
+
+    assert isa.architecture.family == Family.Aspen
+
+    isa = await get_instruction_set_architecture_async("Aspen-M-3")
 
     assert isa.architecture.family == Family.Aspen