Repository: PennyLaneAI/pennylane
Pull request #2044 (2021-12-17 23:26:03)

`QuantumTape.shape` method and `QuantumTape.numeric_type`
**Context**

There are certain use cases where knowing the shape and the data type (`dtype`) object for the results of a `QuantumTape` is required. These use cases include defining gradient rules more seamlessly and customizing ML framework functions better.

One such example comes when using JAX and its [`host_callback.call` function](https://jax.readthedocs.io/en/latest/jax.experimental.host_callback.html#using-call-to-call-a-host-function-and-return-results-to-device). `host_callback.call` compiles the code provided and hence requires the information of both the shape and the `dtype` about the results in advance.

The `jax-jit` interface uses this `host_callback.call` function, but it makes the assumption that QNodes are scalar-valued and floating-point when defining the output shape of a QNode. To support vector-valued QNodes, the ability to determine the shape (and the dtype) of a QNode and specific a quantum tape is necessary.

**Changes**

Adds two convenience methods to `QuantumTape`:
1. `QuantumTape.shape`;
2. `QuantumTape.numeric_type`.

`QuantumTape.shape` supports tapes with a single measurement and most homogenous multi-measurement tapes (i.e., where all measurements are of the same type). Getting the output shape of heterogenous multi-measurement remains as a todo. It is worth further noting that multi-measurement output shapes might change in the near future (e.g., QNodes may return tuples instead of arrays for multiple measurements).

`QuantumTape.numeric_type` returns a Python numeric type object: either `int`, `float` or `complex`. The aim of this method is to provide the ability to create a `dtype` based on the precision determined by the user (be it a `dtype` specified on the device or in the interface). This dtype could be determined on the QNode level where the interface, the device and the tape are all accessible.

**Benefits**

* Shape and output domain information can be extracted from `QuantumTape` objects for most use cases;
* The shape and domain of QNodes (through their underlying tapes) is getting organized and checked via test cases, this helps with future standardization;
* The jittable JAX interface can be upgraded to define vector-valued QNode support (see https://github.com/PennyLaneAI/pennylane/pull/2034).

**Drawbacks**

N/A

**Furter notes**

*JAX error messages when calling `host_callback.call` with incorrect shape/dtype*

See for example the following error messages produced by JAX:
* Mismatch in shape:
```python
E   TypeError: Callback func <function _call.<locals>.<lambda> at 0x7f1d3857db80> should have returned a result with abstract values [ShapedArray(int32[10])] but returned [ShapedArray(int32[1,10])]
```
* Mismatch in dtype:
```python
E   TypeError: Callback func <function _call.<locals>.<lambda> at 0x7fd580312af0> should have returned a result with abstract values [ShapedArray(float32[1,10])] but returned [ShapedArray(int32[1,10])]
```

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://codecov.io/gh/PennyLaneAI/pennylane/pull/2044?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#2044](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (08508fb) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/62cc566d0f90a44561adec52dc15d383990fd06c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (62cc566) will **decrease** coverage by `2.17%`.
> The diff coverage is `84.61%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@            Coverage Diff             @@
##           master    #2044      +/-   ##
==========================================
- Coverage   99.19%   97.01%   -2.18%     
==========================================
  Files         228      229       +1     
  Lines       17472    17686     +214     
==========================================
- Hits        17331    17158     -173     
- Misses        141      528     +387     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/tape/tape.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RhcGUvdGFwZS5weQ==) | `95.99% <82.79%> (-3.23%)` | :arrow_down: |
| [pennylane/\_device.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19kZXZpY2UucHk=) | `97.99% <100.00%> (+0.01%)` | :arrow_up: |
| [pennylane/measure.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmUucHk=) | `99.08% <100.00%> (+0.04%)` | :arrow_up: |
| [pennylane/interfaces/batch/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvYmF0Y2gvamF4LnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvamF4LnB5) | `0.00% <0.00%> (-97.44%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF9qYXgucHk=) | `0.00% <0.00%> (-96.63%)` | :arrow_down: |
| [pennylane/qnn/cost.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3Fubi9jb3N0LnB5) | `42.85% <0.00%> (-57.15%)` | :arrow_down: |
| [pennylane/transforms/classical\_jacobian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvY2xhc3NpY2FsX2phY29iaWFuLnB5) | `77.08% <0.00%> (-22.92%)` | :arrow_down: |
| [pennylane/math/utils.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvdXRpbHMucHk=) | `84.41% <0.00%> (-15.59%)` | :arrow_down: |
| [pennylane/math/multi\_dispatch.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvbXVsdGlfZGlzcGF0Y2gucHk=) | `87.73% <0.00%> (-12.27%)` | :arrow_down: |
| ... and [20 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [62cc566...08508fb](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2044?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

josh146: @antalszava what is the status of this PR? this is really nice functionality, so would be great to get this in!
antalszava: Hi Josh, thanks for checking in! I'd like to get back to it, with v0.22.0 airing soon I'll be in a good position to do so. :slightly_smiling_face: 
antalszava: [sc-17243]

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 73569b47f..b74f7441e 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -216,6 +216,15 @@
 
 <h3>Improvements</h3>
 
+* Added the `QuantumTape.shape` method and `QuantumTape.numeric_type`
+  attribute to allow extracting information about the shape and numeric type of
+  quantum tapes.
+  [(#2044)](https://github.com/PennyLaneAI/pennylane/pull/2044)
+
+* Defined a `MeasurementProcess.shape` method and a
+  `MeasurementProcess.numeric_type` attribute.
+  [(#2044)](https://github.com/PennyLaneAI/pennylane/pull/2044)
+
 * The parameter-shift Hessian can now be computed for arbitrary
   operations that support the general parameter-shift rule for
   gradients, using `qml.gradients.param_shift_hessian`
diff --git a/pennylane/_device.py b/pennylane/_device.py
index 06f9ff513..c1a583ed5 100644
--- a/pennylane/_device.py
+++ b/pennylane/_device.py
@@ -247,6 +247,7 @@ class Device(abc.ABC):
             # device is in analytic mode
             self._shots = shots
             self._shot_vector = None
+            self._raw_shot_sequence = None
 
         elif isinstance(shots, int):
             # device is in sampling mode (unbatched)
@@ -261,6 +262,7 @@ class Device(abc.ABC):
         elif isinstance(shots, Sequence) and not isinstance(shots, str):
             # device is in batched sampling mode
             self._shots, self._shot_vector = _process_shot_sequence(shots)
+            self._raw_shot_sequence = shots
 
         else:
             raise DeviceError(
diff --git a/pennylane/devices/default_qubit_tf.py b/pennylane/devices/default_qubit_tf.py
index 0b5cbfb4f..0b61076a9 100644
--- a/pennylane/devices/default_qubit_tf.py
+++ b/pennylane/devices/default_qubit_tf.py
@@ -150,10 +150,11 @@ class DefaultQubitTF(DefaultQubit):
         try:
             res = tf.convert_to_tensor(array, dtype=dtype)
         except InvalidArgumentError:
-            res = tf.concat([tf.reshape(i, [-1]) for i in array], axis=0)
+            axis = 0
+            res = tf.concat([tf.reshape(i, [-1]) for i in array], axis)
 
             if dtype is not None:
-                res = tf.cast(res, dtype=dtype)
+                res = tf.cast(res, dtype)
 
         return res
 
diff --git a/pennylane/measurements.py b/pennylane/measurements.py
index 0f716bcc3..b94181b08 100644
--- a/pennylane/measurements.py
+++ b/pennylane/measurements.py
@@ -21,6 +21,7 @@ and measurement samples using AnnotatedQueues.
 import copy
 import uuid
 import warnings
+import functools
 from enum import Enum
 from typing import Generic, TypeVar
 
@@ -73,6 +74,11 @@ MidMeasure = ObservableReturnTypes.MidMeasure
 basis in the middle of the circuit."""
 
 
+class MeasurementShapeError(ValueError):
+    """An error raised when an unsupported operation is attempted with a
+    quantum tape."""
+
+
 class MeasurementProcess:
     """Represents a measurement process occurring at the end of a
     quantum variational circuit.
@@ -87,12 +93,31 @@ class MeasurementProcess:
             This can only be specified if an observable was not provided.
         eigvals (array): A flat array representing the eigenvalues of the measurement.
             This can only be specified if an observable was not provided.
+        shape (tuple[int] or None): The output shape of the measurement proccess. For
+            some measurement processes a shape is not applicable or the shape
+            may depend on device options, in such cases ``shape=None``. The
+            shape is determined by the return type. For example, assuming a
+            device with ``shots=None``, expectation values and variances define
+            ``shape=(1,)``, whereas probabilities in the qubit define
+            ``shape=(1, 2**num_wires)`` where ``num_wires`` is the number of
+            wires the measurement acts on.
+        numeric_type (type): The expected Python numeric type of the result;
+            either ``int``, ``float`` or ``complex``.
     """
 
     # pylint: disable=too-few-public-methods
     # pylint: disable=too-many-arguments
 
-    def __init__(self, return_type, obs=None, wires=None, eigvals=None, id=None):
+    def __init__(
+        self,
+        return_type,
+        obs=None,
+        wires=None,
+        eigvals=None,
+        id=None,
+        shape=None,
+        numeric_type=None,
+    ):
         self.return_type = return_type
         self.obs = obs
         self.id = id
@@ -109,6 +134,14 @@ class MeasurementProcess:
 
             self._eigvals = np.array(eigvals)
 
+        self._shape = shape
+        """tuple[int] or None: The output shape of the measurement proccess. For some a
+        shape is not applicable or the shape may depend on device options, in
+        such cases ``shape=None``."""
+
+        self.numeric_type = numeric_type
+        """tuple[int]: The Python numeric type of the measurement result."""
+
         # TODO: remove the following lines once devices
         # have been refactored to accept and understand recieving
         # measurement processes rather than specific observables.
@@ -125,6 +158,136 @@ class MeasurementProcess:
         # Queue the measurement process
         self.queue()
 
+    @functools.lru_cache()
+    def shape(self, device=None):
+        """The expected output shape of the MeasurementProcess.
+
+        Note that the output shape is dependent on the device when:
+
+        * The ``return_type`` is either ``Probability``, ``State`` (from :func:`.state`) or
+          ``Sample``;
+        * The shot vector was defined in the device.
+
+        Args:
+            device (.Device): a PennyLane device to use for determining the
+                shape
+
+        Returns:
+            tuple: the output shape
+        """
+        if device is None:
+            if self._shape is not None:
+                return self._shape
+
+            if self.return_type in (Probability, State, Sample):
+                raise MeasurementShapeError(
+                    f"Return type {self.return_type} requires the device argument to be passed to obtain the shape."
+                )
+
+        if device._shot_vector is None:
+
+            # Device dependent shapes
+            if self.return_type == Probability:
+                len_wires = len(self.wires)
+                dim = self._get_num_basis_states(len_wires, device)
+                shape = (1, dim)
+
+            elif self.return_type == State and self._shape is None:
+
+                # Note: qml.density_matrix has its shape defined, so we're handling
+                # the qml.state case; acts on all device wires
+                dim = 2 ** len(device.wires)
+                shape = (1, dim)
+
+            elif self.return_type == Sample:
+                len_wires = len(device.wires)
+
+                if self.obs is not None:
+                    # qml.sample(some_observable) case
+                    shape = (1, device.shots)
+
+                else:
+                    # qml.sample() case
+                    shape = (1, device.shots, len_wires)
+            else:
+                shape = self._shape
+
+            # The other return types should have their shapes pre-defined
+            return shape
+
+        return self._shot_vector_shape(device)
+
+    @functools.lru_cache()
+    def _shot_vector_shape(self, device):
+        """Auxiliary function for getting the output shape when the device has
+        the shot vector defined.
+
+        The shape is device dependent even if the return type has a shape
+        pre-defined (e.g., expectation values, states, etc.).
+        """
+        shot_vector = device._shot_vector
+        num_shot_elements = sum([s.copies for s in shot_vector])
+        shape = ()
+
+        if self._shape is not None:
+
+            # Expval, var and density_matrix case
+            shape = list(self._shape)
+            shape[0] *= num_shot_elements
+            shape = tuple(shape)
+
+        elif self.return_type == qml.measurements.Probability:
+
+            len_wires = len(self.wires)
+            dim = self._get_num_basis_states(len_wires, device)
+            shape = (num_shot_elements, dim)
+
+        elif self.return_type == qml.measurements.Sample:
+            if self.obs is not None:
+                shape = tuple(
+                    (shot_val,) if shot_val != 1 else tuple()
+                    for shot_val in device._raw_shot_sequence
+                )
+            else:
+                # TODO: revisit when qml.sample without an observable fully
+                # supports shot vectors
+                raise MeasurementShapeError(
+                    "Getting the output shape of a measurement returning samples along with "
+                    "a device with a shot vector is not supported."
+                )
+
+        elif self.return_type == qml.measurements.State:
+
+            # Note: qml.density_matrix has its shape defined, so we're handling
+            # the qml.state case; acts on all device wires
+            dim = 2 ** len(device.wires)
+            shape = (num_shot_elements, dim)
+
+        return shape
+
+    @staticmethod
+    @functools.lru_cache()
+    def _get_num_basis_states(num_wires, device):
+        """Auxiliary function to determine the number of basis states given the
+        number of systems and a quantum device.
+
+        This function is meant to be used with the Probability measurement to
+        determine how many outcomes there will be. With qubit based devices
+        we'll have two outcomes for each subsystem. With continuous variable
+        devices that impose a Fock cutoff the number of basis states per
+        subsystem equals the cutoff value.
+
+        Args:
+            num_wires (int): the number of qubits/qumodes
+            device (.Device): a PennyLane device
+
+        Returns:
+            int: the number of basis states
+        """
+        cutoff = getattr(device, "cutoff", None)
+        base = 2 if cutoff is None else cutoff
+        return base**num_wires
+
     def diagonalizing_gates(self):
         """Returns the gates that diagonalize the measured wires such that they
         are in the eigenbasis of the circuit observables.
@@ -336,7 +499,7 @@ def expval(op):
             f"{op.name} is not an observable: cannot be used with expval"
         )
 
-    return MeasurementProcess(Expectation, obs=op)
+    return MeasurementProcess(Expectation, obs=op, shape=(1,), numeric_type=float)
 
 
 def var(op):
@@ -369,7 +532,7 @@ def var(op):
     if not isinstance(op, Observable):
         raise qml.QuantumFunctionError(f"{op.name} is not an observable: cannot be used with var")
 
-    return MeasurementProcess(Variance, obs=op)
+    return MeasurementProcess(Variance, obs=op, shape=(1,), numeric_type=float)
 
 
 def sample(op=None, wires=None):
@@ -378,6 +541,9 @@ def sample(op=None, wires=None):
     If no observable is provided then basis state samples are returned directly
     from the device.
 
+    Note that the output shape of this measurement process depends on the shots
+    specified on the device.
+
     Args:
         op (Observable or None): a quantum observable object
         wires (Sequence[int] or int or None): the wires we wish to sample from, ONLY set wires if op is None
@@ -446,6 +612,20 @@ def sample(op=None, wires=None):
             f"{op.name} is not an observable: cannot be used with sample"
         )
 
+    # Note: we only assume an integer numeric type if the observable is a
+    # built-in observable with integer eigenvalues or a tensor product thereof
+    if op is None:
+
+        # Computational basis samples
+        numeric_type = int
+    else:
+        int_eigval_obs = {qml.PauliX, qml.PauliY, qml.PauliZ, qml.Hadamard, qml.Identity}
+        tensor_terms = [op] if not hasattr(op, "obs") else op.obs
+        if not all(o.__class__ in int_eigval_obs for o in tensor_terms):
+            numeric_type = float
+        else:
+            numeric_type = int
+
     if wires is not None:
         if op is not None:
             raise ValueError(
@@ -453,9 +633,11 @@ def sample(op=None, wires=None):
                 "provided. The wires to sample will be determined directly from the observable."
             )
 
-        return MeasurementProcess(Sample, obs=op, wires=qml.wires.Wires(wires))
+        return MeasurementProcess(
+            Sample, obs=op, wires=qml.wires.Wires(wires), numeric_type=numeric_type
+        )
 
-    return MeasurementProcess(Sample, obs=op)
+    return MeasurementProcess(Sample, obs=op, numeric_type=numeric_type)
 
 
 def probs(wires=None, op=None):
@@ -512,6 +694,9 @@ def probs(wires=None, op=None):
     to a :math:`14.6\%` chance of measuring the rotated :math:`|0\rangle` state
     and :math:`85.4\%` of measuring the rotated :math:`|1\rangle` state.
 
+    Note that the output shape of this measurement process depends on whether
+    the device simulates qubit or continuous variable quantum systems.
+
     Args:
         wires (Sequence[int] or int): the wire the operation acts on
         op (Observable): Observable (with a diagonalzing_gates attribute) that rotates
@@ -532,14 +717,18 @@ def probs(wires=None, op=None):
             f"{op} does not define diagonalizing gates : cannot be used to rotate the probability"
         )
 
+    numeric_type = float
+
     if wires is not None:
         if op is not None:
             raise qml.QuantumFunctionError(
                 "Cannot specify the wires to probs if an observable is "
                 "provided. The wires for probs will be determined directly from the observable."
             )
-        return MeasurementProcess(Probability, wires=qml.wires.Wires(wires))
-    return MeasurementProcess(Probability, obs=op)
+        return MeasurementProcess(
+            Probability, wires=qml.wires.Wires(wires), numeric_type=numeric_type
+        )
+    return MeasurementProcess(Probability, obs=op, numeric_type=numeric_type)
 
 
 def state():
@@ -549,6 +738,9 @@ def state():
     ``wires`` argument should *not* be provided since ``state()`` always returns a pure state
     describing all wires in the device.
 
+    Note that the output shape of this measurement process depends on the
+    number of wires defined for the device.
+
     **Example:**
 
     .. code-block:: python3
@@ -592,7 +784,7 @@ def state():
         -0.07471906623679961
     """
     # pylint: disable=protected-access
-    return MeasurementProcess(State)
+    return MeasurementProcess(State, numeric_type=complex)
 
 
 def density_matrix(wires):
@@ -633,7 +825,10 @@ def density_matrix(wires):
         with a compatible device.
     """
     # pylint: disable=protected-access
-    return MeasurementProcess(State, wires=qml.wires.Wires(wires))
+    wires = qml.wires.Wires(wires)
+    dim = 2 ** len(wires)
+    shape = (1, dim, dim)
+    return MeasurementProcess(State, wires=wires, shape=shape, numeric_type=complex)
 
 
 T = TypeVar("T")
diff --git a/pennylane/tape/__init__.py b/pennylane/tape/__init__.py
index 7a4526020..26e16d2f7 100644
--- a/pennylane/tape/__init__.py
+++ b/pennylane/tape/__init__.py
@@ -15,7 +15,7 @@
 This subpackage contains the quantum tape, which tracks, queues, and
 validates quantum operations and measurements.
 """
-from .tape import QuantumTape, get_active_tape
+from .tape import QuantumTape, get_active_tape, TapeError
 from .operation_recorder import OperationRecorder
 from .stop_recording import stop_recording
 from .unwrap import Unwrap, UnwrapTape
diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py
index 9bc72baeb..02386b61b 100644
--- a/pennylane/tape/tape.py
+++ b/pennylane/tape/tape.py
@@ -82,6 +82,10 @@ https://github.com/Qiskit/openqasm/blob/master/examples/stdgates.inc
 """
 
 
+class TapeError(ValueError):
+    """An error raised with a quantum tape."""
+
+
 def get_active_tape():
     """Returns the currently recording tape.
     If no tape is currently recording, ``None`` is returned.
@@ -491,7 +495,6 @@ class QuantumTape(AnnotatedQueue):
                     # operations such that the order of the operators in the
                     # tape is correct
                     self._ops.append(obj)
-
                 else:
 
                     # measurement process
@@ -499,6 +502,8 @@ class QuantumTape(AnnotatedQueue):
 
                     # attempt to infer the output dimension
                     if obj.return_type is qml.measurements.Probability:
+                        # TODO: what if we had a CV device here? Having the base as
+                        # 2 would have to be swapped to the cutoff value
                         self._output_dim += 2 ** len(obj.wires)
                     elif obj.return_type is qml.measurements.State:
                         continue  # the output_dim is worked out automatically
@@ -930,6 +935,245 @@ class QuantumTape(AnnotatedQueue):
             op = self._par_info[idx]["op"]
             op.data[self._par_info[idx]["p_idx"]] = p
 
+    @staticmethod
+    def _single_measurement_shape(measurement_process, device):
+        """Auxiliary function of shape that determines the output
+        shape of a tape with a single measurement.
+
+        Args:
+            measurement_process (MeasurementProcess): the measurement process
+                associated with the single measurement
+            device (~.Device): a PennyLane device
+
+        Returns:
+            tuple: output shape
+        """
+        return measurement_process.shape(device)
+
+    @staticmethod
+    def _multi_homogenous_measurement_shape(mps, device):
+        """Auxiliary function of shape that determines the output
+        shape of a tape with multiple homogenous measurements.
+
+        .. note::
+
+            Assuming multiple probability measurements where not all
+            probability measurements have the same number of wires specified,
+            the output shape of the tape is a sum of the output shapes produced
+            by each probability measurement.
+
+            Consider the `qml.probs(wires=[0]), qml.probs(wires=[1,2])`
+            multiple probability measurement with an analytic device as an
+            example.
+
+            The output shape will be a one element tuple `(6,)`, where the
+            element `6` is equal to `2 ** 1 + 2 ** 2 = 6`. The base of each
+            term is determined by the number of basis states and the exponent
+            of each term comes from the length of the wires specified for the
+            probability measurements: `1 == len([0]) and 2 == len([1, 2])`.
+        """
+        shape = tuple()
+
+        # We know that there's one type of return_type, gather it from the
+        # first one
+        ret_type = mps[0].return_type
+        if ret_type == qml.measurements.State:
+            raise TapeError(
+                "Getting the output shape of a tape with multiple state measurements is not supported."
+            )
+
+        shot_vector = device._shot_vector
+        if shot_vector is None:
+            if ret_type in (qml.measurements.Expectation, qml.measurements.Variance):
+
+                shape = (len(mps),)
+
+            elif ret_type == qml.measurements.Probability:
+
+                wires_num_set = {len(meas.wires) for meas in mps}
+                same_num_wires = len(wires_num_set) == 1
+                if same_num_wires:
+                    # All probability measurements have the same number of
+                    # wires, gather the length from the first one
+
+                    len_wires = len(mps[0].wires)
+                    dim = mps[0]._get_num_basis_states(len_wires, device)
+                    shape = (len(mps), dim)
+
+                else:
+                    # There are a varying number of wires that the probability
+                    # measurement processes act on
+                    shape = (sum(2 ** len(m.wires) for m in mps),)
+
+            elif ret_type == qml.measurements.Sample:
+
+                shape = (len(mps), device.shots)
+
+            # No other measurement type to check
+
+        else:
+            shape = QuantumTape._shape_shot_vector_multi_homogenous(mps, device)
+
+        return shape
+
+    @staticmethod
+    def _shape_shot_vector_multi_homogenous(mps, device):
+        """Auxiliary function for determining the output shape of the tape for
+        multiple homogenous measurements for a device with a shot vector.
+
+        Note: it is assumed that getting the output shape of a tape with
+        multiple state measurements is not supported.
+        """
+        shape = tuple()
+
+        ret_type = mps[0].return_type
+        shot_vector = device._shot_vector
+
+        # Shot vector was defined
+        if ret_type in (qml.measurements.Expectation, qml.measurements.Variance):
+            num = sum(shottup.copies for shottup in shot_vector)
+            shape = (num, len(mps))
+
+        elif ret_type == qml.measurements.Probability:
+
+            wires_num_set = {len(meas.wires) for meas in mps}
+            same_num_wires = len(wires_num_set) == 1
+            if same_num_wires:
+                # All probability measurements have the same number of
+                # wires, gather the length from the first one
+
+                len_wires = len(mps[0].wires)
+                dim = mps[0]._get_num_basis_states(len_wires, device)
+                shot_copies_sum = sum(s.copies for s in shot_vector)
+                shape = (shot_copies_sum, len(mps), dim)
+
+            else:
+                # There is a varying number of wires that the probability
+                # measurement processes act on
+                # TODO: revisit when issues with this case are resolved
+                raise TapeError(
+                    "Getting the output shape of a tape with multiple probability measurements "
+                    "along with a device that defines a shot vector is not supported."
+                )
+
+        elif ret_type == qml.measurements.Sample:
+            shape = []
+            for shot_val in device.shot_vector:
+                for _ in range(shot_val.copies):
+                    shots = shot_val.shots
+                    if shots != 1:
+                        shape.append(tuple([shots, len(mps)]))
+                    else:
+                        shape.append((len(mps),))
+        return shape
+
+    def shape(self, device):
+        """Produces the output shape of the tape by inspecting its measurements
+        and the device used for execution.
+
+        .. note::
+
+            The computed shape is not stored because the output shape may be
+            dependent on the device used for execution.
+
+        Args:
+            device (.Device): the device that will be used for the tape execution
+
+        Raises:
+            TapeError: raised for unsupported cases for
+                example when the tape contains heterogeneous measurements
+
+        Returns:
+            Union[tuple[int], list[tuple[int]]]: the output shape(s) of the
+            tape result
+
+        **Example:**
+
+        .. code-block:: python
+
+            dev = qml.device("default.qubit", wires=2)
+            a = np.array([0.1, 0.2, 0.3])
+
+            def func(a):
+                qml.RY(a[0], wires=0)
+                qml.RX(a[1], wires=0)
+                qml.RY(a[2], wires=0)
+
+            with qml.tape.QuantumTape() as tape:
+                func(a)
+                qml.state()
+
+        .. code-block:: pycon
+
+            >>> tape.shape(dev)
+            (1, 4)
+        """
+        output_shape = tuple()
+
+        if len(self._measurements) == 1:
+            output_shape = self._single_measurement_shape(self._measurements[0], device)
+        else:
+            num_measurements = len(set(meas.return_type for meas in self._measurements))
+            if num_measurements == 1:
+                output_shape = self._multi_homogenous_measurement_shape(self._measurements, device)
+            else:
+                raise TapeError(
+                    "Getting the output shape of a tape that contains multiple types of measurements is unsupported."
+                )
+        return output_shape
+
+    @property
+    def numeric_type(self):
+        """Returns the expected numeric type of the tape result by inspecting
+        its measurements.
+
+        Raises:
+            TapeError: raised for unsupported cases for
+                example when the tape contains heterogeneous measurements
+
+        Returns:
+            type: the numeric type corresponding to the result type of the
+            tape
+
+        **Example:**
+
+        .. code-block:: python
+
+            dev = qml.device("default.qubit", wires=2)
+            a = np.array([0.1, 0.2, 0.3])
+
+            def func(a):
+                qml.RY(a[0], wires=0)
+                qml.RX(a[1], wires=0)
+                qml.RY(a[2], wires=0)
+
+            with qml.tape.QuantumTape() as tape:
+                func(a)
+                qml.state()
+
+        .. code-block:: pycon
+
+            >>> tape.numeric_type
+            complex
+        """
+        measurement_types = set(meas.return_type for meas in self._measurements)
+        if len(measurement_types) > 1:
+            raise TapeError(
+                "Getting the numeric type of a tape that contains multiple types of measurements is unsupported."
+            )
+
+        if list(measurement_types)[0] == qml.measurements.Sample:
+
+            for observable in self._measurements:
+                # Note: if one of the sample measurements contains outputs that
+                # are real, then the entire result will be real
+                if observable.numeric_type == float:
+                    return observable.numeric_type
+
+            return int
+
+        return self._measurements[0].numeric_type
+
     def unwrap(self):
         """A context manager that unwraps a tape with tensor-like parameters
         to NumPy arrays.
@@ -1232,8 +1476,8 @@ class QuantumTape(AnnotatedQueue):
                 if op.inverse:
                     op.inv()
 
-        # pylint: disable=no-member
         # decompose the queue
+        # pylint: disable=no-member
         operations = tape.expand(depth=2, stop_at=lambda obj: obj.name in OPENQASM_GATES).operations
 
         # create the QASM code representing the operations
diff --git a/pennylane/templates/subroutines/grover.py b/pennylane/templates/subroutines/grover.py
index c7a5ba246..7e7f09b3e 100644
--- a/pennylane/templates/subroutines/grover.py
+++ b/pennylane/templates/subroutines/grover.py
@@ -114,7 +114,9 @@ class GroverOperator(Operation):
         return 0
 
     @staticmethod
-    def compute_decomposition(wires, work_wires, **kwargs):  # pylint: disable=arguments-differ
+    def compute_decomposition(
+        wires, work_wires, **kwargs
+    ):  # pylint: disable=arguments-differ,unused-argument
         r"""Representation of the operator as a product of other operators.
 
         .. math:: O = O_1 O_2 \dots O_n.


Test patch:
diff --git a/tests/devices/test_default_qubit_tf.py b/tests/devices/test_default_qubit_tf.py
index 6f0a47313..623e85bb3 100644
--- a/tests/devices/test_default_qubit_tf.py
+++ b/tests/devices/test_default_qubit_tf.py
@@ -1548,3 +1548,20 @@ class TestHighLevelIntegration:
 
         assert isinstance(grad, tf.Tensor)
         assert grad.shape == weights.shape
+
+
+def test_asarray_ragged_dtype_conversion(monkeypatch):
+    """Test that the _asarray internal method handles ragged arrays well when
+    the dtype argument was provided."""
+    from tensorflow.python.framework.errors_impl import InvalidArgumentError
+
+    dev = qml.device("default.qubit.tf", wires=2)
+
+    def mock_func(arr, dtype):
+        raise InvalidArgumentError(
+            None, None, "SomeMessage"
+        )  # args passed are non-significant for test case
+
+    monkeypatch.setattr(tf, "convert_to_tensor", mock_func)
+    res = dev._asarray(np.array([1]), tf.float32)
+    assert res.dtype == tf.float32
diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py
index b6c0fc590..6ee87953b 100644
--- a/tests/tape/test_tape.py
+++ b/tests/tape/test_tape.py
@@ -21,8 +21,8 @@ import pytest
 
 import pennylane as qml
 from pennylane import CircuitGraph
-from pennylane.tape import QuantumTape
-from pennylane.measurements import MeasurementProcess, expval, sample, var
+from pennylane.tape import QuantumTape, TapeError
+from pennylane.measurements import MeasurementProcess, expval, sample, var, MeasurementShapeError
 
 
 def TestOperationMonkeypatching():
@@ -1760,6 +1760,498 @@ class TestHashing:
         assert tape1.hash == tape2.hash
 
 
+def cost(tape, dev):
+    return qml.execute([tape], dev, interface="autograd", gradient_fn=qml.gradients.param_shift)
+
+
+measures = [
+    (qml.expval(qml.PauliZ(0)), (1,)),
+    (qml.var(qml.PauliZ(0)), (1,)),
+    (qml.probs(wires=[0]), (1, 2)),
+    (qml.probs(wires=[0, 1]), (1, 4)),
+    (qml.state(), (1, 8)),  # Assumes 3-qubit device
+    (qml.density_matrix(wires=[0, 1]), (1, 4, 4)),
+    (
+        qml.sample(qml.PauliZ(0)),
+        None,
+    ),  # Shape is None because the expected shape is in the test case
+    (qml.sample(), None),  # Shape is None because the expected shape is in the test case
+]
+
+multi_measurements = [
+    ([qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))], (2,)),
+    ([qml.probs(wires=[0]), qml.probs(wires=[1])], (2, 2)),
+    ([qml.probs(wires=[0]), qml.probs(wires=[1, 2])], (2**1 + 2**2,)),
+    ([qml.probs(wires=[0, 2]), qml.probs(wires=[1])], (2**2 + 2**1,)),
+    (
+        [qml.probs(wires=[0]), qml.probs(wires=[1, 2]), qml.probs(wires=[0, 1, 2])],
+        (2**1 + 2**2 + 2**3,),
+    ),
+]
+
+
+@pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences")
+class TestOutputShape:
+    """Tests for determining the tape output shape of tapes."""
+
+    @pytest.mark.parametrize("measurement, expected_shape", measures)
+    @pytest.mark.parametrize("shots", [None, 1, 10])
+    def test_output_shapes_single(self, measurement, expected_shape, shots):
+        """Test that the output shape produced by the tape matches the expected
+        output shape."""
+        if shots is None and measurement.return_type is qml.measurements.Sample:
+            pytest.skip("Sample doesn't support analytic computations.")
+
+        num_wires = 3
+        dev = qml.device("default.qubit", wires=num_wires, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            qml.apply(measurement)
+
+        shot_dim = shots if not isinstance(shots, tuple) else len(shots)
+        if expected_shape is None:
+            expected_shape = shot_dim if shot_dim == 1 else (shot_dim,)
+
+        if measurement.return_type is qml.measurements.Sample:
+            if measurement.obs is not None:
+                expected_shape = (1, shots)
+
+            else:
+                expected_shape = (1, shots, num_wires)
+
+        assert tape.shape(dev) == expected_shape
+
+    @pytest.mark.parametrize("measurement, expected_shape", measures)
+    @pytest.mark.parametrize("shots", [None, 1, 10, (1, 2, 5, 3)])
+    def test_output_shapes_single_qnode_check(self, measurement, expected_shape, shots):
+        """Test that the output shape produced by the tape matches the output
+        shape of a QNode for a single measurement."""
+        if shots is None and measurement.return_type is qml.measurements.Sample:
+            pytest.skip("Sample doesn't support analytic computations.")
+
+        # TODO: revisit when qml.sample without an observable has been updated
+        # with shot vectors
+        if (
+            isinstance(shots, tuple)
+            and measurement.return_type is qml.measurements.Sample
+            and not measurement.obs
+        ):
+            pytest.skip("qml.sample with no observable is to be updated for shot vectors.")
+
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            qml.apply(measurement)
+
+        res = qml.execute([tape], dev, gradient_fn=qml.gradients.param_shift)[0]
+
+        if isinstance(res, tuple):
+            res_shape = tuple([r.shape for r in res])
+        else:
+            res_shape = res.shape
+
+        res_shape = res_shape if res_shape != tuple() else 1
+        assert tape.shape(dev) == res_shape
+
+    def test_output_shapes_single_qnode_check_cutoff(self):
+        """Test that the tape output shape is correct when computing
+        probabilities with a dummy device that defines a cutoff value."""
+
+        class CustomDevice(qml.QubitDevice):
+            """A dummy device that has a cutoff value specified and returns
+            analytic probabilities in a fashion similar to the
+            strawberryfields.fock device.
+
+            Note: this device definition is used as PennyLane-SF is not a
+            dependency of PennyLane core and there are no CV device in
+            PennyLane core using a cutoff value.
+            """
+
+            name = "Device with cutoff"
+            short_name = "dummy.device"
+            pennylane_requires = "0.1.0"
+            version = "0.0.1"
+            author = "CV quantum"
+
+            operations = {}
+            observables = {"Identity"}
+
+            def __init__(self, shots=None, wires=None, cutoff=None):
+                super().__init__(wires=wires, shots=shots)
+                self.cutoff = cutoff
+
+            def apply(self, operations, **kwargs):
+                pass
+
+            def analytic_probability(self, wires=None):
+                if wires is None:
+                    wires = self.wires
+                return np.zeros(self.cutoff ** len(wires))
+
+        dev = CustomDevice(wires=2, cutoff=13)
+
+        # If PennyLane-SF is installed, the following can be checked e.g., locally:
+        # dev = qml.device("strawberryfields.fock", wires=2, cutoff_dim=13)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.probs(wires=[0])
+
+        @qml.qnode(dev)
+        def circuit(a, b):
+            return qml.probs(wires=[0])
+
+        res_shape = qml.execute([tape], dev, gradient_fn=qml.gradients.param_shift_cv)[0]
+        assert tape.shape(dev) == res_shape.shape
+
+    @pytest.mark.parametrize("measurements, expected", multi_measurements)
+    @pytest.mark.parametrize("shots", [None, 1, 10])
+    def test_multi_measure(self, measurements, expected, shots):
+        """Test that the expected output shape is obtained when using multiple
+        expectation value, variance and probability measurements."""
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            for m in measurements:
+                qml.apply(m)
+
+        if measurements[0].return_type is qml.measurements.Sample:
+            expected[1] = shots
+            expected = tuple(expected)
+
+        res = tape.shape(dev)
+        assert res == expected
+
+        execution_results = cost(tape, dev)
+        expected = execution_results[0].shape
+        assert res == expected
+
+    @pytest.mark.parametrize("measurements, expected", multi_measurements)
+    def test_multi_measure_shot_vector(self, measurements, expected):
+        """Test that the expected output shape is obtained when using multiple
+        expectation value, variance and probability measurements with a shot
+        vector."""
+        if measurements[0].return_type is qml.measurements.Probability:
+            num_wires = set(len(m.wires) for m in measurements)
+            if len(num_wires) > 1:
+                pytest.skip(
+                    "Multi-probs with varying number of varies when using a shot vector is to be updated in PennyLane."
+                )
+
+        shots = (1, 1, 5, 1)
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            for m in measurements:
+                qml.apply(m)
+
+        if measurements[0].return_type is qml.measurements.Sample:
+            expected[1] = shots
+            expected = tuple(expected)
+
+        # Update expected as we're using a shotvector
+        expected = (len(shots),) + expected
+        res = tape.shape(dev)
+        assert res == expected
+
+        execution_results = cost(tape, dev)
+        expected = execution_results[0].shape
+        assert res == expected
+
+    @pytest.mark.parametrize("shots", [1, 10])
+    def test_multi_measure_sample(self, shots):
+        """Test that the expected output shape is obtained when using multiple
+        qml.sample measurements."""
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        num_samples = 3
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            for i in range(num_samples):
+                qml.sample(qml.PauliZ(i))
+
+        expected = (num_samples, shots)
+
+        res = tape.shape(dev)
+        assert res == expected
+
+        execution_results = cost(tape, dev)
+        expected = execution_results[0].shape
+        assert res == expected
+
+    def test_multi_measure_sample_shot_vector(self):
+        """Test that the expected output shape is obtained when using multiple
+        qml.sample measurements with a shot vector."""
+        shots = (1, 1, 5, 1)
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        a = np.array(0.1)
+        b = np.array(0.2)
+
+        num_samples = 3
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            for i in range(num_samples):
+                qml.sample(qml.PauliZ(i))
+
+        expected = []
+        for s in shots:
+            shape = (num_samples,) if s == 1 else (s, num_samples)
+            expected.append(shape)
+
+        res = tape.shape(dev)
+        for r, e in zip(res, expected):
+            assert r == e
+
+        execution_results = cost(tape, dev)[0]
+        for r, e in zip(res, execution_results):
+            assert r == e.shape
+
+    def test_multi_measure_probs_shot_vector_errors(self):
+        """Test that getting the output shape of a tape containing multiple
+        probability measurements with different number of wires errors when
+        using a device with a shot vector."""
+        dev = qml.device("default.qubit", wires=3, shots=(1, 2, 3))
+
+        with qml.tape.QuantumTape() as tape:
+            qml.probs(wires=[0])
+            qml.probs(wires=[1, 2])
+
+        with pytest.raises(TapeError, match="multiple probability measurements"):
+            tape.shape(dev)
+
+    def test_raises_multiple_different_measurements(self):
+        """Test that getting the output shape of a tape that contains multiple
+        types of measurements raises an error."""
+        dev = qml.device("default.qubit", wires=3)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(0.3, wires=0)
+            qml.RX(0.2, wires=0)
+            qml.expval(qml.PauliZ(0))
+            qml.sample(qml.PauliZ(0))
+
+        with pytest.raises(
+            TapeError,
+            match="contains multiple types of measurements is unsupported",
+        ):
+            tape.shape(dev)
+
+    def test_raises_multi_state(self):
+        """Test that getting the output shape of a tape that contains multiple
+        state measurements raises an error."""
+        dev = qml.device("default.qubit", wires=3)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(0.3, wires=0)
+            qml.RX(0.2, wires=0)
+            qml.state()
+            qml.density_matrix(wires=0)
+
+        with pytest.raises(TapeError, match="multiple state measurements is not supported"):
+            tape.shape(dev)
+
+    def test_raises_sample_shot_vector(self):
+        """Test that getting the output shape of a tape that returns samples
+        along with a device with a shot vector raises an error."""
+        dev = qml.device("default.qubit", wires=3, shots=(1, 2, 3))
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(0.3, wires=0)
+            qml.RX(0.2, wires=0)
+            qml.sample()
+
+        with pytest.raises(
+            TapeError,
+            match="returning samples along with a device with a shot vector",
+        ):
+            tape.shape(dev)
+
+    def test_raises_sample_shot_vector(self):
+        """Test that getting the output shape of a tape that returns samples
+        along with a device with a shot vector raises an error."""
+        dev = qml.device("default.qubit", wires=3, shots=(1, 2, 3))
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(0.3, wires=0)
+            qml.RX(0.2, wires=0)
+            qml.sample()
+
+        with pytest.raises(
+            MeasurementShapeError,
+            match="returning samples along with a device with a shot vector",
+        ):
+            tape.shape(dev)
+
+
+class TestNumericType:
+    """Tests for determining the numeric type of the tape output."""
+
+    @pytest.mark.parametrize(
+        "ret", [qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(0)), qml.probs(wires=[0])]
+    )
+    @pytest.mark.parametrize("shots", [None, 1, (1, 2, 3)])
+    def test_float_measures(self, ret, shots):
+        """Test that most measurements output floating point values and that
+        the tape output domain correctly identifies this."""
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit(a, b):
+            qml.RY(a, wires=[0])
+            qml.RZ(b, wires=[0])
+            return qml.apply(ret)
+
+        result = circuit(0.3, 0.2)
+
+        # Double-check the domain of the QNode output
+        assert np.issubdtype(result.dtype, float)
+        assert circuit.qtape.numeric_type is float
+
+    @pytest.mark.parametrize(
+        "ret", [qml.state(), qml.density_matrix(wires=[0, 1]), qml.density_matrix(wires=[2, 0])]
+    )
+    def test_complex_state(self, ret):
+        """Test that a tape with qml.state correctly determines that the output
+        domain will be complex."""
+        dev = qml.device("default.qubit", wires=3)
+
+        @qml.qnode(dev)
+        def circuit(a, b):
+            qml.RY(a, wires=[0])
+            qml.RZ(b, wires=[0])
+            return qml.apply(ret)
+
+        result = circuit(0.3, 0.2)
+
+        # Double-check the domain of the QNode output
+        assert np.issubdtype(result.dtype, complex)
+        assert circuit.qtape.numeric_type is complex
+
+    @pytest.mark.parametrize("ret", [qml.sample(), qml.sample(qml.PauliZ(wires=0))])
+    def test_sample_int_eigvals(self, ret):
+        """Test that the tape can correctly determine the output domain for a
+        sampling measurement with a Hermitian observable with integer
+        eigenvalues."""
+        dev = qml.device("default.qubit", wires=3, shots=5)
+
+        arr = np.array(
+            [
+                1.32,
+                2.312,
+            ]
+        )
+        herm = np.outer(arr, arr)
+
+        @qml.qnode(dev)
+        def circuit():
+            qml.RY(0.4, wires=[0])
+            return qml.apply(ret)
+
+        result = circuit()
+
+        # Double-check the domain of the QNode output
+        assert np.issubdtype(result.dtype, int)
+        assert circuit.qtape.numeric_type is int
+
+    # TODO: add cases for each interface once qml.Hermitian supports other
+    # interfaces
+    def test_sample_real_eigvals(self):
+        """Test that the tape can correctly determine the output domain when
+        sampling a Hermitian observable with real eigenvalues."""
+        dev = qml.device("default.qubit", wires=3, shots=5)
+
+        arr = np.array(
+            [
+                1.32,
+                2.312,
+            ]
+        )
+        herm = np.outer(arr, arr)
+
+        @qml.qnode(dev)
+        def circuit(a, b):
+            qml.RY(0.4, wires=[0])
+            return qml.sample(qml.Hermitian(herm, wires=0))
+
+        result = circuit(0.3, 0.2)
+
+        # Double-check the domain of the QNode output
+        assert np.issubdtype(result.dtype, float)
+        assert circuit.qtape.numeric_type is float
+
+    def test_sample_real_and_int_eigvals(self):
+        """Test that the tape can correctly determine the output domain for
+        multiple sampling measurements with a Hermitian observable with real
+        eigenvalues and another one with integer eigenvalues."""
+        dev = qml.device("default.qubit", wires=3, shots=5)
+
+        arr = np.array(
+            [
+                1.32,
+                2.312,
+            ]
+        )
+        herm = np.outer(arr, arr)
+
+        @qml.qnode(dev, interface="autograd")
+        def circuit(a, b):
+            qml.RY(a, wires=0)
+            qml.RX(b, wires=0)
+            return qml.sample(qml.Hermitian(herm, wires=0)), qml.sample(qml.PauliZ(1))
+
+        result = circuit(0, 3)
+
+        # Double-check the domain of the QNode output
+        assert np.issubdtype(result.dtype, float)
+        assert circuit.qtape.numeric_type is float
+
+    def test_multi_type_measurements_numeric_type_error(self):
+        """Test that querying the numeric type of a tape with several types of
+        measurements raises an error."""
+        a = 0.3
+        b = 0.3
+
+        with qml.tape.QuantumTape() as tape:
+            qml.RY(a, wires=[0])
+            qml.RZ(b, wires=[0])
+            qml.expval(qml.PauliZ(0))
+            qml.probs(wires=[0])
+
+        with pytest.raises(
+            TapeError,
+            match="Getting the numeric type of a tape that contains multiple types of measurements is unsupported.",
+        ):
+            tape.numeric_type
+
+
 class TestTapeDraw:
     """Test the tape draw method."""
 
diff --git a/tests/test_measurements.py b/tests/test_measurements.py
index 47b53ece8..a4f5c576e 100644
--- a/tests/test_measurements.py
+++ b/tests/test_measurements.py
@@ -38,6 +38,7 @@ from pennylane.measurements import (
     MeasurementProcess,
     MeasurementValue,
     MeasurementValueError,
+    MeasurementShapeError,
 )
 
 
@@ -120,6 +121,35 @@ class TestExpval:
 
         circuit()
 
+    @pytest.mark.parametrize(
+        "obs",
+        [qml.PauliZ(0), qml.Hermitian(np.diag([1, 2]), 0), qml.Hermitian(np.diag([1.0, 2.0]), 0)],
+    )
+    def test_numeric_type(self, obs):
+        """Test that the numeric type is correct."""
+        res = qml.expval(obs)
+        assert res.numeric_type is float
+
+    @pytest.mark.parametrize(
+        "obs",
+        [qml.PauliZ(0), qml.Hermitian(np.diag([1, 2]), 0), qml.Hermitian(np.diag([1.0, 2.0]), 0)],
+    )
+    def test_shape(self, obs):
+        """Test that the shape is correct."""
+        res = qml.expval(obs)
+        assert res.shape() == (1,)
+
+    @pytest.mark.parametrize(
+        "obs",
+        [qml.PauliZ(0), qml.Hermitian(np.diag([1, 2]), 0), qml.Hermitian(np.diag([1.0, 2.0]), 0)],
+    )
+    def test_shape_shot_vector(self, obs):
+        """Test that the shape is correct with the shot vector too."""
+        res = qml.expval(obs)
+        shot_vector = (1, 2, 3)
+        dev = qml.device("default.qubit", wires=3, shots=shot_vector)
+        assert res.shape(dev) == (len(shot_vector),)
+
 
 class TestVar:
     """Tests for the var function"""
@@ -167,6 +197,74 @@ class TestVar:
 
         circuit()
 
+    @pytest.mark.parametrize(
+        "obs",
+        [qml.PauliZ(0), qml.Hermitian(np.diag([1, 2]), 0), qml.Hermitian(np.diag([1.0, 2.0]), 0)],
+    )
+    def test_numeric_type(self, obs):
+        """Test that the numeric type is correct."""
+        res = qml.var(obs)
+        assert res.numeric_type is float
+
+    @pytest.mark.parametrize(
+        "obs",
+        [qml.PauliZ(0), qml.Hermitian(np.diag([1, 2]), 0), qml.Hermitian(np.diag([1.0, 2.0]), 0)],
+    )
+    def test_shape(self, obs):
+        """Test that the shape is correct."""
+        res = qml.var(obs)
+        assert res.shape() == (1,)
+
+    @pytest.mark.parametrize(
+        "obs",
+        [qml.PauliZ(0), qml.Hermitian(np.diag([1, 2]), 0), qml.Hermitian(np.diag([1.0, 2.0]), 0)],
+    )
+    def test_shape_shot_vector(self, obs):
+        """Test that the shape is correct with the shot vector too."""
+        res = qml.var(obs)
+        shot_vector = (1, 2, 3)
+        dev = qml.device("default.qubit", wires=3, shots=shot_vector)
+        assert res.shape(dev) == (len(shot_vector),)
+
+
+class TestProbs:
+    """Tests for the probs function"""
+
+    @pytest.mark.parametrize("wires", [[0], [2, 1], ["a", "c", 3]])
+    def test_numeric_type(self, wires):
+        """Test that the numeric type is correct."""
+        res = qml.probs(wires=wires)
+        assert res.numeric_type is float
+
+    @pytest.mark.parametrize("wires", [[0], [2, 1], ["a", "c", 3]])
+    @pytest.mark.parametrize("shots", [None, 10])
+    def test_shape(self, wires, shots):
+        """Test that the shape is correct."""
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+        res = qml.probs(wires=wires)
+        assert res.shape(dev) == (1, 2 ** len(wires))
+
+    @pytest.mark.parametrize("wires", [[0], [2, 1], ["a", "c", 3]])
+    def test_shape_shot_vector(self, wires):
+        """Test that the shape is correct with the shot vector too."""
+        res = qml.probs(wires=wires)
+        shot_vector = (1, 2, 3)
+        dev = qml.device("default.qubit", wires=3, shots=shot_vector)
+        assert res.shape(dev) == (len(shot_vector), 2 ** len(wires))
+
+    @pytest.mark.parametrize(
+        "measurement",
+        [qml.probs(wires=[0]), qml.state(), qml.sample(qml.PauliZ(0))],
+    )
+    def test_shape_no_device_error(self, measurement):
+        """Test that an error is raised if a device is not passed when querying
+        the shape of certain measurements."""
+        with pytest.raises(
+            MeasurementShapeError,
+            match="requires the device argument to be passed to obtain the shape",
+        ):
+            measurement.shape()
+
 
 class TestSample:
     """Tests for the sample function"""
@@ -333,6 +431,80 @@ class TestSample:
 
         circuit()
 
+    @pytest.mark.parametrize(
+        "obs,exp",
+        [
+            # Single observables
+            (None, int),  # comp basis samples
+            (qml.PauliX(0), int),
+            (qml.PauliY(0), int),
+            (qml.PauliZ(0), int),
+            (qml.Hadamard(0), int),
+            (qml.Identity(0), int),
+            (qml.Hermitian(np.diag([1, 2]), 0), float),
+            (qml.Hermitian(np.diag([1.0, 2.0]), 0), float),
+            # Tensor product observables
+            (
+                qml.PauliX("c")
+                @ qml.PauliY("a")
+                @ qml.PauliZ(1)
+                @ qml.Hadamard("wire1")
+                @ qml.Identity("b"),
+                int,
+            ),
+            (qml.Projector([0, 1], wires=[0, 1]) @ qml.PauliZ(2), float),
+            (qml.Hermitian(np.array(np.eye(2)), wires=[0]) @ qml.PauliZ(2), float),
+            (
+                qml.Projector([0, 1], wires=[0, 1]) @ qml.Hermitian(np.array(np.eye(2)), wires=[2]),
+                float,
+            ),
+        ],
+    )
+    def test_numeric_type(self, obs, exp):
+        """Test that the numeric type is correct."""
+        res = qml.sample(obs) if obs is not None else qml.sample()
+        assert res.numeric_type is exp
+
+    @pytest.mark.parametrize(
+        "obs",
+        [
+            None,
+            qml.PauliZ(0),
+            qml.Hermitian(np.diag([1, 2]), 0),
+            qml.Hermitian(np.diag([1.0, 2.0]), 0),
+        ],
+    )
+    def test_shape(self, obs):
+        """Test that the shape is correct."""
+        shots = 10
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+        res = qml.sample(obs) if obs is not None else qml.sample()
+        expected = (1, shots) if obs is not None else (1, shots, 3)
+        assert res.shape(dev) == expected
+
+    @pytest.mark.parametrize(
+        "obs",
+        [qml.PauliZ(0), qml.Hermitian(np.diag([1, 2]), 0), qml.Hermitian(np.diag([1.0, 2.0]), 0)],
+    )
+    def test_shape_shot_vector(self, obs):
+        """Test that the shape is correct with the shot vector too."""
+        shot_vector = (1, 2, 3)
+        dev = qml.device("default.qubit", wires=3, shots=shot_vector)
+        res = qml.sample(obs)
+        expected = ((), (2,), (3,))
+        assert res.shape(dev) == expected
+
+    def test_shape_shot_vector_no_obs(self):
+        """Test that the shape is correct with the shot vector too."""
+        shot_vector = (1, 2, 3)
+        dev = qml.device("default.qubit", wires=3, shots=shot_vector)
+        res = qml.sample()
+        with pytest.raises(
+            qml.measurements.MeasurementShapeError,
+            match="Getting the output shape of a measurement returning samples along with a device with a shot vector is not supported.",
+        ):
+            res.shape(dev)
+
 
 class TestMeasure:
     """Tests for the measure function"""
@@ -927,6 +1099,39 @@ class TestState:
         with pytest.raises(qml.QuantumFunctionError, match="custom wire labels"):
             func()
 
+    @pytest.mark.parametrize("shots", [None, 1, 10])
+    def test_shape(self, shots):
+        """Test that the shape is correct for qml.state."""
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+        res = qml.state()
+        assert res.shape(dev) == (1, 2**3)
+
+    @pytest.mark.parametrize("s_vec", [(3, 2, 1), (1, 5, 10), (3, 1, 20)])
+    def test_shape_shot_vector(self, s_vec):
+        """Test that the shape is correct for qml.state with the shot vector too."""
+        dev = qml.device("default.qubit", wires=3, shots=s_vec)
+        res = qml.state()
+        assert res.shape(dev) == (3, 2**3)
+
+    @pytest.mark.parametrize("shots", [None, 1, 10])
+    def test_shape(self, shots):
+        """Test that the shape is correct for qml.density_matrix."""
+        dev = qml.device("default.qubit", wires=3, shots=shots)
+        res = qml.density_matrix(wires=[0, 1])
+        assert res.shape(dev) == (1, 2**2, 2**2)
+
+    @pytest.mark.parametrize("s_vec", [(3, 2, 1), (1, 5, 10), (3, 1, 20)])
+    def test_shape_shot_vector(self, s_vec):
+        """Test that the shape is correct for qml.density_matrix with the shot vector too."""
+        dev = qml.device("default.qubit", wires=3, shots=s_vec)
+        res = qml.density_matrix(wires=[0, 1])
+        assert res.shape(dev) == (3, 2**2, 2**2)
+
+    def test_numeric_type(self):
+        """Test that the numeric type of state measurements."""
+        assert qml.state().numeric_type == complex
+        assert qml.density_matrix(wires=[0, 1]).numeric_type == complex
+
 
 class TestDensityMatrix:
     """Tests for the density matrix function"""