Repository: PennyLaneAI/pennylane
Pull request #7320: Remove keras layer
Author: login: JerryChen97
Labels: name: review-ready :ok_hand:
description: PRs which are ready for review by someone from the core team.
Created: 2025-04-22T15:01:06Z | Merged: 2025-04-24T15:07:02Z

**Context:**
We are to finish the deprecation cycle for `KerasLayer`. Note that this does not mean that we are fully removing Keras from our path. There is [another PR ](https://github.com/PennyLaneAI/pennylane/pull/7272)ongoing to introduce a compatible version `KerasLayer` for Keras3; also, several other modules depend on keras but not involving `KerasLayer`.

**Description of the Change:**
Deletion of source code file along with test file.
Removal of necessary special treatment in the middle of pipeline.

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**
[sc-89146]


Diff:
diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst
index ae95f05364a..cacb96fc803 100644
--- a/doc/development/deprecations.rst
+++ b/doc/development/deprecations.rst
@@ -14,11 +14,6 @@ Pending deprecations
   - Deprecated in v0.42
   - Will be removed in v0.43
 
-* The ``KerasLayer`` in ``qml.qnn.keras`` is deprecated because Keras 2 is no longer actively maintained.  Please consider using a different machine learning framework instead of ``TensorFlow/Keras 2``.
-
-  - Deprecated in v0.41
-  - Will be removed in v0.42
-
 * The property ```MeasurementProcess.return_type``` has been deprecated.
   If observable type checking is needed, please use direct ```isinstance```; if other text information is needed, please use class name, or another internal temporary private member ``_shortname``.
 
@@ -65,6 +60,12 @@ for details on how to port your legacy code to the new system. The following fun
 Completed deprecation cycles
 ----------------------------
 
+* The ``KerasLayer`` class in ``qml.qnn.keras`` has been removed because Keras 2 is no longer actively maintained.
+  Please consider using a different machine learning framework, like :doc:`PyTorch <demos/tutorial_qnn_module_torch>` or :doc:`JAX <demos/tutorial_How_to_optimize_QML_model_using_JAX_and_Optax>`.
+
+  - Deprecated in v0.41
+  - Removed in v0.42
+
 * The ``qml.gradients.hamiltonian_grad`` function has been removed because this gradient recipe is no
   longer required with the :doc:`new operator arithmetic system </news/new_opmath>`.
 
diff --git a/doc/introduction/interfaces.rst b/doc/introduction/interfaces.rst
index 5bd0d8623ec..4886d8616e5 100644
--- a/doc/introduction/interfaces.rst
+++ b/doc/introduction/interfaces.rst
@@ -109,14 +109,13 @@ See the links below for walkthroughs of each specific interface:
     </div>
 
 In addition to the core automatic differentiation frameworks discussed above,
-PennyLane also provides higher-level classes for converting QNodes into both Keras and ``torch.nn`` layers:
+PennyLane also provides higher-level classes for converting QNodes into ``torch.nn`` layers:
 
 
 :html:`<div class="summary-table">`
 
 .. autosummary::
 
-    pennylane.qnn.KerasLayer
     pennylane.qnn.TorchLayer
 
 
diff --git a/doc/introduction/interfaces/tf.rst b/doc/introduction/interfaces/tf.rst
index 944ae2dc174..217f37f4a9e 100644
--- a/doc/introduction/interfaces/tf.rst
+++ b/doc/introduction/interfaces/tf.rst
@@ -193,11 +193,3 @@ The final weights and circuit value are:
 <tf.Variable 'Variable:0' shape=() dtype=float64, numpy=0.20000000000000001>
 >>> circuit4(phi, theta)
 <tf.Tensor: id=106269, shape=(), dtype=float64, numpy=0.5000000000000091>
-
-Keras integration
------------------
-
-Once you have a TensorFlow-compaible QNode, it is easy to convert this into a Keras layer. To
-help automate this process, PennyLane also provides a :class:`~.qnn.KerasLayer` class to easily
-convert a QNode to a Keras layer. Please see the corresponding :class:`~.qnn.KerasLayer`
-documentation for more details and examples.
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 64fd23e85b8..cf8b7ad3824 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -19,6 +19,10 @@
 
 <h3>Breaking changes 💔</h3>
 
+* The `KerasLayer` class in `qml.qnn.keras` has been removed because Keras 2 is no longer actively maintained.
+  Please consider using a different machine learning framework, like :doc:`PyTorch <demos/tutorial_qnn_module_torch>` or :doc:`JAX <demos/tutorial_How_to_optimize_QML_model_using_JAX_and_Optax>`.
+  [(#7320)](https://github.com/PennyLaneAI/pennylane/pull/7320)
+
 * The `qml.gradients.hamiltonian_grad` function has been removed because this gradient recipe is no
   longer required with the :doc:`new operator arithmetic system </news/new_opmath>`.
   [(#7302)](https://github.com/PennyLaneAI/pennylane/pull/7302)
diff --git a/pennylane/qnn/__init__.py b/pennylane/qnn/__init__.py
index 973f41aad8f..6bc35152fd9 100644
--- a/pennylane/qnn/__init__.py
+++ b/pennylane/qnn/__init__.py
@@ -13,12 +13,11 @@
 # limitations under the License.
 """
 This module contains functionality for converting PennyLane QNodes into layers that are compatible
-with Keras and PyTorch.
+with PyTorch.
 
 .. note::
 
-    Check out our :doc:`Keras <demos/qnn_module_tf>` and
-    :doc:`Torch <demos/tutorial_qnn_module_torch>` tutorials for further details.
+    Check out our :doc:`Torch <demos/tutorial_qnn_module_torch>` tutorials for further details.
 
 
 .. rubric:: Classes
@@ -28,19 +27,18 @@
     :nosignatures:
     :template: autosummary/class_no_inherited.rst
 
-    ~KerasLayer
     ~TorchLayer
 """
 import importlib
 
 from . import cost
 
-class_map = {"KerasLayer": "keras", "TorchLayer": "torch"}
-mods = ("keras", "torch")
+class_map = {"TorchLayer": "torch"}
+mods = "torch"
 
 
 def __getattr__(name):
-    """Allow for lazy-loading of KerasLayer and TorchLayer so that TensorFlow and PyTorch are not
+    """Allow for lazy-loading of TorchLayer so that PyTorch are not
     automatically loaded with PennyLane"""
     if name in class_map:
         mod = importlib.import_module("." + class_map[name], __name__)
diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py
deleted file mode 100644
index 6340389d74d..00000000000
--- a/pennylane/qnn/keras.py
+++ /dev/null
@@ -1,543 +0,0 @@
-# Copyright 2018-2024 Xanadu Quantum Technologies Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""This module contains the classes and functions for integrating QNodes with the Keras Layer
-API."""
-import inspect
-import warnings
-from collections.abc import Iterable
-from typing import Optional, Text
-
-from packaging.version import Version
-
-import pennylane as qml
-
-try:
-    import tensorflow as tf
-    from tensorflow.keras.layers import Layer
-
-    CORRECT_TF_VERSION = Version(tf.__version__) >= Version("2.0.0")
-    try:
-        # this feels a bit hacky, but if users *only* have an old (i.e. PL-compatible) version of Keras installed
-        # then tf.keras doesn't have a version attribute, and we *should be* good to go.
-        # if you have a newer version of Keras installed, then you can use tf.keras.version to check if you
-        # are configured to use Keras 3 or Keras 2
-        CORRECT_KERAS_VERSION = Version(tf.keras.version()) < Version("3.0.0")
-    except AttributeError:
-        CORRECT_KERAS_VERSION = True
-
-except ImportError:
-    # The following allows this module to be imported even if TensorFlow is not installed. Users
-    # will instead see an ImportError when instantiating the KerasLayer.
-    from abc import ABC
-
-    Layer = ABC
-    CORRECT_TF_VERSION = False
-
-
-class KerasLayer(Layer):
-    """Converts a :class:`~.QNode` to a Keras
-    `Layer <https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer>`__.
-
-    The result can be used within the Keras
-    `Sequential <https://www.tensorflow.org/api_docs/python/tf/keras/Sequential>`__ or
-    `Model <https://www.tensorflow.org/api_docs/python/tf/keras/Model>`__ classes for
-    creating quantum and hybrid models.
-
-    .. warning::
-        This class is deprecated because Keras 2 is no longer actively maintained.  Please consider using torch instead of TensorFlow/Keras 2.
-
-    .. note::
-
-        ``KerasLayer`` currently only supports Keras 2. If you are running the newest version
-        of TensorFlow and Keras, you may automatically be using Keras 3. For instructions
-        on running with Keras 2, instead, see the
-        `documentation on backwards compatibility <https://keras.io/getting_started/#tensorflow--keras-2-backwards-compatibility>`__ .
-
-    Args:
-        qnode (qml.QNode): the PennyLane QNode to be converted into a Keras Layer_
-        weight_shapes (dict[str, tuple]): a dictionary mapping from all weights used in the QNode to
-            their corresponding shapes
-        output_dim (int): the output dimension of the QNode
-        weight_specs (dict[str, dict]): An optional dictionary for users to provide additional
-            specifications for weights used in the QNode, such as the method of parameter
-            initialization. This specification is provided as a dictionary with keys given by the
-            arguments of the `add_weight()
-            <https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer#add_weight>`__
-            method and values being the corresponding specification.
-        **kwargs: additional keyword arguments passed to the Layer_ base class
-
-    **Example**
-
-    First let's define the QNode that we want to convert into a Keras Layer_:
-
-    .. code-block:: python
-
-        n_qubits = 2
-        dev = qml.device("default.qubit", wires=n_qubits)
-
-        @qml.qnode(dev)
-        def qnode(inputs, weights_0, weight_1):
-            qml.RX(inputs[0], wires=0)
-            qml.RX(inputs[1], wires=1)
-            qml.Rot(*weights_0, wires=0)
-            qml.RY(weight_1, wires=1)
-            qml.CNOT(wires=[0, 1])
-            return qml.expval(qml.Z(0)), qml.expval(qml.Z(1))
-
-    The signature of the QNode **must** contain an ``inputs`` named argument for input data,
-    with all other arguments to be treated as internal weights. We can then convert to a Keras
-    Layer_ with:
-
-    >>> weight_shapes = {"weights_0": 3, "weight_1": 1}
-    >>> qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2)
-
-    The internal weights of the QNode are automatically initialized within the
-    :class:`~.KerasLayer` and must have their shapes specified in a ``weight_shapes`` dictionary.
-    It is then easy to combine with other neural network layers from the
-    `tensorflow.keras.layers <https://www.tensorflow.org/api_docs/python/tf/keras/layers>`__ module
-    and create a hybrid:
-
-    >>> clayer = tf.keras.layers.Dense(2)
-    >>> model = tf.keras.models.Sequential([qlayer, clayer])
-
-    .. details::
-        :title: Usage Details
-
-        **QNode signature**
-
-        The QNode must have a signature that satisfies the following conditions:
-
-        - Contain an ``inputs`` named argument for input data.
-        - All other arguments must accept an array or tensor and are treated as internal
-          weights of the QNode.
-        - All other arguments must have no default value.
-        - The ``inputs`` argument is permitted to have a default value provided the gradient with
-          respect to ``inputs`` is not required.
-        - There cannot be a variable number of positional or keyword arguments, e.g., no ``*args``
-          or ``**kwargs`` present in the signature.
-
-        **Output shape**
-
-        If the QNode returns a single measurement, then the output of the ``KerasLayer`` will have
-        shape ``(batch_dim, *measurement_shape)``, where ``measurement_shape`` is the output shape
-        of the measurement:
-
-        .. code-block::
-
-            def print_output_shape(measurements):
-                n_qubits = 2
-                dev = qml.device("default.qubit", wires=n_qubits, shots=100)
-
-                @qml.qnode(dev)
-                def qnode(inputs, weights):
-                    qml.templates.AngleEmbedding(inputs, wires=range(n_qubits))
-                    qml.templates.StronglyEntanglingLayers(weights, wires=range(n_qubits))
-                    if len(measurements) == 1:
-                        return qml.apply(measurements[0])
-                    return [qml.apply(m) for m in measurements]
-
-                weight_shapes = {"weights": (3, n_qubits, 3)}
-                qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=None)
-
-                batch_dim = 5
-                x = tf.zeros((batch_dim, n_qubits))
-                return qlayer(x).shape
-
-        >>> print_output_shape([qml.expval(qml.Z(0))])
-        TensorShape([5])
-        >>> print_output_shape([qml.probs(wires=[0, 1])])
-        TensorShape([5, 4])
-        >>> print_output_shape([qml.sample(wires=[0, 1])])
-        TensorShape([5, 100, 2])
-
-        If the QNode returns multiple measurements, then the measurement results will be flattened
-        and concatenated, resulting in an output of shape ``(batch_dim, total_flattened_dim)``:
-
-        >>> print_output_shape([qml.expval(qml.Z(0)), qml.probs(wires=[0, 1])])
-        TensorShape([5, 5])
-        >>> print_output_shape([qml.probs([0, 1]), qml.sample(wires=[0, 1])])
-        TensorShape([5, 204])
-
-        **Initializing weights**
-
-        The optional ``weight_specs`` argument of :class:`~.KerasLayer` allows for a more
-        fine-grained specification of the QNode weights, such as the method of initialization and
-        any regularization or constraints. For example, the initialization method of the ``weights``
-        argument in the example above could be specified by:
-
-        .. code-block::
-
-            weight_specs = {"weights": {"initializer": "random_uniform"}}
-
-        The values of ``weight_specs`` are dictionaries with keys given by arguments of
-        the Keras
-        `add_weight() <https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer#add_weight>`__
-        method. For the ``"initializer"`` argument, one can specify a string such as
-        ``"random_uniform"`` or an instance of an `Initializer
-        <https://www.tensorflow.org/api_docs/python/tf/keras/initializers>`__ class, such as
-        `tf.keras.initializers.RandomUniform <https://www.tensorflow.org/api_docs/python/tf/random_uniform_initializer>`__.
-
-        If ``weight_specs`` is not specified, weights will be added using the Keras default
-        initialization and without any regularization or constraints.
-
-        **Model saving**
-
-        The weights of models that contain ``KerasLayers`` can be saved using the usual
-        ``tf.keras.Model.save_weights`` method:
-
-        .. code-block::
-
-            clayer = tf.keras.layers.Dense(2, input_shape=(2,))
-            qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2)
-            model = tf.keras.Sequential([clayer, qlayer])
-            model.save_weights(SAVE_PATH)
-
-        To load the model weights, first instantiate the model like before, then call
-        ``tf.keras.Model.load_weights``:
-
-        .. code-block::
-
-            clayer = tf.keras.layers.Dense(2, input_shape=(2,))
-            qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2)
-            model = tf.keras.Sequential([clayer, qlayer])
-            model.load_weights(SAVE_PATH)
-
-        Models containing ``KerasLayer`` objects can also be saved directly using
-        ``tf.keras.Model.save``. This method also saves the model architecture, weights,
-        and training configuration, including the optimizer state:
-
-        .. code-block::
-
-            clayer = tf.keras.layers.Dense(2, input_shape=(2,))
-            qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2)
-            model = tf.keras.Sequential([clayer, qlayer])
-            model.save(SAVE_PATH)
-
-        In this case, loading the model requires no knowledge of the original source code:
-
-        .. code-block::
-
-            model = tf.keras.models.load_model(SAVE_PATH)
-
-        .. note::
-
-            Currently ``KerasLayer`` objects cannot be saved in the ``HDF5`` file format. In order
-            to save a model using the latter method above, the ``SavedModel`` file format (default
-            in TensorFlow 2.x) should be used.
-
-        **Additional example**
-
-        The code block below shows how a circuit composed of templates from the
-        :doc:`/introduction/templates` module can be combined with classical
-        `Dense <https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dense>`__ layers to learn
-        the two-dimensional `moons <https://scikit-learn.org/stable/modules/generated/sklearn
-        .datasets.make_moons.html>`__ dataset.
-
-        .. code-block:: python
-
-            import pennylane as qml
-            import tensorflow as tf
-            import sklearn.datasets
-
-            n_qubits = 2
-            dev = qml.device("default.qubit", wires=n_qubits)
-
-            @qml.qnode(dev)
-            def qnode(inputs, weights):
-                qml.templates.AngleEmbedding(inputs, wires=range(n_qubits))
-                qml.templates.StronglyEntanglingLayers(weights, wires=range(n_qubits))
-                return qml.expval(qml.Z(0)), qml.expval(qml.Z(1))
-
-            weight_shapes = {"weights": (3, n_qubits, 3)}
-
-            qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2)
-            clayer1 = tf.keras.layers.Dense(2)
-            clayer2 = tf.keras.layers.Dense(2, activation="softmax")
-            model = tf.keras.models.Sequential([clayer1, qlayer, clayer2])
-
-            data = sklearn.datasets.make_moons()
-            X = tf.constant(data[0])
-            Y = tf.one_hot(data[1], depth=2)
-
-            opt = tf.keras.optimizers.SGD(learning_rate=0.5)
-            model.compile(opt, loss='mae')
-
-        The model can be trained using:
-
-        >>> model.fit(X, Y, epochs=8, batch_size=5)
-        Train on 100 samples
-        Epoch 1/8
-        100/100 [==============================] - 9s 90ms/sample - loss: 0.3524
-        Epoch 2/8
-        100/100 [==============================] - 9s 87ms/sample - loss: 0.2441
-        Epoch 3/8
-        100/100 [==============================] - 9s 87ms/sample - loss: 0.1908
-        Epoch 4/8
-        100/100 [==============================] - 9s 87ms/sample - loss: 0.1832
-        Epoch 5/8
-        100/100 [==============================] - 9s 88ms/sample - loss: 0.1596
-        Epoch 6/8
-        100/100 [==============================] - 9s 87ms/sample - loss: 0.1637
-        Epoch 7/8
-        100/100 [==============================] - 9s 86ms/sample - loss: 0.1613
-        Epoch 8/8
-        100/100 [==============================] - 9s 87ms/sample - loss: 0.1474
-
-        **Returning a state**
-
-        If your QNode returns the state of the quantum circuit using :func:`~pennylane.state` or
-        :func:`~pennylane.density_matrix`, you must immediately follow your quantum Keras Layer with a layer
-        that casts to reals. For example, you could use
-        `tf.keras.layers.Lambda <https://www.tensorflow.org/api_docs/python/tf/keras/layers/Lambda>`__
-        with the function ``lambda x: tf.abs(x)``. This casting is required because TensorFlow's
-        Keras layers require a real input and are differentiated with respect to real parameters.
-
-    .. _Layer: https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer
-    """
-
-    def __init__(
-        self,
-        qnode,
-        weight_shapes: dict,
-        output_dim,
-        weight_specs: Optional[dict] = None,
-        **kwargs,
-    ):
-        warnings.warn(
-            "The 'KerasLayer' class is deprecated and will be removed in v0.42. ",
-            qml.PennyLaneDeprecationWarning,
-        )
-        # pylint: disable=too-many-arguments
-        if not CORRECT_TF_VERSION:
-            raise ImportError(
-                "KerasLayer requires TensorFlow version 2 or above. The latest "
-                "version of TensorFlow can be installed using:\n"
-                "pip install tensorflow --upgrade\nAlternatively, visit "
-                "https://www.tensorflow.org/install for detailed instructions."
-            )
-
-        self.weight_shapes = {
-            weight: (tuple(size) if isinstance(size, Iterable) else (size,) if size > 1 else ())
-            for weight, size in weight_shapes.items()
-        }
-
-        self._signature_validation(qnode, weight_shapes)
-
-        self.qnode = qnode
-        if self.qnode.interface not in (
-            "auto",
-            "tf",
-            "tensorflow",
-            "tensorflow-autograph",
-            "tf-autograph",
-        ):
-            raise ValueError(f"Invalid interface '{self.qnode.interface}' for KerasLayer")
-
-        # Allows output_dim to be specified as an int or as a tuple, e.g, 5, (5,), (5, 2), [5, 2]
-        # Note: Single digit values will be considered an int and multiple as a tuple, e.g [5,] or (5,)
-        # are passed as integer 5 and [5, 2] will be passes as tuple (5, 2)
-        if isinstance(output_dim, Iterable) and len(output_dim) > 1:
-            self.output_dim = tuple(output_dim)
-        else:
-            self.output_dim = output_dim[0] if isinstance(output_dim, Iterable) else output_dim
-
-        self.weight_specs = weight_specs if weight_specs is not None else {}
-
-        self.qnode_weights = {}
-
-        if CORRECT_KERAS_VERSION:
-            super().__init__(dynamic=True, **kwargs)
-        else:  # pragma: no cover
-            super().__init__(**kwargs)
-
-        # no point in delaying the initialization of weights, since we already know their shapes
-        self.build(None)
-        self._initialized = True
-
-    def _signature_validation(self, qnode, weight_shapes):
-        sig = inspect.signature(qnode.func).parameters
-
-        if self.input_arg not in sig:
-            raise TypeError(
-                f"QNode must include an argument with name {self.input_arg} for inputting data"
-            )
-
-        if self.input_arg in set(weight_shapes.keys()):
-            raise ValueError(
-                f"{self.input_arg} argument should not have its dimension specified in "
-                f"weight_shapes"
-            )
-
-        param_kinds = [p.kind for p in sig.values()]
-
-        if inspect.Parameter.VAR_POSITIONAL in param_kinds:
-            raise TypeError("Cannot have a variable number of positional arguments")
-
-        if inspect.Parameter.VAR_KEYWORD not in param_kinds:
-            if set(weight_shapes.keys()) | {self.input_arg} != set(sig.keys()):
-                raise ValueError("Must specify a shape for every non-input parameter in the QNode")
-
-    def build(self, input_shape):
-        """Initializes the QNode weights.
-
-        Args:
-            input_shape (tuple or tf.TensorShape): shape of input data; this is unused since
-                the weight shapes are already known in the __init__ method.
-        """
-        for weight, size in self.weight_shapes.items():
-            spec = self.weight_specs.get(weight, {})
-            self.qnode_weights[weight] = self.add_weight(name=weight, shape=size, **spec)
-
-        super().build(input_shape)
-
-    def call(self, inputs):
-        """Evaluates the QNode on input data using the initialized weights.
-
-        Args:
-            inputs (tensor): data to be processed
-
-        Returns:
-            tensor: output data
-        """
-        has_batch_dim = len(inputs.shape) > 1
-
-        # in case the input has more than one batch dimension
-        if has_batch_dim:
-            batch_dims = tf.shape(inputs)[:-1]
-            inputs = tf.reshape(inputs, (-1, inputs.shape[-1]))
-
-        # calculate the forward pass as usual
-        results = self._evaluate_qnode(inputs)
-
-        # reshape to the correct number of batch dims
-        if has_batch_dim:
-            # pylint:disable=unexpected-keyword-arg,no-value-for-parameter
-            new_shape = tf.concat([batch_dims, tf.shape(results)[1:]], axis=0)
-            results = tf.reshape(results, new_shape)
-
-        return results
-
-    def _evaluate_qnode(self, x):
-        """Evaluates a QNode for a single input datapoint.
-
-        Args:
-            x (tensor): the datapoint
-
-        Returns:
-            tensor: output datapoint
-        """
-        kwargs = {
-            **{self.input_arg: x},
-            **{k: 1.0 * w for k, w in self.qnode_weights.items()},
-        }
-        res = self.qnode(**kwargs)
-
-        if isinstance(res, (list, tuple)):
-            if len(x.shape) > 1:
-                # multi-return and batch dim case
-                res = [tf.reshape(r, (tf.shape(x)[0], tf.reduce_prod(r.shape[1:]))) for r in res]
-
-            # multi-return and no batch dim
-            return tf.experimental.numpy.hstack(res)
-
-        return res
-
-    def construct(self, args, kwargs):
-        """Constructs the wrapped QNode on input data using the initialized weights.
-
-        This method was added to match the QNode interface. The provided args
-        must contain a single item, which is the input to the layer. The provided
-        kwargs is unused.
-
-        Args:
-            args (tuple): A tuple containing one entry that is the input to this layer
-            kwargs (dict): Unused
-        """
-        # GradientTape required to ensure that the weights show up as trainable on the qtape
-        with tf.GradientTape() as tape:
-            tape.watch(list(self.qnode_weights.values()))
-
-            inputs = args[0]
-            kwargs = {self.input_arg: inputs, **{k: 1.0 * w for k, w in self.qnode_weights.items()}}
-            self.qnode.construct((), kwargs)
-
-    def __getattr__(self, item):
-        """If the given attribute does not exist in the class, look for it in the wrapped QNode."""
-        if self._initialized and hasattr(self.qnode, item):
-            return getattr(self.qnode, item)
-
-        return super().__getattr__(item)
-
-    def __setattr__(self, item, val):
-        """If the given attribute does not exist in the class, try to set it in the wrapped QNode."""
-        if self._initialized and hasattr(self.qnode, item):
-            setattr(self.qnode, item, val)
-        else:
-            super().__setattr__(item, val)
-
-    def compute_output_shape(self, input_shape):
-        """Computes the output shape after passing data of shape ``input_shape`` through the
-        QNode.
-
-        Args:
-            input_shape (tuple or tf.TensorShape): shape of input data
-
-        Returns:
-            tf.TensorShape: shape of output data
-        """
-        return tf.TensorShape([input_shape[0]]).concatenate(self.output_dim)
-
-    def __str__(self):
-        detail = "<Quantum Keras Layer: func={}>"
-        return detail.format(self.qnode.func.__name__)
-
-    __repr__ = __str__
-
-    _input_arg = "inputs"
-    _initialized = False
-
-    @property
-    def input_arg(self):
-        """Name of the argument to be used as the input to the Keras
-        `Layer <https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer>`__. Set to
-        ``"inputs"``."""
-        return self._input_arg
-
-    @staticmethod
-    def set_input_argument(input_name: Text = "inputs") -> None:
-        """
-        Set the name of the input argument.
-
-        Args:
-            input_name (str): Name of the input argument
-        """
-        KerasLayer._input_arg = input_name
-
-    def get_config(self) -> dict:
-        """
-        Get serialized layer configuration
-
-        Returns:
-            dict: layer configuration
-        """
-        config = super().get_config()
-
-        config.update(
-            {
-                "output_dim": self.output_dim,
-                "weight_specs": self.weight_specs,
-                "weight_shapes": self.weight_shapes,
-            }
-        )
-        return config
diff --git a/pennylane/workflow/construct_batch.py b/pennylane/workflow/construct_batch.py
index 00c5eea7fd6..b362dc41551 100644
--- a/pennylane/workflow/construct_batch.py
+++ b/pennylane/workflow/construct_batch.py
@@ -14,7 +14,6 @@
 """Contains a function extracting the tapes at postprocessing at any stage of a transform program."""
 import inspect
 from collections.abc import Callable
-from contextlib import nullcontext
 from functools import wraps
 from typing import Literal, Union
 
@@ -228,9 +227,8 @@ def circuit():
     return resolved_program
 
 
-# !TODO: remove KerasLayer in 0.42
 def construct_batch(
-    qnode: Union[QNode, "qml.qnn.KerasLayer", "qml.qnn.TorchLayer"],
+    qnode: Union[QNode, "qml.qnn.TorchLayer"],
     level: Union[Literal["top", "user", "device", "gradient"], int, slice, None] = "user",
 ) -> Callable:
     """Construct the batch of tapes and post processing for a designated stage in the transform program.
@@ -339,36 +337,16 @@ def batch_constructor(*args, **kwargs) -> tuple[QuantumScriptBatch, Postprocessi
         else:
             shots = kwargs.pop("shots", qnode.device.shots)
 
-        context_fn = nullcontext
-
-        # !TODO: remove KerasLayer in 0.42
-        if type(qnode).__name__ == "KerasLayer":
-            # note that calling qml.qnn.KerasLayer pulls in a tf import
-            # pylint: disable=import-outside-toplevel
-            import tensorflow as tf
-
-            context_fn = tf.GradientTape
-
-        elif type(qnode).__name__ == "TorchLayer":
+        if type(qnode).__name__ == "TorchLayer":
             # avoid triggering import of torch if its not needed.
             x = args[0]
             kwargs = {
                 **{arg: weight.to(x) for arg, weight in qnode.qnode_weights.items()},
             }
 
-        with context_fn() as cntxt:
-            # If TF tape, use the watch function
-            if hasattr(cntxt, "watch"):
-                cntxt.watch(list(qnode.qnode_weights.values()))
-
-                kwargs = {
-                    **{k: 1.0 * w for k, w in qnode.qnode_weights.items()},
-                    **kwargs,
-                }
-
-            initial_tape = qml.tape.make_qscript(qnode.func, shots=shots)(*args, **kwargs)
-            params = initial_tape.get_parameters(trainable_only=False)
-            initial_tape.trainable_params = qml.math.get_trainable_indices(params)
+        initial_tape = qml.tape.make_qscript(qnode.func, shots=shots)(*args, **kwargs)
+        params = initial_tape.get_parameters(trainable_only=False)
+        initial_tape.trainable_params = qml.math.get_trainable_indices(params)
 
         config = qml.workflow.construct_execution_config(qnode, resolve=False)(*args, **kwargs)
         # pylint: disable = protected-access
diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py
deleted file mode 100644
index cf0c3047d16..00000000000
--- a/tests/qnn/test_keras.py
+++ /dev/null
@@ -1,1164 +0,0 @@
-# Copyright 2018-2020 Xanadu Quantum Technologies Inc.
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-#     http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""
-Tests for the pennylane.qnn.keras module.
-"""
-from collections import defaultdict
-
-import numpy as np
-import pytest
-from packaging.version import Version
-
-import pennylane as qml
-
-KerasLayer = qml.qnn.keras.KerasLayer
-
-tf = pytest.importorskip("tensorflow", minversion="2")
-# Check for Keras 3
-try:
-    # Check TensorFlow version - 2.16+ uses Keras 3 by default
-    USING_KERAS3 = Version(tf.__version__) >= Version("2.16.0")
-
-    # Alternative check using keras.version if available
-    try:
-        import keras
-
-        if hasattr(keras, "version"):
-            USING_KERAS3 = Version(keras.version()) >= Version("3.0.0")
-    except (ImportError, AttributeError):
-        pass
-
-except ImportError:
-    USING_KERAS3 = False
-
-# Skip marker for Keras 3
-KERAS3_XFAIL_INFO = "This test requires Keras 2. Skipping for Keras 3 (TF >= 2.16) until proper support is implemented."
-# pylint: disable=unnecessary-dunder-call
-
-
-@pytest.fixture
-def model(get_circuit, n_qubits, output_dim):
-    """Fixture for creating a hybrid Keras model. The model is composed of KerasLayers sandwiched
-    between Dense layers."""
-    c, w = get_circuit
-    with pytest.warns(
-        qml.PennyLaneDeprecationWarning, match="The 'KerasLayer' class is deprecated"
-    ):
-        layer1 = KerasLayer(c, w, output_dim)
-        layer2 = KerasLayer(c, w, output_dim)
-
-    m = tf.keras.models.Sequential(
-        [
-            tf.keras.layers.Dense(n_qubits, input_shape=(n_qubits,)),
-            layer1,
-            tf.keras.layers.Dense(n_qubits),
-            layer2,
-            tf.keras.layers.Dense(output_dim),
-        ]
-    )
-
-    return m
-
-
-@pytest.fixture
-def model_dm(get_circuit_dm, n_qubits, output_dim):
-    """The Keras NN model."""
-    c, w = get_circuit_dm
-    with pytest.warns(
-        qml.PennyLaneDeprecationWarning, match="The 'KerasLayer' class is deprecated"
-    ):
-        layer1 = KerasLayer(c, w, output_dim)
-        layer2 = KerasLayer(c, w, output_dim)
-
-    m = tf.keras.models.Sequential(
-        [
-            tf.keras.layers.Dense(n_qubits, input_shape=(n_qubits,)),
-            layer1,
-            # Adding a lambda layer to take only the real values from density matrix
-            tf.keras.layers.Lambda(lambda x: tf.abs(x)),  # pylint: disable=unnecessary-lambda
-            tf.keras.layers.Flatten(),
-            tf.keras.layers.Dense(n_qubits),
-            layer2,
-            # Adding a lambda layer to take only the real values from density matrix
-            tf.keras.layers.Lambda(lambda x: tf.abs(x)),  # pylint: disable=unnecessary-lambda
-            tf.keras.layers.Flatten(),
-            tf.keras.layers.Dense(output_dim[0] * output_dim[1]),
-        ]
-    )
-
-    return m
-
-
-@pytest.fixture(autouse=True)
-def reset_keraslayer_input_arg():
-    # Reset for every test so they don't interfere
-    KerasLayer.set_input_argument("inputs")
-    yield
-    KerasLayer.set_input_argument("inputs")
-
-
-def indices_up_to(n_max):
-    """Returns an iterator over the number of qubits and output dimension, up to value n_max.
-    The output dimension never exceeds the number of qubits."""
-
-    a, b = np.tril_indices(n_max)
-    return zip(*[a + 1, b + 1])
-
-
-def indices_up_to_dm(n_max):
-    """Returns an iterator over the number of qubits and output dimension, up to value n_max.
-    The output dimension values never exceeds 2 ** (n_max). This is to test for density_matrix
-    qnodes."""
-
-    # If the output_dim is to be used as a tuple. First element is for n_qubits and
-    # the second is for output_dim. For example, for n_max = 3 it will return,
-    # [(1, (2, 2)), (2, (2, 2)), (2, (4, 4)), (3, (2, 2)), (3, (4, 4)), (3, (8, 8))]
-
-    a, b = np.tril_indices(n_max)
-    return zip(*[a + 1], zip(*[2 ** (b + 1), 2 ** (b + 1)]))
-
-
-@pytest.mark.tf
-def test_no_attribute():
-    """Test that the qnn module raises an AttributeError if accessing an unavailable attribute"""
-    with pytest.raises(AttributeError, match="module 'pennylane.qnn' has no attribute 'random'"):
-        qml.qnn.random  # pylint: disable=pointless-statement
-
-
-@pytest.mark.tf
-@pytest.mark.parametrize("interface", ["tf"])  # required for the get_circuit fixture
-@pytest.mark.usefixtures("get_circuit")
-@pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-def test_bad_tf_version(get_circuit, output_dim, monkeypatch):  # pylint: disable=no-self-use
-    """Test if an ImportError is raised when instantiated with an incorrect version of
-    TensorFlow"""
-    c, w = get_circuit
-    with monkeypatch.context() as m:
-        m.setattr(qml.qnn.keras, "CORRECT_TF_VERSION", False)
-
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            with pytest.raises(ImportError, match="KerasLayer requires TensorFlow version 2"):
-                KerasLayer(c, w, output_dim)
-
-
-# pylint: disable=too-many-public-methods
-@pytest.mark.tf
-@pytest.mark.parametrize("interface", ["tf"])  # required for the get_circuit fixture
-@pytest.mark.usefixtures("get_circuit")
-class TestKerasLayer:
-    """Unit tests for the pennylane.qnn.keras.KerasLayer class."""
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_no_input(self):  # pylint: disable=no-self-use
-        """Test if a TypeError is raised when instantiated with a QNode that does not have an
-        argument with name equal to the input_arg class attribute of KerasLayer"""
-        dev = qml.device("default.qubit", wires=1)
-        weight_shapes = {"w1": (3, 3), "w2": 1}
-
-        @qml.qnode(dev, interface="tf")
-        def circuit(w1, w2):  # pylint: disable=unused-argument
-            return qml.expval(qml.PauliZ(0))
-
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            with pytest.raises(TypeError, match="QNode must include an argument with name"):
-                KerasLayer(circuit, weight_shapes, output_dim=1)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_input_in_weight_shapes(
-        self, get_circuit, n_qubits, output_dim
-    ):  # pylint: disable=no-self-use
-        """Test if a ValueError is raised when instantiated with a weight_shapes dictionary that
-        contains the shape of the input argument given by the input_arg class attribute of
-        KerasLayer"""
-        c, w = get_circuit
-        w[qml.qnn.keras.KerasLayer._input_arg] = n_qubits  # pylint: disable=protected-access
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            with pytest.raises(
-                ValueError,
-                match=f"{qml.qnn.keras.KerasLayer._input_arg} argument should not have its dimension",  # pylint: disable=protected-access
-            ):
-                KerasLayer(c, w, output_dim)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_weight_shape_unspecified(self, get_circuit, output_dim):  # pylint: disable=no-self-use
-        """Test if a ValueError is raised when instantiated with a weight missing from the
-        weight_shapes dictionary"""
-        c, w = get_circuit
-        del w["w1"]
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            with pytest.raises(
-                ValueError, match="Must specify a shape for every non-input parameter"
-            ):
-                KerasLayer(c, w, output_dim)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_var_pos(self):  # pylint: disable=no-self-use
-        """Test if a TypeError is raised when instantiated with a variable number of positional
-        arguments"""
-        dev = qml.device("default.qubit", wires=1)
-        weight_shapes = {"w1": (3, 3), "w2": 1}
-
-        @qml.qnode(dev, interface="tf")
-        def circuit(inputs, w1, w2, *args):  # pylint: disable=unused-argument
-            return qml.expval(qml.PauliZ(0))
-
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            with pytest.raises(TypeError, match="Cannot have a variable number of positional"):
-                KerasLayer(circuit, weight_shapes, output_dim=1)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_var_keyword(self):  # pylint: disable=no-self-use
-        """Test that variable number of keyword arguments works"""
-        n_qubits = 2
-        output_dim = 2
-
-        dev = qml.device("default.qubit", wires=n_qubits)
-        w = {
-            "w1": (3, n_qubits, 3),
-            "w2": (1,),
-            "w3": 1,
-            "w4": [3],
-            "w5": (2, n_qubits, 3),
-            "w6": 3,
-            "w7": 0,
-        }
-
-        @qml.qnode(dev, interface="tf")
-        def c(inputs, **kwargs):
-            """A circuit that embeds data using the AngleEmbedding and then performs a variety of
-            operations. The output is a PauliZ measurement on the first output_dim qubits. One set of
-            parameters, w5, are specified as non-trainable."""
-            qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits)))
-            qml.templates.StronglyEntanglingLayers(kwargs["w1"], wires=list(range(n_qubits)))
-            qml.RX(kwargs["w2"][0], wires=0 % n_qubits)
-            qml.RX(kwargs["w3"], wires=1 % n_qubits)
-            qml.Rot(*kwargs["w4"], wires=2 % n_qubits)
-            qml.templates.StronglyEntanglingLayers(kwargs["w5"], wires=list(range(n_qubits)))
-            qml.Rot(*kwargs["w6"], wires=3 % n_qubits)
-            qml.RX(kwargs["w7"], wires=4 % n_qubits)
-            return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)]
-
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim=output_dim)
-        x = tf.ones((2, n_qubits))
-
-        layer_out = layer(x)
-
-        circ_weights = layer.qnode_weights.copy()
-        circ_weights["w4"] = tf.convert_to_tensor(circ_weights["w4"])  # To allow for slicing
-        circ_weights["w6"] = tf.convert_to_tensor(circ_weights["w6"])
-        circuit_out = tf.stack(c(x[0], **circ_weights))
-
-        assert np.allclose(layer_out, circuit_out)
-
-    @pytest.mark.parametrize("n_qubits", [1])
-    @pytest.mark.parametrize("output_dim", zip(*[[[1], (1,), 1], [1, 1, 1]]))
-    def test_output_dim(self, get_circuit, output_dim):  # pylint: disable=no-self-use
-        """Test if the output_dim is correctly processed, i.e., that an iterable is mapped to
-        its first element while an int is left unchanged."""
-        c, w = get_circuit
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim[0])
-        assert layer.output_dim == output_dim[1]
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(2))
-    def test_weight_shapes(self, get_circuit, output_dim, n_qubits):  # pylint: disable=no-self-use
-        """Test if the weight_shapes input argument is correctly processed to be a dictionary
-        with values that are tuples."""
-        c, w = get_circuit
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim)
-        assert layer.weight_shapes == {
-            "w1": (3, n_qubits, 3),
-            "w2": (1,),
-            "w3": (),
-            "w4": (3,),
-            "w5": (2, n_qubits, 3),
-            "w6": (3,),
-            "w7": (),
-        }
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_non_input_defaults(self):  # pylint: disable=no-self-use
-        """Test that everything works when default arguments that are not the input argument are
-        present in the QNode"""
-
-        n_qubits = 2
-        output_dim = 2
-
-        dev = qml.device("default.qubit", wires=n_qubits)
-        w = {
-            "w1": (3, n_qubits, 3),
-            "w2": (1,),
-            "w3": 1,
-            "w4": [3],
-            "w5": (2, n_qubits, 3),
-            "w6": 3,
-            "w7": 0,
-        }
-
-        @qml.qnode(dev, interface="tf")
-        def c(inputs, w1, w2, w4, w5, w6, w7, w3=0.5):  # pylint: disable=too-many-arguments
-            """A circuit that embeds data using the AngleEmbedding and then performs a variety of
-            operations. The output is a PauliZ measurement on the first output_dim qubits. One set of
-            parameters, w5, are specified as non-trainable."""
-            qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits)))
-            qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits)))
-            qml.RX(w2[0], wires=0 % n_qubits)
-            qml.RX(w3, wires=1 % n_qubits)
-            qml.Rot(*w4, wires=2 % n_qubits)
-            qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits)))
-            qml.Rot(*w6, wires=3 % n_qubits)
-            qml.RX(w7, wires=4 % n_qubits)
-            return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)]
-
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim=output_dim)
-        x = tf.ones((2, n_qubits))
-
-        layer_out = layer(x)
-        circ_weights = layer.qnode_weights.copy()
-        circ_weights["w4"] = tf.convert_to_tensor(circ_weights["w4"])  # To allow for slicing
-        circ_weights["w6"] = tf.convert_to_tensor(circ_weights["w6"])
-        circuit_out = c(x[0], **circ_weights)
-
-        assert np.allclose(layer_out, circuit_out)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(2))
-    def test_qnode_weights(self, get_circuit, n_qubits, output_dim):  # pylint: disable=no-self-use
-        """Test if the build() method correctly initializes the weights in the qnode_weights
-        dictionary, i.e., that each value of the dictionary has correct shape and name."""
-        c, w = get_circuit
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim)
-        layer.build(input_shape=(10, n_qubits))
-
-        for weight, shape in layer.weight_shapes.items():
-            assert layer.qnode_weights[weight].shape == shape
-            assert layer.qnode_weights[weight].name[:-2] == weight
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_qnode_weights_with_spec(
-        self, get_circuit, monkeypatch, output_dim, n_qubits
-    ):  # pylint: disable=no-self-use
-        """Test if the build() method correctly passes on user specified weight_specs to the
-        inherited add_weight() method. This is done by monkeypatching add_weight() so that it
-        simply returns its input keyword arguments. The qnode_weights dictionary should then have
-        values that are the input keyword arguments, and we check that the specified weight_specs
-        keywords are there."""
-
-        add_weight = KerasLayer.add_weight
-
-        specs = {}
-
-        def add_weight_dummy(self, **kwargs):
-            """Dummy function for mocking out the add_weight method to store the kwargs in a dict"""
-            specs[kwargs["name"]] = kwargs
-            return add_weight(self, **kwargs)
-
-        weight_specs = {
-            "w1": {"initializer": "random_uniform", "trainable": False},
-            "w2": {"initializer": tf.keras.initializers.RandomNormal(mean=0, stddev=0.5)},
-            "w3": {},
-            "w4": {},
-            "w5": {},
-            "w6": {},
-            "w7": {},
-        }
-
-        with monkeypatch.context() as m:
-            m.setattr(tf.keras.layers.Layer, "add_weight", add_weight_dummy)
-            c, w = get_circuit
-            with pytest.warns(
-                qml.PennyLaneDeprecationWarning,
-                match="The 'KerasLayer' class is deprecated",
-            ):
-                layer = KerasLayer(c, w, output_dim, weight_specs=weight_specs)
-            layer.build(input_shape=(10, n_qubits))
-
-            for weight in layer.weight_shapes:
-                assert all(item in specs[weight].items() for item in weight_specs[weight].items())
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(3))
-    @pytest.mark.parametrize("input_shape", [(10, 4), (8, 3)])
-    def test_compute_output_shape(
-        self, get_circuit, output_dim, input_shape
-    ):  # pylint: disable=no-self-use
-        """Test if the compute_output_shape() method performs correctly, i.e., that it replaces
-        the last element in the input_shape tuple with the specified output_dim and that the
-        output shape is of type tf.TensorShape"""
-        c, w = get_circuit
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim)
-
-        assert layer.compute_output_shape(input_shape) == (input_shape[0], output_dim)
-        assert isinstance(layer.compute_output_shape(input_shape), tf.TensorShape)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(2))
-    @pytest.mark.parametrize("batch_size", [2])
-    def test_call(
-        self, get_circuit, output_dim, batch_size, n_qubits
-    ):  # pylint: disable=no-self-use
-        """Test if the call() method performs correctly, i.e., that it outputs with shape
-        (batch_size, output_dim) with results that agree with directly calling the QNode"""
-        c, w = get_circuit
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim)
-        x = tf.ones((batch_size, n_qubits))
-
-        layer_out = layer(x)
-        weights = [w.numpy() for w in layer.qnode_weights.values()]
-        assert layer_out.shape == (batch_size, output_dim)
-        assert np.allclose(layer_out[0], c(x[0], *weights), atol=1e-7)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    @pytest.mark.parametrize("batch_size", [2])
-    def test_call_shuffled_args(
-        self, get_circuit, output_dim, batch_size, n_qubits
-    ):  # pylint: disable=no-self-use
-        """Test if the call() method performs correctly when the inputs argument is not the first
-        positional argument, i.e., that it outputs with shape (batch_size, output_dim) with
-        results that agree with directly calling the QNode"""
-        c, w = get_circuit
-
-        @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf")
-        def c_shuffled(w1, inputs, w2, w3, w4, w5, w6, w7):  # pylint: disable=too-many-arguments
-            """Version of the circuit with a shuffled signature"""
-            qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits)))
-            qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits)))
-            qml.RX(w2[0], wires=0)
-            qml.RX(w3, wires=0)
-            qml.Rot(*w4, wires=0)
-            qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits)))
-            qml.Rot(*w6, wires=0)
-            qml.RX(w7, wires=0)
-            return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)]
-
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c_shuffled, w, output_dim)
-        x = tf.ones((batch_size, n_qubits))
-
-        layer_out = layer(x)
-        weights = [w.numpy() for w in layer.qnode_weights.values()]
-
-        assert layer_out.shape == (batch_size, output_dim)
-        assert np.allclose(layer_out[0], c(x[0], *weights), atol=1e-7)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    @pytest.mark.parametrize("batch_size", [2])
-    def test_call_default_input(
-        self, get_circuit, output_dim, batch_size, n_qubits
-    ):  # pylint: disable=no-self-use
-        """Test if the call() method performs correctly when the inputs argument is a default
-        argument, i.e., that it outputs with shape (batch_size, output_dim) with results that
-        agree with directly calling the QNode"""
-        c, w = get_circuit
-
-        @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf")
-        def c_default(
-            w1, w2, w3, w4, w5, w6, w7, inputs=None
-        ):  # pylint: disable=too-many-arguments
-            """Version of the circuit with inputs as a default argument"""
-            qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits)))
-            qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits)))
-            qml.RX(w2[0], wires=0)
-            qml.RX(w3, wires=0)
-            qml.Rot(*w4, wires=0)
-            qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits)))
-            qml.Rot(*w6, wires=0)
-            qml.RX(w7, wires=0)
-            return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)]
-
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c_default, w, output_dim)
-        x = tf.ones((batch_size, n_qubits))
-
-        layer_out = layer(x)
-        weights = [w.numpy() for w in layer.qnode_weights.values()]
-
-        assert layer_out.shape == (batch_size, output_dim)
-        assert np.allclose(layer_out[0], c(x[0], *weights), atol=1e-7)
-
-    @pytest.mark.slow
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(2))
-    @pytest.mark.parametrize("batch_size", [2, 4, 6])
-    @pytest.mark.parametrize("middle_dim", [2, 5, 8])
-    def test_call_broadcast(
-        self, get_circuit, output_dim, middle_dim, batch_size, n_qubits
-    ):  # pylint: disable=no-self-use,too-many-arguments
-        """Test if the call() method performs correctly when the inputs argument has an arbitrary shape (that can
-        correctly be broadcast over), i.e., for input of shape (batch_size, dn, ... , d0) it outputs with shape
-        (batch_size, dn, ... , d1, output_dim). Also tests if gradients are still backpropagated correctly.
-        """
-        c, w = get_circuit
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim)
-        x = tf.ones((batch_size, middle_dim, n_qubits))
-
-        with tf.GradientTape() as tape:
-            layer_out = layer(x)
-
-        g_layer = tape.gradient(layer_out, layer.trainable_variables)
-
-        # test gradients are at least calculated
-        assert g_layer is not None
-        assert layer_out.shape == (batch_size, middle_dim, output_dim)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_str_repr(self, get_circuit, output_dim):  # pylint: disable=no-self-use
-        """Test the __str__ and __repr__ representations"""
-        c, w = get_circuit
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim)
-
-        assert layer.__str__() == "<Quantum Keras Layer: func=circuit>"
-        assert layer.__repr__() == "<Quantum Keras Layer: func=circuit>"
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_gradients(self, get_circuit, output_dim, n_qubits):  # pylint: disable=no-self-use
-        """Test if the gradients of the KerasLayer are equal to the gradients of the circuit when
-        taken with respect to the trainable variables"""
-        c, w = get_circuit
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim)
-        x = tf.ones((1, n_qubits))
-
-        with tf.GradientTape() as tape:
-            out_layer = layer(x)
-
-        g_layer = tape.gradient(out_layer, layer.trainable_variables)
-
-        circuit_weights = layer.trainable_variables.copy()
-        circuit_weights[3] = tf.convert_to_tensor(circuit_weights[3])  # To allow for slicing
-        circuit_weights[5] = tf.convert_to_tensor(circuit_weights[5])
-
-        with tf.GradientTape() as tape:
-            out_circuit = c(x[0], *circuit_weights)
-
-        g_circuit = tape.gradient(out_circuit, layer.trainable_variables)
-
-        for i in range(len(out_layer)):
-            assert np.allclose(g_layer[i], g_circuit[i])
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_backprop_gradients(self, mocker):  # pylint: disable=no-self-use
-        """Test if KerasLayer is compatible with the backprop diff method."""
-
-        dev = qml.device("default.qubit")
-
-        @qml.qnode(dev, interface="tf", diff_method="backprop")
-        def f(inputs, weights):
-            qml.templates.AngleEmbedding(inputs, wires=range(2))
-            qml.templates.StronglyEntanglingLayers(weights, wires=range(2))
-            return [qml.expval(qml.PauliZ(i)) for i in range(2)]
-
-        weight_shapes = {"weights": (3, 2, 3)}
-
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            qlayer = qml.qnn.KerasLayer(f, weight_shapes, output_dim=2)
-
-        inputs = tf.ones((4, 2))
-
-        with tf.GradientTape() as tape:
-            out = tf.reduce_sum(qlayer(inputs))
-
-        spy = mocker.spy(qml.gradients, "param_shift")
-
-        grad = tape.gradient(out, qlayer.trainable_weights)
-        assert grad is not None
-        spy.assert_not_called()
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1))
-    def test_compute_output_shape_2(self, get_circuit, output_dim):  # pylint: disable=no-self-use
-        """Test that the compute_output_shape method returns the expected shape"""
-        c, w = get_circuit
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim)
-
-        inputs = tf.keras.Input(shape=(2,))
-        inputs_shape = inputs.shape
-
-        output_shape = layer.compute_output_shape(inputs_shape)
-        assert output_shape.as_list() == [None, 1]
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(3))
-    def test_construct(self, get_circuit, n_qubits, output_dim):
-        """Test that the construct method builds the correct tape with correct differentiability"""
-        c, w = get_circuit
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            layer = KerasLayer(c, w, output_dim)
-
-        x = tf.ones((1, n_qubits))
-
-        tape = qml.workflow.construct_tape(layer)(x)
-
-        assert tape is not None
-        assert (
-            len(tape.get_parameters(trainable_only=False))
-            == len(tape.get_parameters(trainable_only=True)) + 1
-        )
-
-
-@pytest.mark.all_interfaces
-@pytest.mark.parametrize("interface", ["autograd", "jax", "torch"])
-def test_invalid_interface_error(interface):
-    """Test an error gets raised if input QNode has the wrong interface"""
-    dev = qml.device("default.qubit", wires=3)
-    weight_shapes = {"w1": 1}
-
-    @qml.qnode(dev, interface=interface)
-    def circuit(inputs, w1):
-        qml.templates.AngleEmbedding(inputs, wires=[0, 1])
-        qml.RX(w1, wires=0)
-        return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
-
-    with pytest.warns(
-        qml.PennyLaneDeprecationWarning, match="The 'KerasLayer' class is deprecated"
-    ):
-        with pytest.raises(ValueError, match="Invalid interface"):
-            _ = KerasLayer(circuit, weight_shapes, output_dim=2)
-
-
-@pytest.mark.tf
-@pytest.mark.parametrize(
-    "interface", ("auto", "tf", "tensorflow", "tensorflow-autograph", "tf-autograph")
-)
-def test_qnode_interface_not_mutated(interface):
-    """Test that the input QNode's interface is not mutated by KerasLayer"""
-    dev = qml.device("default.qubit", wires=3)
-    weight_shapes = {"w1": 1}
-
-    @qml.qnode(dev, interface=interface)
-    def circuit(inputs, w1):
-        qml.templates.AngleEmbedding(inputs, wires=[0, 1])
-        qml.RX(w1, wires=0)
-        return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
-
-    with pytest.warns(
-        qml.PennyLaneDeprecationWarning, match="The 'KerasLayer' class is deprecated"
-    ):
-        qlayer = KerasLayer(circuit, weight_shapes, output_dim=2)
-    assert (
-        qlayer.qnode.interface
-        == circuit.interface
-        == qml.math.get_canonical_interface_name(interface).value
-    )
-
-
-@pytest.mark.tf
-@pytest.mark.parametrize("interface", ["tf"])
-@pytest.mark.usefixtures("get_circuit", "model")
-class TestKerasLayerIntegration:
-    """Integration tests for the pennylane.qnn.keras.KerasLayer class."""
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(2))
-    @pytest.mark.parametrize("batch_size", [2])
-    def test_train_model(
-        self, model, batch_size, n_qubits, output_dim
-    ):  # pylint: disable=no-self-use,redefined-outer-name
-        """Test if a model can train using the KerasLayer. The model is composed of two
-        KerasLayers sandwiched between Dense neural network layers, and the dataset is simply
-        input and output vectors of zeros."""
-
-        x = np.zeros((batch_size, n_qubits))
-        y = np.zeros((batch_size, output_dim))
-
-        model.compile(optimizer="sgd", loss="mse")
-
-        model.fit(x, y, batch_size=batch_size, verbose=0)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(2))
-    def test_model_gradients(
-        self, model, output_dim, n_qubits
-    ):  # pylint: disable=no-self-use,redefined-outer-name
-        """Test if a gradient can be calculated with respect to all of the trainable variables in
-        the model"""
-        x = tf.zeros((2, n_qubits))
-        y = tf.zeros((2, output_dim))
-
-        with tf.GradientTape() as tape:
-            out = model(x)
-            loss = tf.keras.losses.mean_squared_error(out, y)
-
-        gradients = tape.gradient(loss, model.trainable_variables)
-        assert all(g.dtype == tf.keras.backend.floatx() for g in gradients)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(2))
-    def test_save_model_weights(
-        self, get_circuit, n_qubits, output_dim, tmpdir
-    ):  # pylint: disable=no-self-use,redefined-outer-name
-        """Test if the model can be successfully saved and reloaded using the get_weights()
-        method"""
-        clayer = tf.keras.layers.Dense(n_qubits, use_bias=False, input_shape=(n_qubits,))
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            qlayer = KerasLayer(*get_circuit, output_dim)
-        model = tf.keras.models.Sequential([clayer, qlayer])
-        weights = model.get_weights()
-
-        file = str(tmpdir) + "/model"
-        model.save_weights(file)
-
-        new_clayer = tf.keras.layers.Dense(n_qubits, use_bias=False, input_shape=(n_qubits,))
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            new_qlayer = KerasLayer(*get_circuit, output_dim)
-        new_model = tf.keras.models.Sequential([new_clayer, new_qlayer])
-        new_weights = new_model.get_weights()
-
-        assert len(weights) == len(new_weights)
-        assert all(w.shape == nw.shape for w, nw in zip(weights, new_weights))
-
-        # assert that the new model's weights are different
-        assert all(tf.math.reduce_any(w != nw) for w, nw in zip(weights, new_weights))
-
-        new_model.load_weights(file)
-        new_weights = new_model.get_weights()
-
-        assert len(weights) == len(new_weights)
-        assert all(w.shape == nw.shape for w, nw in zip(weights, new_weights))
-
-        # assert that the new model's weights are now the same
-        assert all(tf.math.reduce_all(w == nw) for w, nw in zip(weights, new_weights))
-
-        # assert that the results are the same
-        x = tf.constant(np.arange(5 * n_qubits).reshape(5, n_qubits))
-        res = model(x)
-        new_res = new_model(x)
-        assert tf.math.reduce_all(res == new_res)
-
-    # the test is slow since TensorFlow needs to compile the execution graph
-    # in order to save the model
-    @pytest.mark.slow
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(2))
-    def test_save_whole_model(
-        self, model, n_qubits, tmpdir
-    ):  # pylint: disable=redefined-outer-name
-        """Test if the entire model can be successfully saved and reloaded
-        using the .save() method"""
-        weights = model.get_weights()
-
-        file = str(tmpdir) + "/model"
-        model.save(file)
-
-        new_model = tf.keras.models.load_model(file)
-        new_weights = new_model.get_weights()
-
-        assert len(weights) == len(new_weights)
-        assert all(w.shape == nw.shape for w, nw in zip(weights, new_weights))
-        assert all(tf.math.reduce_all(w == nw) for w, nw in zip(weights, new_weights))
-
-        # assert that the results are the same
-        x = tf.constant(np.arange(5 * n_qubits).reshape(5, n_qubits))
-        res = model(x)
-        new_res = new_model(x)
-        assert tf.math.reduce_all(res == new_res)
-
-
-@pytest.mark.tf
-@pytest.mark.parametrize("interface", ["tf"])
-@pytest.mark.usefixtures("get_circuit_dm", "model_dm")
-class TestKerasLayerIntegrationDM:
-    """Integration tests for the pennylane.qnn.keras.KerasLayer class for
-    density_matrix() returning circuits."""
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to_dm(3))
-    @pytest.mark.parametrize("batch_size", [2])
-    def test_train_model_dm(
-        self, model_dm, batch_size, n_qubits, output_dim
-    ):  # pylint: disable=no-self-use,redefined-outer-name
-        """Test if a model can train using the KerasLayer when QNode returns a density_matrix().
-        The model is composed of two KerasLayers sandwiched between Dense neural network layers,
-        and the dataset is simply input and output vectors of zeros."""
-
-        x = np.zeros((batch_size, n_qubits))
-        y = np.zeros((batch_size, output_dim[0] * output_dim[1]))
-
-        model_dm.compile(optimizer="sgd", loss="mse")
-
-        model_dm.fit(x, y, batch_size=batch_size, verbose=0)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to_dm(2))
-    def test_model_gradients_dm(
-        self, model_dm, output_dim, n_qubits
-    ):  # pylint: disable=no-self-use,redefined-outer-name
-        """Test if a gradient can be calculated with respect to all of the trainable variables in
-        the model."""
-        x = tf.zeros((2, n_qubits))
-        y = tf.zeros((2, output_dim[0] * output_dim[1]))
-
-        with tf.GradientTape() as tape:
-            out = model_dm(x)
-            loss = tf.keras.losses.mean_squared_error(out, y)
-
-        gradients = tape.gradient(loss, model_dm.trainable_variables)
-        assert all(g.dtype == tf.keras.backend.floatx() for g in gradients)
-
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to_dm(2))
-    def test_save_model_weights_dm(
-        self, get_circuit_dm, n_qubits, output_dim, tmpdir
-    ):  # pylint: disable=no-self-use,redefined-outer-name
-        """Test if the model_dm can be successfully saved and reloaded using the get_weights()
-        method"""
-        clayer = tf.keras.layers.Dense(n_qubits, use_bias=False, input_shape=(n_qubits,))
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            qlayer = KerasLayer(*get_circuit_dm, output_dim)
-        model = tf.keras.models.Sequential([clayer, qlayer])
-        weights = model.get_weights()
-
-        file = str(tmpdir) + "/model"
-        model.save_weights(file)
-
-        new_clayer = tf.keras.layers.Dense(n_qubits, use_bias=False, input_shape=(n_qubits,))
-        with pytest.warns(
-            qml.PennyLaneDeprecationWarning,
-            match="The 'KerasLayer' class is deprecated",
-        ):
-            new_qlayer = KerasLayer(*get_circuit_dm, output_dim)
-        new_model = tf.keras.models.Sequential([new_clayer, new_qlayer])
-        new_weights = new_model.get_weights()
-
-        assert len(weights) == len(new_weights)
-        assert all(w.shape == nw.shape for w, nw in zip(weights, new_weights))
-
-        # assert that the new model's weights are different
-        assert all(tf.math.reduce_any(w != nw) for w, nw in zip(weights, new_weights))
-
-        new_model.load_weights(file)
-        new_weights = new_model.get_weights()
-
-        assert len(weights) == len(new_weights)
-        assert all(w.shape == nw.shape for w, nw in zip(weights, new_weights))
-
-        # assert that the new model's weights are now the same
-        assert all(tf.math.reduce_all(w == nw) for w, nw in zip(weights, new_weights))
-
-        # assert that the results are the same
-        x = tf.constant(np.arange(5 * n_qubits).reshape(5, n_qubits))
-        res = model(x)
-        new_res = new_model(x)
-        assert tf.math.reduce_all(res == new_res)
-
-    # the test is slow since TensorFlow needs to compile the execution graph
-    # in order to save the model
-    @pytest.mark.slow
-    @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to_dm(2))
-    def test_save_whole_model_dm(
-        self, model_dm, n_qubits, tmpdir
-    ):  # pylint: disable=redefined-outer-name
-        """Test if the entire model can be successfully saved and reloaded
-        using the .save() method"""
-        weights = model_dm.get_weights()
-
-        file = str(tmpdir) + "/model"
-        model_dm.save(file)
-
-        new_model_dm = tf.keras.models.load_model(file)
-        new_weights = new_model_dm.get_weights()
-
-        for w, nw in zip(weights, new_weights):
-            assert np.allclose(w, nw)
-
-        # assert that the results are the same
-        x = tf.constant(np.arange(5 * n_qubits).reshape(5, n_qubits))
-        res = model_dm(x)
-        new_res = new_model_dm(x)
-        assert tf.math.reduce_all(res == new_res)
-
-
-@pytest.mark.tf
-def test_batch_input_single_measure(tol):
-    """Test input batching in keras"""
-    dev = qml.device("default.qubit")
-
-    @qml.qnode(dev, interface="tf", diff_method="parameter-shift")
-    def circuit(x, weights):
-        qml.AngleEmbedding(x, wires=range(4), rotation="Y")
-        qml.RY(weights[0], wires=0)
-        qml.RY(weights[1], wires=1)
-        return qml.probs(op=qml.PauliZ(1))
-
-    KerasLayer.set_input_argument("x")
-    with pytest.warns(
-        qml.PennyLaneDeprecationWarning, match="The 'KerasLayer' class is deprecated"
-    ):
-        layer = KerasLayer(circuit, weight_shapes={"weights": (2,)}, output_dim=(2,))
-    layer.build((None, 2))
-
-    x = tf.constant(np.random.uniform(0, 1, (10, 4)))
-    res = layer(x)
-
-    assert res.shape == (10, 2)
-
-    for x_, r in zip(x, res):
-        assert qml.math.allclose(r, circuit(x_, layer.qnode_weights["weights"]), atol=tol)
-
-
-@pytest.mark.tf
-def test_batch_input_multi_measure(tol):
-    """Test input batching in keras for multiple measurements"""
-    dev = qml.device("default.qubit")
-
-    @qml.qnode(dev, interface="tf", diff_method="parameter-shift")
-    def circuit(x, weights):
-        qml.AngleEmbedding(x, wires=range(4), rotation="Y")
-        qml.RY(weights[0], wires=0)
-        qml.RY(weights[1], wires=1)
-        return [qml.expval(qml.PauliZ(1)), qml.probs(wires=range(2))]
-
-    KerasLayer.set_input_argument("x")
-    with pytest.warns(
-        qml.PennyLaneDeprecationWarning, match="The 'KerasLayer' class is deprecated"
-    ):
-        layer = KerasLayer(circuit, weight_shapes={"weights": (2,)}, output_dim=(5,))
-    layer.build((None, 4))
-
-    x = tf.constant(np.random.uniform(0, 1, (10, 4)))
-    res = layer(x)
-
-    assert res.shape == (10, 5)
-
-    for x_, r in zip(x, res):
-        exp = tf.experimental.numpy.hstack(circuit(x_, layer.qnode_weights["weights"]))
-        assert qml.math.allclose(r, exp, atol=tol)
-
-
-@pytest.mark.tf
-def test_draw():
-    """Test that a KerasLayer can be drawn using qml.draw"""
-
-    dev = qml.device("default.qubit", wires=2)
-    weight_shapes = {"w1": 1, "w2": (3, 2, 3)}
-
-    @qml.qnode(dev, interface="tensorflow")
-    def circuit(inputs, w1, w2):
-        qml.templates.AngleEmbedding(inputs, wires=[0, 1])
-        qml.RX(w1, wires=0)
-        qml.templates.StronglyEntanglingLayers(w2, wires=[0, 1])
-        return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
-
-    with pytest.warns(
-        qml.PennyLaneDeprecationWarning, match="The 'KerasLayer' class is deprecated"
-    ):
-        qlayer = KerasLayer(circuit, weight_shapes, output_dim=2)
-
-    # make the rotation angle positive to prevent the extra minus sign messing up the alignment
-    qlayer.qnode_weights["w1"] = tf.abs(qlayer.qnode_weights["w1"])
-
-    batch_size = 5
-    x = tf.constant(np.random.uniform(0, 1, (batch_size, 2)))
-
-    actual = qml.draw(qlayer)(x)
-
-    w1 = f"{qlayer.qnode_weights['w1'].numpy():.2f}"
-    m1 = f"{qlayer.qnode_weights['w2'].numpy()}"
-    expected = (
-        f"0: ─╭AngleEmbedding(M0)──RX({w1})─╭StronglyEntanglingLayers(M1)─┤  <Z>\n"
-        f"1: ─╰AngleEmbedding(M0)───────────╰StronglyEntanglingLayers(M1)─┤  <Z>\n"
-        f"\n"
-        f"M0 = \n{x}\n"
-        f"M1 = \n{m1}"
-    )
-
-    assert actual == expected
-
-
-@pytest.mark.tf
-def test_draw_mpl():
-    """Test that a KerasLayer can be drawn using qml.draw_mpl"""
-
-    dev = qml.device("default.qubit", wires=2)
-    weight_shapes = {"w1": 1, "w2": (3, 2, 3)}
-
-    @qml.qnode(dev, interface="tensorflow")
-    def circuit(inputs, w1, w2):
-        qml.templates.AngleEmbedding(inputs, wires=[0, 1])
-        qml.RX(w1, wires=0)
-        qml.templates.StronglyEntanglingLayers(w2, wires=[0, 1])
-        return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
-
-    with pytest.warns(
-        qml.PennyLaneDeprecationWarning, match="The 'KerasLayer' class is deprecated"
-    ):
-        qlayer = KerasLayer(circuit, weight_shapes, output_dim=2)
-
-    # make the rotation angle positive to prevent the extra minus sign messing up the alignment
-    qlayer.qnode_weights["w1"] = tf.abs(qlayer.qnode_weights["w1"])
-
-    batch_size = 5
-    x = tf.constant(np.random.uniform(0, 1, (batch_size, 2)))
-
-    _, ax = qml.draw_mpl(qlayer)(x)
-
-    assert len(ax.patches) == 9  # 3 boxes, 3 patches for each measure
-    assert len(ax.lines) == 2  # 2 wires
-    assert len(ax.texts) == 5  # 2 wire labels, 3 box labels
-
-    assert ax.texts[0].get_text() == "0"
-    assert ax.texts[1].get_text() == "1"
-    assert ax.texts[2].get_text() == "AngleEmbedding"
-    assert ax.texts[3].get_text() == "RX"
-    assert ax.texts[4].get_text() == "StronglyEntanglingLayers"
-
-
-@pytest.mark.tf
-def test_specs():
-    """Test that the qml.specs transform works for KerasLayer"""
-
-    dev = qml.device("default.qubit", wires=3)
-    weight_shapes = {"w1": 1, "w2": (3, 2, 3)}
-
-    @qml.qnode(dev, interface="tf")
-    def circuit(inputs, w1, w2):
-        qml.templates.AngleEmbedding(inputs, wires=[0, 1])
-        qml.RX(w1, wires=0)
-        qml.templates.StronglyEntanglingLayers(w2, wires=[0, 1])
-        return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
-
-    with pytest.warns(
-        qml.PennyLaneDeprecationWarning, match="The 'KerasLayer' class is deprecated"
-    ):
-        qlayer = KerasLayer(circuit, weight_shapes, output_dim=2)
-
-    batch_size = 5
-    x = tf.constant(np.random.uniform(0, 1, (batch_size, 2)))
-
-    info = qml.specs(qlayer)(x)
-
-    gate_sizes = defaultdict(int, {1: 1, 2: 2})
-    gate_types = defaultdict(int, {"AngleEmbedding": 1, "RX": 1, "StronglyEntanglingLayers": 1})
-    expected_resources = qml.resource.Resources(
-        num_wires=2, num_gates=3, gate_types=gate_types, gate_sizes=gate_sizes, depth=3
-    )
-    assert info["resources"] == expected_resources
-
-    assert info["num_observables"] == 2
-    assert info["num_device_wires"] == 3
-    assert info["num_tape_wires"] == 2
-    assert info["num_trainable_params"] == 2
-    assert info["interface"] == "tf"
-    assert info["device_name"] == "default.qubit"
-
-
-@pytest.mark.xfail(
-    condition=USING_KERAS3,
-    reason=KERAS3_XFAIL_INFO,
-)
-@pytest.mark.slow
-@pytest.mark.tf
-def test_save_and_load_preserves_weights(tmpdir):
-    """
-    Test that saving and loading a model doesn't lose the weights. This particular
-    test is important aside from `test_save_whole_model` because a bug caused issues
-    when the name of (at least one of) your weights is 'weights'.
-    """
-
-    dev = qml.device("default.qubit")
-    n_qubits = 2
-
-    @qml.qnode(dev)
-    def circuit(inputs, weights):
-        qml.AngleEmbedding(inputs, wires=range(n_qubits))
-        qml.RX(weights[0], 0)
-        qml.RX(weights[1], 1)
-        return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))]
-
-    weight_shapes = {"weights": (n_qubits,)}
-    with pytest.warns(
-        qml.PennyLaneDeprecationWarning, match="The 'KerasLayer' class is deprecated"
-    ):
-        quantum_layer = qml.qnn.KerasLayer(circuit, weight_shapes, output_dim=2)
-
-    model0 = tf.keras.models.Sequential()
-    model0.add(quantum_layer)
-    model0.add(tf.keras.layers.Dense(2, activation=tf.nn.softmax, input_shape=(2,)))
-
-    model0.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])
-
-    num_points = 5
-    dummy_input_data = np.random.uniform(0, np.pi, size=(num_points, 2))
-    dummy_output_data = np.random.randint(2, size=(num_points, 2))
-
-    model0.fit(dummy_input_data, dummy_output_data, epochs=1, batch_size=0)
-    file = str(tmpdir) + "/model"
-    model0.save(file)
-    loaded_model = tf.keras.models.load_model(file)
-    assert np.array_equal(model0.layers[0].weights, loaded_model.layers[0].weights)


