Repository: PennyLaneAI/pennylane
Pull request #4762 (2023-10-30 21:31:20)

Deprecate Observable.return_type
**Context:**
`Observable.return_type` is a relic of an older PennyLane, and should be removed.

**Description of the Change:**
- raise a warning whenever `Observable.return_type` is accessed
- Observable reprs (like Tensor) will never try to add the return type to it
- change CircuitGraph to expect measurements instead of observables
- funky stuff with `tape._par_info[x]` -> I kept the info["op"] to be the observable (and not the surrounding measurement) because a lot of code expects it to be an operator (fair enough!). whenever code needed the return type, I instead get the `op_idx` from the info, then access the tape at that index to get the measurement
- `var_param_shift` now checks if it's replacing a trainable hamiltonian and fails before passing to `expval_param_shift`. This was a weird one: `expval_param_shift` would receive something like `qml.expval(H)`, but `H.return_type` would still be `Variance` because we never updated it when swapping measurements. The new way is to just fail right away.

**Benefits:**
- the property has been deprecated!
- Kept it as minimal as possible while avoiding using the property in PL source

**Possible Drawbacks:**
To keep things minimal, there are still some arguments named "observables" when they are actually supposed to be measurements. I can change that too if we want.

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4762?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`02f982e`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/02f982eb1848ecfa882035c88393823749225488?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.64% compared to head [(`38b2861`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4762?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.64%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4762      +/-   ##
==========================================
- Coverage   99.64%   99.64%   -0.01%     
==========================================
  Files         380      380              
  Lines       34266    33998     -268     
==========================================
- Hits        34145    33876     -269     
- Misses        121      122       +1     
```


| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4762?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/\_qubit\_device.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4762?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19xdWJpdF9kZXZpY2UucHk=) | `99.44% <100.00%> (-0.01%)` | :arrow_down: |
| [pennylane/measurements/measurements.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4762?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmVtZW50cy9tZWFzdXJlbWVudHMucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/operation.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4762?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wZXJhdGlvbi5weQ==) | `97.25% <100.00%> (-0.04%)` | :arrow_down: |
| [pennylane/ops/qubit/hamiltonian.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4762?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9oYW1pbHRvbmlhbi5weQ==) | `100.00% <ø> (ø)` | |
| [pennylane/tape/qscript.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4762?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RhcGUvcXNjcmlwdC5weQ==) | `100.00% <100.00%> (ø)` | |

... and [38 files with indirect coverage changes](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4762/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4762?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

timmysilv: [sc-45858]

Patch:
diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst
index de52631ca..1a5bcf311 100644
--- a/doc/development/deprecations.rst
+++ b/doc/development/deprecations.rst
@@ -6,6 +6,12 @@ Deprecations
 Pending deprecations
 --------------------
 
+* `Observable.return_type` is deprecated. Instead, you should inspect the type
+  of the surrounding measurement process.
+
+  - Deprecated in v0.34
+  - Will be removed in v0.35
+
 * ``single_tape_transform``, ``batch_transform``, ``qfunc_transform``, and ``op_transform`` are
   deprecated. Instead switch to using the new ``qml.transform`` function. Please refer to
   `the transform docs <https://docs.pennylane.ai/en/stable/code/qml_transforms.html#custom-transforms>`_
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 83627789c..8994076ff 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -30,6 +30,10 @@
 * `qml.gradients.pulse_generator` has become `qml.gradients.pulse_odegen` to adhere to paper naming conventions.
   [(#4769)](https://github.com/PennyLaneAI/pennylane/pull/4769)
 
+* `QuantumScript.graph` is now built using `tape.measurements` instead of `tape.observables`
+  because it depended on the now-deprecated `Observable.return_type` property.
+  [(#4762)](https://github.com/PennyLaneAI/pennylane/pull/4762)
+
 <h3>Deprecations 👋</h3>
 
 * `QuantumScript.is_sampled` and `QuantumScript.all_sampled` are deprecated.
@@ -40,6 +44,10 @@
   Instead switch to using the new `qml.transform` function.
   [(#4774)](https://github.com/PennyLaneAI/pennylane/pull/4774)
 
+* `Observable.return_type` is deprecated. Instead, you should inspect the type
+  of the surrounding measurement process.
+  [(#4762)](https://github.com/PennyLaneAI/pennylane/pull/4762)
+
 <h3>Documentation 📝</h3>
 
 * Documentation page for `qml.measurements` now links top-level accessible functions (e.g. `qml.expval`) 
diff --git a/pennylane/_device.py b/pennylane/_device.py
index 853550cf0..95fc190b6 100644
--- a/pennylane/_device.py
+++ b/pennylane/_device.py
@@ -27,6 +27,7 @@ import numpy as np
 
 import pennylane as qml
 from pennylane.measurements import (
+    MeasurementProcess,
     CountsMP,
     Expectation,
     ExpectationMP,
@@ -458,28 +459,29 @@ class Device(abc.ABC):
 
             self.pre_measure()
 
-            for obs in observables:
+            for mp in observables:
+                obs = mp.obs if isinstance(mp, MeasurementProcess) and mp.obs is not None else mp
                 if isinstance(obs, Tensor):
                     wires = [ob.wires for ob in obs.obs]
                 else:
                     wires = obs.wires
 
-                if obs.return_type is Expectation:
+                if mp.return_type is Expectation:
                     results.append(self.expval(obs.name, wires, obs.parameters))
 
-                elif obs.return_type is Variance:
+                elif mp.return_type is Variance:
                     results.append(self.var(obs.name, wires, obs.parameters))
 
-                elif obs.return_type is Sample:
+                elif mp.return_type is Sample:
                     results.append(np.array(self.sample(obs.name, wires, obs.parameters)))
 
-                elif obs.return_type is Probability:
+                elif mp.return_type is Probability:
                     results.append(list(self.probability(wires=wires).values()))
 
-                elif obs.return_type is State:
+                elif mp.return_type is State:
                     raise qml.QuantumFunctionError("Returning the state is not supported")
 
-                elif obs.return_type is not None:
+                elif mp.return_type is not None:
                     raise qml.QuantumFunctionError(
                         f"Unsupported return type specified for observable {obs.name}"
                     )
@@ -499,9 +501,9 @@ class Device(abc.ABC):
 
             # Ensures that a combination with sample does not put
             # expvals and vars in superfluous arrays
-            if all(obs.return_type is Sample for obs in observables):
+            if all(mp.return_type is Sample for mp in observables):
                 return self._asarray(results)
-            if any(obs.return_type is Sample for obs in observables):
+            if any(mp.return_type is Sample for mp in observables):
                 return self._asarray(results, dtype="object")
 
             return self._asarray(results)
@@ -527,7 +529,7 @@ class Device(abc.ABC):
             # not start the next computation in the zero state
             self.reset()
 
-            res = self.execute(circuit.operations, circuit.observables)
+            res = self.execute(circuit.operations, circuit.measurements)
             results.append(res)
 
         if self.tracker.active:
diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py
index 2b052f7a5..7d4d7778d 100644
--- a/pennylane/_qubit_device.py
+++ b/pennylane/_qubit_device.py
@@ -35,7 +35,6 @@ from pennylane.interfaces import set_shots
 from pennylane.math import multiply as qmlmul
 from pennylane.math import sum as qmlsum
 from pennylane.measurements import (
-    AllCounts,
     ClassicalShadowMP,
     CountsMP,
     ExpectationMP,
@@ -614,14 +613,7 @@ class QubitDevice(Device):
 
         for m in measurements:
             # TODO: Remove this when all overriden measurements support the `MeasurementProcess` class
-            if m.obs is not None:
-                obs = m.obs
-                obs.return_type = m.return_type
-            elif m.mv is not None:
-                obs = m.mv
-                obs.return_type = m.return_type
-            else:
-                obs = m
+            obs = m.obs or m.mv or m
             # Check if there is an overriden version of the measurement process
             if method := getattr(self, self.measurement_map[type(m)], False):
                 if isinstance(m, MeasurementTransform):
@@ -641,7 +633,7 @@ class QubitDevice(Device):
                 result = self._asarray(qml.math.squeeze(samples))
 
             elif isinstance(m, CountsMP):
-                result = self.sample(obs, shot_range=shot_range, bin_size=bin_size, counts=True)
+                result = self.sample(m, shot_range=shot_range, bin_size=bin_size, counts=True)
 
             elif isinstance(m, ProbabilityMP):
                 result = self.probability(wires=obs.wires, shot_range=shot_range, bin_size=bin_size)
@@ -729,7 +721,7 @@ class QubitDevice(Device):
             elif isinstance(m, (SampleMeasurement, StateMeasurement)):
                 result = self._measure(m, shot_range=shot_range, bin_size=bin_size)
 
-            elif obs.return_type is not None:
+            elif m.return_type is not None:
                 raise qml.QuantumFunctionError(
                     f"Unsupported return type specified for observable {obs.name}"
                 )
@@ -1380,11 +1372,11 @@ class QubitDevice(Device):
         # TODO: do we need to squeeze here? Maybe remove with new return types
         return np.squeeze(np.var(samples, axis=axis))
 
-    def _samples_to_counts(self, samples, obs, num_wires):
+    def _samples_to_counts(self, samples, mp: CountsMP, num_wires):
         """Groups the samples into a dictionary showing number of occurences for
         each possible outcome.
 
-        The format of the dictionary depends on obs.return_type, which is set when
+        The format of the dictionary depends on mp.return_type, which is set when
         calling measurements.counts by setting the kwarg all_outcomes (bool). By default,
         the dictionary will only contain the observed outcomes. Optionally (all_outcomes=True)
         the dictionary will instead contain all possible outcomes, with a count of 0
@@ -1392,10 +1384,10 @@ class QubitDevice(Device):
 
 
         Args:
-            samples: An array of samples, with the shape being ``(shots,len(wires))`` if an observable
+            samples: An array of samples, with the shape being ``(shots,len(wires))`` if no observable
                 is provided, with sample values being an array of 0s or 1s for each wire. Otherwise, it
                 has shape ``(shots,)``, with sample values being scalar eigenvalues of the observable
-            obs (Observable): the observable sampled
+            mp (~.measurements.CountsMP): the measurement process sampled
             num_wires (int): number of wires the sampled observable was performed on
 
         Returns:
@@ -1404,17 +1396,14 @@ class QubitDevice(Device):
 
         **Example**
 
-            >>> samples
-            tensor([[0, 0],
-                    [0, 0],
-                    [1, 0]], requires_grad=True)
-
-            By default, this will return:
-            >>> self._samples_to_counts(samples, obs, num_wires)
+            >>> num_wires = 2
+            >>> dev = qml.device("default.qubit.legacy", wires=num_wires)
+            >>> mp = qml.counts()
+            >>> samples = pnp.array([[0, 0], [0, 0], [1, 0]])
+            >>> dev._samples_to_counts(samples, mp, num_wires)
             {'00': 2, '10': 1}
-
-            However, if obs.return_type is AllCounts, this will return:
-            >>> self._samples_to_counts(samples, obs, num_wires)
+            >>> mp = qml.counts(all_outcomes=True)
+            >>> dev._samples_to_counts(samples, mp, num_wires)
             {'00': 2, '01': 0, '10': 1, '11': 0}
 
             The variable all_outcomes can be set when running measurements.counts, i.e.:
@@ -1436,19 +1425,19 @@ class QubitDevice(Device):
         batched_ndims = 2
         shape = samples.shape
 
-        if isinstance(obs, CountsMP):
+        if mp.obs is None and mp.mv is None:
             # convert samples and outcomes (if using) from arrays to str for dict keys
             samples = np.array([sample for sample in samples if not np.any(np.isnan(sample))])
             samples = qml.math.cast_like(samples, qml.math.int8(0))
             samples = np.apply_along_axis(_sample_to_str, -1, samples)
             batched_ndims = 3  # no observable was provided, batched samples will have shape (batch_size, shots, len(wires))
-            if obs.all_outcomes:
+            if mp.all_outcomes:
                 outcomes = list(map(_sample_to_str, self.generate_basis_states(num_wires)))
-        elif obs.return_type is AllCounts:
+        elif mp.all_outcomes:
             outcomes = (
-                qml.eigvals(obs)
-                if not isinstance(obs, MeasurementValue)
-                else np.arange(0, 2 ** len(obs.wires), 1)
+                qml.eigvals(mp.obs)
+                if mp.obs is not None
+                else np.arange(0, 2 ** len(mp.mv.wires), 1)
             )
 
         batched = len(shape) == batched_ndims
@@ -1487,6 +1476,16 @@ class QubitDevice(Device):
             Union[array[float], dict, list[dict]]: samples in an array of
             dimension ``(shots,)`` or counts
         """
+        mp = observable
+        no_observable_provided = False
+        if isinstance(mp, MeasurementProcess):
+            if mp.obs is not None:
+                observable = mp.obs
+            elif mp.mv is not None:
+                observable = mp.mv
+            else:
+                no_observable_provided = True
+
         # translate to wire labels used by device
         device_wires = self.map_wires(observable.wires)
         name = observable.name
@@ -1499,8 +1498,6 @@ class QubitDevice(Device):
             # Ellipsis (...) otherwise would take up broadcasting and shots axes.
             sub_samples = self._samples[..., slice(*shot_range), :]
 
-        no_observable_provided = isinstance(observable, MeasurementProcess)
-
         if isinstance(name, str) and name in {"PauliX", "PauliY", "PauliZ", "Hadamard"}:
             # Process samples for observables with eigenvalues {1, -1}
             samples = 1 - 2 * sub_samples[..., device_wires[0]]
@@ -1534,13 +1531,13 @@ class QubitDevice(Device):
         num_wires = len(device_wires) if len(device_wires) > 0 else self.num_wires
         if bin_size is None:
             if counts:
-                return self._samples_to_counts(samples, observable, num_wires)
+                return self._samples_to_counts(samples, mp, num_wires)
             return samples
 
         if counts:
             shape = (-1, bin_size, num_wires) if no_observable_provided else (-1, bin_size)
             return [
-                self._samples_to_counts(bin_sample, observable, num_wires)
+                self._samples_to_counts(bin_sample, mp, num_wires)
                 for bin_sample in samples.reshape(shape)
             ]
 
@@ -1650,10 +1647,11 @@ class QubitDevice(Device):
         trainable_params = []
         for k in tape.trainable_params:
             # pylint: disable=protected-access
-            if hasattr(tape._par_info[k]["op"], "return_type"):
+            mp_or_op = tape[tape._par_info[k]["op_idx"]]
+            if isinstance(mp_or_op, MeasurementProcess):
                 warnings.warn(
                     "Differentiating with respect to the input parameters of "
-                    f"{tape._par_info[k]['op'].name} is not supported with the "
+                    f"{mp_or_op.obs.name} is not supported with the "
                     "adjoint differentiation method. Gradients are computed "
                     "only with regards to the trainable parameters of the circuit.\n\n Mark "
                     "the parameters of the measured observables as non-trainable "
diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py
index 8f31e0ae2..bf8edba6e 100644
--- a/pennylane/circuit_graph.py
+++ b/pennylane/circuit_graph.py
@@ -22,6 +22,7 @@ from collections import namedtuple
 import numpy as np
 import rustworkx as rx
 
+from pennylane.measurements import MeasurementProcess
 from pennylane.resource import ResourcesOperation
 
 
@@ -48,7 +49,7 @@ def _is_observable(x):
     Returns:
         bool: True iff x is an observable
     """
-    return getattr(x, "return_type", None) is not None
+    return isinstance(x, MeasurementProcess)
 
 
 Layer = namedtuple("Layer", ["ops", "param_inds"])
@@ -206,8 +207,9 @@ class CircuitGraph:
         # name of the operation and wires
         serialization_string += "|||"
 
-        for obs in self.observables_in_order:
-            serialization_string += str(obs.return_type)
+        for mp in self.observables_in_order:
+            obs = mp.obs or mp
+            serialization_string += str(mp.return_type)
             serialization_string += delimiter
             serialization_string += str(obs.name)
             for param in obs.data:
diff --git a/pennylane/devices/default_mixed.py b/pennylane/devices/default_mixed.py
index 7581f4fee..a9486e985 100644
--- a/pennylane/devices/default_mixed.py
+++ b/pennylane/devices/default_mixed.py
@@ -652,7 +652,7 @@ class DefaultMixed(QubitDevice):
         the ``QNode`` hash that can be used later for parametric compilation.
 
         Args:
-            circuit (~.CircuitGraph): circuit to execute on the device
+            circuit (QuantumTape): circuit to execute on the device
 
         Raises:
             QuantumFunctionError: if the value of :attr:`~.Observable.return_type` is not supported
diff --git a/pennylane/devices/preprocess.py b/pennylane/devices/preprocess.py
index 8b988048d..3bd5d433e 100644
--- a/pennylane/devices/preprocess.py
+++ b/pennylane/devices/preprocess.py
@@ -25,6 +25,7 @@ import pennylane as qml
 from pennylane import Snapshot
 from pennylane.operation import Tensor, StatePrepBase
 from pennylane.measurements import (
+    MeasurementProcess,
     StateMeasurement,
     SampleMeasurement,
 )
@@ -214,10 +215,11 @@ def warn_about_trainable_observables(
     """
 
     for k in tape.trainable_params:
-        if hasattr(tape._par_info[k]["op"], "return_type"):
+        mp_or_op = tape[tape._par_info[k]["op_idx"]]
+        if isinstance(mp_or_op, MeasurementProcess):
             warnings.warn(
                 "Differentiating with respect to the input parameters of "
-                f"{tape._par_info[k]['op'].name} is not supported with the "
+                f"{mp_or_op.obs.name} is not supported with the "
                 "adjoint differentiation method. Gradients are computed "
                 "only with regards to the trainable parameters of the circuit.\n\n Mark "
                 "the parameters of the measured observables as non-trainable "
diff --git a/pennylane/gradients/gradient_transform.py b/pennylane/gradients/gradient_transform.py
index 218a92260..3b8a12509 100644
--- a/pennylane/gradients/gradient_transform.py
+++ b/pennylane/gradients/gradient_transform.py
@@ -131,14 +131,17 @@ def _gradient_analysis(tape, use_graph=True, grad_fn=None):
             # non-trainable parameters do not require a grad_method
             info["grad_method"] = None
         else:
-            op = tape._par_info[idx]["op"]
+            par_info = tape._par_info[idx]
+            op = par_info["op"]
 
             if not qml.operation.has_grad_method(op):
                 # no differentiation method is registered for this operation
                 info["grad_method"] = None
 
             elif (tape._graph is not None) or use_graph:
-                if not any(tape.graph.has_path(op, ob) for ob in tape.observables):
+                # if `op` is an observable, we want the associated MP that's in the graph
+                op_or_mp = tape[par_info["op_idx"]]
+                if not any(tape.graph.has_path(op_or_mp, mp) for mp in tape.measurements):
                     # there is no influence of this operation on any of the observables
                     info["grad_method"] = "0"
                     continue
diff --git a/pennylane/gradients/parameter_shift.py b/pennylane/gradients/parameter_shift.py
index f058d700f..d0a333f9c 100644
--- a/pennylane/gradients/parameter_shift.py
+++ b/pennylane/gradients/parameter_shift.py
@@ -376,14 +376,14 @@ def expval_param_shift(
             gradient_data.append((0, [], None, None, 0))
             continue
 
-        op, *_ = tape.get_operation(idx)
+        op, op_idx, _ = tape.get_operation(idx)
 
         if op.name == "Hamiltonian":
             # operation is a Hamiltonian
-            if op.return_type is not qml.measurements.Expectation:
+            if tape[op_idx].return_type is not qml.measurements.Expectation:
                 raise ValueError(
                     "Can only differentiate Hamiltonian "
-                    f"coefficients for expectations, not {op.return_type.value}"
+                    f"coefficients for expectations, not {tape[op_idx].return_type.value}"
                 )
 
             g_tapes, h_fn = qml.gradients.hamiltonian_grad(tape, idx)
@@ -671,6 +671,16 @@ def var_param_shift(tape, argnum, shifts=None, gradient_recipes=None, f0=None, b
     for i in var_indices:
         obs = expval_tape.measurements[i].obs
         expval_tape._measurements[i] = qml.expval(op=obs)
+        if obs.name == "Hamiltonian":
+            first_obs_idx = len(expval_tape.operations)
+            for t_idx in reversed(range(len(expval_tape.trainable_params))):
+                op, op_idx, _ = expval_tape.get_operation(t_idx)
+                if op_idx < first_obs_idx:
+                    break  # already seen all observables
+                if op is obs:
+                    raise ValueError(
+                        "Can only differentiate Hamiltonian coefficients for expectations, not variances"
+                    )
 
     # evaluate the analytic derivative of <A>
     pdA_tapes, pdA_fn = expval_param_shift(
diff --git a/pennylane/gradients/parameter_shift_cv.py b/pennylane/gradients/parameter_shift_cv.py
index 369fbf2fc..bd5349da6 100644
--- a/pennylane/gradients/parameter_shift_cv.py
+++ b/pennylane/gradients/parameter_shift_cv.py
@@ -24,7 +24,13 @@ import warnings
 import numpy as np
 
 import pennylane as qml
-from pennylane.measurements import ExpectationMP, ProbabilityMP, StateMP, VarianceMP
+from pennylane.measurements import (
+    ExpectationMP,
+    ProbabilityMP,
+    StateMP,
+    VarianceMP,
+    MeasurementProcess,
+)
 from pennylane import transform
 from pennylane.transforms.tape_expand import expand_invalid_trainable
 from pennylane.gradients.gradient_transform import _contract_qjac_with_cjac
@@ -54,7 +60,8 @@ def _grad_method(tape, idx):
             or ``"0"`` (constant parameter).
     """
 
-    op = tape._par_info[idx]["op"]
+    par_info = tape._par_info[idx]
+    op = par_info["op"]
 
     if op.grad_method in (None, "F"):
         return op.grad_method
@@ -74,7 +81,8 @@ def _grad_method(tape, idx):
             continue
 
         # get the set of operations betweens the operation and the observable
-        ops_between = tape.graph.nodes_between(op, m.obs)
+        op_or_mp = tape[par_info["op_idx"]]
+        ops_between = tape.graph.nodes_between(op_or_mp, m)
 
         if not ops_between:
             # if there is no path between the operation and the observable,
@@ -86,6 +94,7 @@ def _grad_method(tape, idx):
         # intervening gates, and the type of the observable.
         best_method = "A"
 
+        ops_between = [o.obs if isinstance(o, MeasurementProcess) else o for o in ops_between]
         if any(not k.supports_heisenberg for k in ops_between):
             # non-Gaussian operators present in-between the operation
             # and the observable. Must fallback to numeric differentiation.
@@ -392,7 +401,8 @@ def second_order_param_shift(tape, dev_wires, argnum=None, shifts=None, gradient
         # transform the descendant observables into their derivatives using Z
         transformed_obs_idx = []
 
-        for obs in observable_descendents:
+        for mp in observable_descendents:
+            obs = mp if mp.obs is None else mp.obs
             # get the index of the descendent observable
             # pylint:disable=undefined-loop-variable
             for obs_idx, tape_obs in enumerate(tape.observables):
diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py
index 3b01a5217..a8f1a6358 100644
--- a/pennylane/measurements/measurements.py
+++ b/pennylane/measurements/measurements.py
@@ -291,10 +291,7 @@ class MeasurementProcess(ABC):
             return f"{self.return_type.value}(eigvals={self._eigvals}, wires={self.wires.tolist()})"
 
         # Todo: when tape is core the return type will always be taken from the MeasurementProcess
-        if getattr(self.obs, "return_type", None) is None:
-            return f"{self.return_type.value}({self.obs})"
-
-        return f"{self.obs}"
+        return f"{self.return_type.value}({self.obs})"
 
     def __copy__(self):
         cls = self.__class__
diff --git a/pennylane/operation.py b/pennylane/operation.py
index bb4dcf7c2..6cc951f5b 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -1853,6 +1853,8 @@ class Observable(Operator):
             can be useful for some applications where the instance has to be identified
     """
 
+    _return_type = None
+
     @property
     def _queue_category(self):
         """Used for sorting objects into their respective lists in `QuantumTape` objects.
@@ -1875,22 +1877,15 @@ class Observable(Operator):
         """All observables must be hermitian"""
         return True
 
-    # pylint: disable=abstract-method
-    return_type = None
-    """None or ObservableReturnTypes: Measurement type that this observable is called with."""
-
-    def __repr__(self):
-        """Constructor-call-like representation."""
-        temp = super().__repr__()
-
-        if self.return_type is None:
-            return temp
-
-        return (
-            f"{repr(self.return_type)}(wires={self.wires.tolist()})"
-            if self.return_type is qml.measurements.Probability
-            else f"{repr(self.return_type)}({temp})"
+    @property
+    def return_type(self):
+        """None or ObservableReturnTypes: Measurement type that this observable is called with."""
+        warnings.warn(
+            "`Observable.return_type` is deprecated. Instead, you should "
+            "inspect the type of the surrounding measurement process.",
+            UserWarning,
         )
+        return self._return_type
 
     def __matmul__(self, other):
         if active_new_opmath():
@@ -2023,7 +2018,6 @@ class Tensor(Observable):
     """
 
     # pylint: disable=abstract-method
-    return_type = None
     tensor = True
     has_matrix = True
 
@@ -2109,17 +2103,7 @@ class Tensor(Observable):
 
     def __repr__(self):
         """Constructor-call-like representation."""
-
-        s = " @ ".join([repr(o) for o in self.obs])
-
-        if self.return_type is None:
-            return s
-
-        return (
-            f"{repr(self.return_type)}(wires={self.wires.tolist()})"
-            if self.return_type is qml.measurements.Probability
-            else f"{repr(self.return_type)}({s})"
-        )
+        return " @ ".join([repr(o) for o in self.obs])
 
     @property
     def name(self):
@@ -2525,7 +2509,7 @@ class Tensor(Observable):
         else:
             obs = Tensor(*self.non_identity_obs)
 
-        obs.return_type = self.return_type
+        obs._return_type = self._return_type
         return obs
 
     def map_wires(self, wire_map: dict):
diff --git a/pennylane/ops/qubit/hamiltonian.py b/pennylane/ops/qubit/hamiltonian.py
index 1a2ff7ac6..1f0e1dff9 100644
--- a/pennylane/ops/qubit/hamiltonian.py
+++ b/pennylane/ops/qubit/hamiltonian.py
@@ -209,8 +209,6 @@ class Hamiltonian(Observable):
 
         self._wires = qml.wires.Wires.all_wires([op.wires for op in self.ops], sort=True)
 
-        self.return_type = None
-
         # attribute to store indices used to form groups of
         # commuting observables, since recomputation is costly
         self._grouping_indices = None
diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py
index baaf7ddaf..1b2951b53 100644
--- a/pennylane/tape/qscript.py
+++ b/pennylane/tape/qscript.py
@@ -304,7 +304,7 @@ class QuantumScript:
 
         for m in self.measurements:
             if m.obs is not None:
-                m.obs.return_type = m.return_type
+                m.obs._return_type = m.return_type
                 obs.append(m.obs)
             else:
                 obs.append(m)
@@ -669,11 +669,12 @@ class QuantumScript:
         if trainable_only:
             params = []
             for p_idx in self.trainable_params:
-                op = self._par_info[p_idx]["op"]
-                if operations_only and hasattr(op, "return_type"):
+                par_info = self._par_info[p_idx]
+                if operations_only and isinstance(self[par_info["op_idx"]], MeasurementProcess):
                     continue
 
-                op_idx = self._par_info[p_idx]["p_idx"]
+                op = par_info["op"]
+                op_idx = par_info["p_idx"]
                 params.append(op.data[op_idx])
             return params
 
@@ -991,7 +992,11 @@ class QuantumScript:
         """
         if self._graph is None:
             self._graph = qml.CircuitGraph(
-                self.operations, self.observables, self.wires, self._par_info, self.trainable_params
+                self.operations,
+                self.measurements,
+                self.wires,
+                self._par_info,
+                self.trainable_params,
             )
 
         return self._graph


Test patch:
diff --git a/tests/gradients/parameter_shift/test_parameter_shift.py b/tests/gradients/parameter_shift/test_parameter_shift.py
index 829cc776a..833e0ab15 100644
--- a/tests/gradients/parameter_shift/test_parameter_shift.py
+++ b/tests/gradients/parameter_shift/test_parameter_shift.py
@@ -3106,6 +3106,27 @@ class TestHamiltonianExpvalGradients:
         with pytest.raises(ValueError, match="for expectations, not var"):
             qml.gradients.param_shift(tape, broadcast=broadcast)
 
+    def test_not_expval_pass_if_not_trainable_hamiltonian(self, broadcast):
+        """Test that if the variance of a non-trainable Hamiltonian is requested,
+        no error is raised"""
+        obs = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)]
+        coeffs = np.array([0.1, 0.2, 0.3])
+        H = qml.Hamiltonian(coeffs, obs)
+
+        weights = np.array([0.4, 0.5])
+
+        with qml.queuing.AnnotatedQueue() as q:
+            qml.RX(weights[0], wires=0)
+            qml.RY(weights[1], wires=1)
+            qml.CNOT(wires=[0, 1])
+            qml.var(H)
+
+        tape = qml.tape.QuantumScript.from_queue(q)
+        tape.trainable_params = {0, 1}  # different from previous test
+
+        tapes, _ = qml.gradients.param_shift(tape, broadcast=broadcast)
+        assert len(tapes) == (3 if broadcast else 5)
+
     def test_no_trainable_coeffs(self, mocker, tol, broadcast):
         """Test no trainable Hamiltonian coefficients"""
         dev = qml.device("default.qubit", wires=2)
diff --git a/tests/measurements/legacy/test_counts_legacy.py b/tests/measurements/legacy/test_counts_legacy.py
index 57adfef7a..b0626237e 100644
--- a/tests/measurements/legacy/test_counts_legacy.py
+++ b/tests/measurements/legacy/test_counts_legacy.py
@@ -16,7 +16,7 @@ import numpy as np
 import pytest
 
 import pennylane as qml
-from pennylane.measurements import AllCounts, Counts
+from pennylane.measurements import Counts
 from pennylane.operation import Operator
 
 
@@ -32,7 +32,7 @@ def custom_measurement_process(device, spy):
         meas = call_args.args[1]
         shot_range, bin_size = (call_args.kwargs["shot_range"], call_args.kwargs["bin_size"])
         if isinstance(meas, Operator):
-            all_outcomes = meas.return_type is AllCounts
+            all_outcomes = call_args.kwargs["all_outcomes"]
             meas = qml.counts(op=meas, all_outcomes=all_outcomes)
         old_res = device.sample(call_args.args[1], **call_args.kwargs)
         new_res = meas.process_samples(
diff --git a/tests/tape/test_operation_recorder.py b/tests/tape/test_operation_recorder.py
index e396a4cb9..3d8cbfbe7 100644
--- a/tests/tape/test_operation_recorder.py
+++ b/tests/tape/test_operation_recorder.py
@@ -99,8 +99,8 @@ class TestOperationRecorder:
             + "\n"
             + "Observables\n"
             + "===========\n"
-            + "var(PauliZ(wires=[0]))\n"
-            + "sample(PauliX(wires=[1]))\n"
+            + "PauliZ(wires=[0])\n"
+            + "PauliX(wires=[1])\n"
         )
 
         def template(x):
diff --git a/tests/tape/test_qscript.py b/tests/tape/test_qscript.py
index 67a964432..31d96a13b 100644
--- a/tests/tape/test_qscript.py
+++ b/tests/tape/test_qscript.py
@@ -410,7 +410,7 @@ class TestInfomationProperties:
         g = qs.graph
         assert isinstance(g, qml.CircuitGraph)
         assert g.operations == qs.operations
-        assert g.observables == qs.observables
+        assert g.observables == qs.measurements
 
         # test that if we request it again, we get the same object
         assert qs.graph is g
diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py
index 965112f4b..f94986d5f 100644
--- a/tests/tape/test_tape.py
+++ b/tests/tape/test_tape.py
@@ -23,6 +23,8 @@ import pennylane as qml
 from pennylane import CircuitGraph
 from pennylane.measurements import (
     MeasurementProcess,
+    ExpectationMP,
+    ProbabilityMP,
     counts,
     expval,
     probs,
@@ -111,8 +113,8 @@ class TestConstruction:
         # test the public measurements property
         assert len(tape.measurements) == 2
         assert all(isinstance(m, MeasurementProcess) for m in tape.measurements)
-        assert tape.observables[0].return_type == qml.measurements.Expectation
-        assert tape.observables[1].return_type == qml.measurements.Probability
+        assert isinstance(tape.measurements[0], ExpectationMP)
+        assert isinstance(tape.measurements[1], ProbabilityMP)
 
     def test_tensor_observables_matmul(self):
         """Test that tensor observables are correctly processed from the annotated
@@ -492,7 +494,7 @@ class TestGraph:
         # requesting the graph creates it
         g = tape.graph
         assert g.operations == [op_]
-        assert g.observables == [obs]
+        assert g.observables == tape.measurements
         assert tape._graph is not None
         spy.assert_called_once()
 
diff --git a/tests/test_device.py b/tests/test_device.py
index a6140f41f..d56f6b0d7 100644
--- a/tests/test_device.py
+++ b/tests/test_device.py
@@ -678,7 +678,6 @@ class TestOperations:
             qml.var(qml.PauliZ(1)),
             qml.sample(qml.PauliZ(2)),
         ]
-        observables = [o.obs for o in observables]
 
         queue_at_pre_measure = []
 
@@ -708,8 +707,6 @@ class TestOperations:
             qml.sample(qml.PauliZ(2)),
         ]
 
-        observables = [o.obs for o in observables]
-
         call_history = []
         with monkeypatch.context() as m:
             m.setattr(
@@ -739,8 +736,6 @@ class TestOperations:
             qml.sample(qml.PauliZ(2)),
         ]
 
-        observables = [o.obs for o in observables]
-
         with pytest.raises(DeviceError, match="Gate Hadamard not supported on device"):
             dev.execute(queue, observables)
 
@@ -748,8 +743,7 @@ class TestOperations:
         """Tests that the execute function raises an error if probabilities are
         not supported by the device"""
         dev = mock_device_with_observables()
-        obs = qml.PauliZ(0)
-        obs.return_type = qml.measurements.ObservableReturnTypes.Probability
+        obs = qml.probs(op=qml.PauliZ(0))
         with pytest.raises(NotImplementedError):
             dev.execute([], [obs])
 
@@ -808,7 +802,6 @@ class TestObservables:
             qml.var(qml.PauliZ(1)),
             qml.sample(qml.PauliZ(2)),
         ]
-        observables = [o.obs for o in observables]
 
         queue_at_pre_measure = []
 
@@ -826,10 +819,7 @@ class TestObservables:
         """Tests that the operations are properly applied and queued"""
         dev = mock_device_with_paulis_and_methods(wires=3)
 
-        observables = []
-        for m in [qml.expval(qml.PauliX(0)), qml.var(qml.PauliY(1)), qml.sample(qml.PauliZ(2))]:
-            m.obs.return_type = m.return_type
-            observables.append(m.obs)
+        observables = [qml.expval(qml.PauliX(0)), qml.var(qml.PauliY(1)), qml.sample(qml.PauliZ(2))]
 
         # capture the arguments passed to dev methods
         expval_args = []
@@ -860,7 +850,6 @@ class TestObservables:
             qml.var(qml.PauliZ(1)),
             qml.sample(qml.PauliZ(2)),
         ]
-        observables = [o.obs for o in observables]
 
         with pytest.raises(DeviceError, match="Observable Hadamard not supported on device"):
             dev.execute(queue, observables)
@@ -874,9 +863,7 @@ class TestObservables:
         queue = [qml.PauliX(wires=0)]
 
         # Make a observable without specifying a return operation upon measuring
-        obs = qml.PauliZ(0)
-        obs.return_type = "SomeUnsupportedReturnType"
-        observables = [obs]
+        observables = [qml.counts(op=qml.PauliZ(0))]
 
         with pytest.raises(
             qml.QuantumFunctionError, match="Unsupported return type specified for observable"
@@ -919,7 +906,6 @@ class TestParameters:
             qml.var(qml.PauliZ(1)),
             qml.sample(qml.PauliZ(2)),
         ]
-        observables = [o.obs for o in observables]
 
         p_mapping = {}
 
@@ -1065,10 +1051,10 @@ class TestBatchExecution:
 
         assert len(res) == 2
         assert np.allclose(
-            res[0], dev.execute(self.tape1.operations, self.tape1.observables), rtol=tol, atol=0
+            res[0], dev.execute(self.tape1.operations, self.tape1.measurements), rtol=tol, atol=0
         )
         assert np.allclose(
-            res[1], dev.execute(self.tape2.operations, self.tape2.observables), rtol=tol, atol=0
+            res[1], dev.execute(self.tape2.operations, self.tape2.measurements), rtol=tol, atol=0
         )
 
     def test_result_empty_tape(self, mock_device_with_paulis_and_methods, tol):
@@ -1082,7 +1068,7 @@ class TestBatchExecution:
 
         assert len(res) == 3
         assert np.allclose(
-            res[0], dev.execute(empty_tape.operations, empty_tape.observables), rtol=tol, atol=0
+            res[0], dev.execute(empty_tape.operations, empty_tape.measurements), rtol=tol, atol=0
         )
 
 
diff --git a/tests/test_operation.py b/tests/test_operation.py
index 48e3e21d2..bc352406c 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -883,7 +883,8 @@ class TestObservableConstruction:
             num_wires = 1
             grad_method = None
 
-        assert DummyObserv(0, wires=[1]).return_type is None
+        with pytest.warns(UserWarning, match="`Observable.return_type` is deprecated. Instead"):
+            assert DummyObserv(0, wires=[1]).return_type is None
 
     def test_construction_with_wires_pos_arg(self):
         """Test that the wires can be given as a positional argument"""
diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py
index 824b62fb2..452deaed9 100644
--- a/tests/test_qubit_device.py
+++ b/tests/test_qubit_device.py
@@ -1473,7 +1473,7 @@ def test_samples_to_counts_with_nan():
     samples[17][1] = np.NaN
     samples[850][0] = np.NaN
 
-    result = device._samples_to_counts(samples, obs=qml.measurements.CountsMP(), num_wires=2)
+    result = device._samples_to_counts(samples, mp=qml.measurements.CountsMP(), num_wires=2)
 
     # no keys with NaNs
     assert len(result) == 4
diff --git a/tests/transforms/test_commutation_dag.py b/tests/transforms/test_commutation_dag.py
index 3716825b2..58b32a79b 100644
--- a/tests/transforms/test_commutation_dag.py
+++ b/tests/transforms/test_commutation_dag.py
@@ -131,7 +131,6 @@ class TestCommutationDAG:
         assert dag.get_node(1).op.compare(b)
         assert dag.get_edge(0, 1) == {0: {"commute": False}}
         assert dag.get_edge(0, 2) is None
-        assert dag.observables[0].return_type.__repr__() == "expval"
         assert dag.observables[0].name == "PauliX"
         assert dag.observables[0].wires.tolist() == [0]
         for i, node in enumerate(dag.get_nodes()):