Reviews:
user: login: albi3ro
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T17:45:51Z
user: login: PietropaoloFrisoni
body: :rocket: 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T18:25:04Z
user: login: isaacdevlugt
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:51:31Z
user: login: JerryChen97
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T14:00:17Z
user: login: isaacdevlugt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T14:04:49Z
user: login: albi3ro
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T15:39:08Z
user: login: albi3ro
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T17:40:02Z
user: login: JerryChen97
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T18:54:40Z
user: login: isaacdevlugt
body: I think this should be removed from the pending dep cycles section too, right? If not, let's make sure to use the same phrasing as we are in the completed dep cycle section 😁 

![Screenshot 2025-04-23 at 5 21 09 PM](https://github.com/user-attachments/assets/e103ae00-c27e-4cfd-95bd-34c330a932e8)

state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T21:23:01Z
user: login: isaacdevlugt
body: Yay!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:32:21Z
user: login: albi3ro
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T14:18:18Z

Review comments:
diff_hunk: @@ -19,6 +19,9 @@
 
 <h3>Breaking changes 💔</h3>
 
+* The `KerasLayer` in `qml.qnn.keras` has been deprecated because Keras 2 is no longer actively maintained.  Please consider using a different machine learning framework.
path: doc/releases/changelog-dev.md
user: login: albi3ro
body: ```suggestion
* The `KerasLayer` in `qml.qnn.keras` has been removed because Keras 2 is no longer actively maintained.  Please consider using a different machine learning framework.
```
created_at: 2025-04-22T17:45:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,6 +19,9 @@
 
 <h3>Breaking changes 💔</h3>
 
+* The `KerasLayer` in `qml.qnn.keras` has been removed because Keras 2 is no longer actively maintained.  Please consider using a different machine learning framework.
path: doc/releases/changelog-dev.md
user: login: PietropaoloFrisoni
body: ```suggestion
* The `KerasLayer` in `qml.qnn.keras` has been removed because Keras 2 is no longer actively maintained.  
   Please consider using a different machine learning framework.
```
created_at: 2025-04-22T18:24:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +83,11 @@ for details on how to port your legacy code to the new system. The following fun
 Completed deprecation cycles
 ----------------------------
 
+* The ``KerasLayer`` in ``qml.qnn.keras`` has been removed because Keras 2 is no longer actively maintained.  Please consider using a different machine learning framework.
path: doc/development/deprecations.rst
user: login: isaacdevlugt
body: ```suggestion
* The ``KerasLayer`` class in ``qml.qnn.keras`` has been removed because Keras 2 is no longer actively maintained.  Please consider using a different machine learning framework, like [Pytorch](https://pennylane.ai/qml/demos/tutorial_qnn_module_torch) or [JAX](https://pennylane.ai/qml/demos/tutorial_How_to_optimize_QML_model_using_JAX_and_Optax).
```

I think we can use sphinx directives for demos here, right? If so, I suggest we use those instead
created_at: 2025-04-23T13:51:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +83,11 @@ for details on how to port your legacy code to the new system. The following fun
 Completed deprecation cycles
 ----------------------------
 
+* The ``KerasLayer`` in ``qml.qnn.keras`` has been removed because Keras 2 is no longer actively maintained.  Please consider using a different machine learning framework.
path: doc/development/deprecations.rst
user: login: JerryChen97
body: let's try
created_at: 2025-04-23T14:00:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,6 +61,13 @@ for details on how to port your legacy code to the new system. The following fun
 Completed deprecation cycles
 ----------------------------
 
+* The ``KerasLayer`` class in ``qml.qnn.keras`` has been removed because Keras 2 is no longer actively maintained.
+  Please consider using a different machine learning framework, like [Pytorch](https://pennylane.ai/qml/demos/tutorial_qnn_module_torch)
+  or [JAX](https://pennylane.ai/qml/demos/tutorial_How_to_optimize_QML_model_using_JAX_and_Optax).
path: doc/development/deprecations.rst
user: login: isaacdevlugt
body: ```suggestion
  Please consider using a different machine learning framework, like :doc:`PyTorch <demos/tutorial_qnn_module_torch>` or :doc:`JAX <demos/tutorial_How_to_optimize_QML_model_using_JAX_and_Optax>`.
```
created_at: 2025-04-23T14:04:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -341,15 +340,7 @@ def batch_constructor(*args, **kwargs) -> tuple[QuantumScriptBatch, Postprocessi
 
         context_fn = nullcontext
path: pennylane/workflow/construct_batch.py
user: login: albi3ro
body: We shouldn't need `context_fn` anymore.
created_at: 2025-04-23T15:39:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -339,24 +338,14 @@ def batch_constructor(*args, **kwargs) -> tuple[QuantumScriptBatch, Postprocessi
         else:
             shots = kwargs.pop("shots", qnode.device.shots)
 
-        context_fn = nullcontext
-
-        # !TODO: remove KerasLayer in 0.42
-        if type(qnode).__name__ == "KerasLayer":
-            # note that calling qml.qnn.KerasLayer pulls in a tf import
-            # pylint: disable=import-outside-toplevel
-            import tensorflow as tf
-
-            context_fn = tf.GradientTape
-
-        elif type(qnode).__name__ == "TorchLayer":
+        if type(qnode).__name__ == "TorchLayer":
             # avoid triggering import of torch if its not needed.
             x = args[0]
             kwargs = {
                 **{arg: weight.to(x) for arg, weight in qnode.qnode_weights.items()},
             }
 
-        with context_fn() as cntxt:
+        with nullcontext() as cntxt:
path: pennylane/workflow/construct_batch.py
user: login: albi3ro
body: Should be able to get rid of this now too. And the associated watching.
created_at: 2025-04-23T17:40:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -339,24 +338,14 @@ def batch_constructor(*args, **kwargs) -> tuple[QuantumScriptBatch, Postprocessi
         else:
             shots = kwargs.pop("shots", qnode.device.shots)
 
-        context_fn = nullcontext
-
-        # !TODO: remove KerasLayer in 0.42
-        if type(qnode).__name__ == "KerasLayer":
-            # note that calling qml.qnn.KerasLayer pulls in a tf import
-            # pylint: disable=import-outside-toplevel
-            import tensorflow as tf
-
-            context_fn = tf.GradientTape
-
-        elif type(qnode).__name__ == "TorchLayer":
+        if type(qnode).__name__ == "TorchLayer":
             # avoid triggering import of torch if its not needed.
             x = args[0]
             kwargs = {
                 **{arg: weight.to(x) for arg, weight in qnode.qnode_weights.items()},
             }
 
-        with context_fn() as cntxt:
+        with nullcontext() as cntxt:
path: pennylane/workflow/construct_batch.py
user: login: JerryChen97
body: fully removed now!
created_at: 2025-04-23T18:54:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,6 +19,10 @@
 
 <h3>Breaking changes 💔</h3>
 
+* The `KerasLayer` in `qml.qnn.keras` has been removed because Keras 2 is no longer actively maintained.
+  Please consider using a different machine learning framework.
+  [(#7320)](https://github.com/PennyLaneAI/pennylane/pull/7320)
path: doc/releases/changelog-dev.md
user: login: isaacdevlugt
body: ```suggestion
* The `KerasLayer` class in `qml.qnn.keras` has been removed because Keras 2 is no longer actively maintained.
  Please consider using a different machine learning framework, like :doc:`PyTorch <demos/tutorial_qnn_module_torch>` or :doc:`JAX <demos/tutorial_How_to_optimize_QML_model_using_JAX_and_Optax>`.
  [(#7320)](https://github.com/PennyLaneAI/pennylane/pull/7320)
```
created_at: 2025-04-23T21:21:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: JerryChen97
created_at: 2025-04-22T15:03:01Z
body: Ready for review only for testing 
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-22T15:22:12Z
body: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7320?dropdown=coverage&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:
> Project coverage is 99.64%. Comparing base [(`43e5d2c`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/43e5d2c029de7308e6503ed3e065137c3f854cdf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`3ba012c`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/3ba012cd281324370a2e14fa5b3a4b5f48121bbe?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 387 commits behind head on master.

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


```diff
@@            Coverage Diff             @@
##           master    #7320      +/-   ##
==========================================
- Coverage   99.64%   99.64%   -0.01%     
==========================================
  Files         514      513       -1     
  Lines       50149    50036     -113     
==========================================
- Hits        49972    49859     -113     
  Misses        177      177              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7320?dropdown=coverage&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).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: JerryChen97
created_at: 2025-04-23T22:42:44Z
body: > I think this should be removed from the pending dep cycles section too, right? If not, let's make sure to use the same phrasing as we are in the completed dep cycle section 😁
> 
> ![Screenshot 2025-04-23 at 5 21 09 PM](https://private-user-images.githubusercontent.com/34751083/436739206-e103ae00-c27e-4cfd-95bd-34c330a932e8.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDU0NDg0NTQsIm5iZiI6MTc0NTQ0ODE1NCwicGF0aCI6Ii8zNDc1MTA4My80MzY3MzkyMDYtZTEwM2FlMDAtYzI3ZS00Y2ZkLTk1YmQtMzRjMzMwYTkzMmU4LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA0MjMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNDIzVDIyNDIzNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTgxODllNmJiZTQ4NDZkY2VkMzgwMmFmNTBmZDE5OGQ2NzkyMDQyZDdiMGFkOWViMmU5ZjMxYmJkNDkyZmMxYTcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.4iGOvalKzCo7K9TCSmGChAQT_zgGuAfAidUMwxZAXOM)

Done!
author_association: CONTRIBUTOR
user: login: JerryChen97
created_at: 2025-04-24T14:17:18Z
body: @albi3ro any more suggestions?
author_association: CONTRIBUTOR