Repository: PennyLaneAI/pennylane

Issue:
[BUG] `SpecialUnitary` is not trainable with autodiff on `"default.mixed"` device
### Expected behavior

The following code should produce a non-vanishing gradient:

<details>
<summary>Code</summary>

```python
dev = qml.device('default.mixed', wires=1)

@qml.qnode(dev, interface="jax")
def circuit(x):
    qml.SpecialUnitary(x, wires=[0])
    return qml.expval(qml.PauliZ(0))

x = jax.numpy.array([0.2, 0.1, 0.6])
jax.grad(circuit)(x) # Should produce Array([-0.68453361, -0.3422668 ,  0.03581418], dtype=float64)
```

</details>


### Actual behavior

The above code exampe produces a vanishing gradient:

<details>
<summary>Code</summary>

```python
dev = qml.device('default.mixed', wires=1)

@qml.qnode(dev, interface="jax")
def circuit(x):
    qml.SpecialUnitary(x, wires=[0])
    return qml.expval(qml.PauliZ(0))

x = jax.numpy.array([0.2, 0.1, 0.6])
```
```pycon
>>> jax.grad(circuit)(x)
Array([0., 0., 0.], dtype=float64)
```

</details>

### Additional information

This is caused by the following pipeline:
- `"default.mixed"` device does not support `SpecialUnitary`, because it is not registered with the device, and other than `"default.qubit"`, having a matrix is not sufficient for an operation to be supported
- `SpecialUnitary` is being decomposed, transferring the trainability of its parameters to `TmpPauliRot` operations, which have parameters 0.
- `TmpPauliRot` again is not supported (which is intended) and also being decomposed. As the `TmpPauliRot` operations have zero parameter, they are removed in this decomposition step, preventing the gradient computation from succeeding.

For `"default.mixed"`, and in fact any device that can deal with operations that provide a matrix, adding `SpecialUnitary` to the supported operations is sufficient.

For other devices, we might run into trouble in the future if a custom device:
- is to be used with automatic differentiation (i.e. not with a gradient transform)
- can not handle operations that provide a matrix generically


### Source code

_No response_

### Tracebacks

_No response_

### System information

```shell
Name: PennyLane
Version: 0.29.0.dev0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /home/david/venvs/dev/lib/python3.10/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, retworkx, scipy, semantic-version, toml
Required-by: PennyLane-Lightning, PennyLane-qiskit

Platform info:           Linux-5.15.0-60-generic-x86_64-with-glibc2.35
Python version:          3.10.6
Numpy version:           1.23.5
Scipy version:           1.9.3
Installed devices:
- default.gaussian (PennyLane-0.29.0.dev0)
- default.mixed (PennyLane-0.29.0.dev0)
- default.qubit (PennyLane-0.29.0.dev0)
- default.qubit.autograd (PennyLane-0.29.0.dev0)
- default.qubit.jax (PennyLane-0.29.0.dev0)
- default.qubit.tf (PennyLane-0.29.0.dev0)
- default.qubit.torch (PennyLane-0.29.0.dev0)
- default.qutrit (PennyLane-0.29.0.dev0)
- null.qubit (PennyLane-0.29.0.dev0)
- lightning.qubit (PennyLane-Lightning-0.28.2)
- qiskit.aer (PennyLane-qiskit-0.28.0)
- qiskit.basicaer (PennyLane-qiskit-0.28.0)
- qiskit.ibmq (PennyLane-qiskit-0.28.0)
- qiskit.ibmq.circuit_runner (PennyLane-qiskit-0.28.0)
- qiskit.ibmq.sampler (PennyLane-qiskit-0.28.0)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.


Discussion:

Resolving patch:
diff --git a/pennylane/devices/default_mixed.py b/pennylane/devices/default_mixed.py
--- a/pennylane/devices/default_mixed.py
+++ b/pennylane/devices/default_mixed.py
@@ -76,6 +76,7 @@ class DefaultMixed(QubitDevice):
         "ControlledQubitUnitary",
         "MultiControlledX",
         "DiagonalQubitUnitary",
+        "SpecialUnitary",
         "PauliX",
         "PauliY",
         "PauliZ",
diff --git a/pennylane/devices/null_qubit.py b/pennylane/devices/null_qubit.py
--- a/pennylane/devices/null_qubit.py
+++ b/pennylane/devices/null_qubit.py
@@ -49,6 +49,7 @@ class NullQubit(QubitDevice):
         "ControlledQubitUnitary",
         "MultiControlledX",
         "DiagonalQubitUnitary",
+        "SpecialUnitary",
         "PauliX",
         "PauliY",
         "PauliZ",
diff --git a/pennylane/ops/qubit/special_unitary.py b/pennylane/ops/qubit/special_unitary.py
--- a/pennylane/ops/qubit/special_unitary.py
+++ b/pennylane/ops/qubit/special_unitary.py
@@ -222,7 +222,7 @@ class SpecialUnitary(Operation):
     Note that for specific operations like the ``RX`` rotation gate above, it is
     strongly recommended to use the specialized implementation ``qml.RX`` rather
     than ``PauliRot`` or ``SpecialUnitary``.
-    However, ``SpecialUnitary`` gates go beyong such rotations: Multiple Pauli words
+    However, ``SpecialUnitary`` gates go beyond such rotations: Multiple Pauli words
     can be activated simultaneously, giving access to more complex operations.
     For two qubits, this could look like this:
 
@@ -418,7 +418,7 @@ def compute_matrix(theta, num_wires):
 
             U(\theta) = \exp(i\sum_{m=1}^d \theta_m P_m)
 
-        See :func:`~.ops.qubit.matrix_ops.pauli_basis_strings` for the ordering of Pauli words.
+        See the main class documentation above for the ordering of Pauli words.
 
         .. note::
 


Test patch:
diff --git a/pennylane/devices/tests/test_gates.py b/pennylane/devices/tests/test_gates.py
--- a/pennylane/devices/tests/test_gates.py
+++ b/pennylane/devices/tests/test_gates.py
@@ -559,7 +559,8 @@ def circuit():
         res = circuit()
 
         # Disabling Pylint test because qml.ops can be misunderstood as qml.ops.qubit.ops
-        basis = qml.ops.qubit.matrix_ops.pauli_basis(n_wires)  # pylint: disable=no-member
+        basis_fn = qml.ops.qubit.special_unitary.pauli_basis_matrices  # pylint: disable=no-member
+        basis = basis_fn(n_wires)
         mat = qml.math.expm(1j * np.tensordot(theta_, basis, axes=[[0], [0]]))
         expected = np.abs(mat @ rnd_state) ** 2
         assert np.allclose(res, expected, atol=tol(dev.shots))
diff --git a/tests/devices/test_default_mixed.py b/tests/devices/test_default_mixed.py
--- a/tests/devices/test_default_mixed.py
+++ b/tests/devices/test_default_mixed.py
@@ -1049,6 +1049,22 @@ def test_apply_qubitunitary(self, tol):
 
         assert np.allclose(dev.state, target_rho, atol=tol, rtol=0)
 
+    @pytest.mark.parametrize("num_wires", [1, 2, 3])
+    def test_apply_specialunitary(self, tol, num_wires):
+        """Tests that a special unitary is correctly applied"""
+        np.random.seed(2514)
+        theta = np.random.random(4**num_wires - 1)
+
+        dev = qml.device("default.mixed", wires=num_wires)
+        dev.apply([qml.SpecialUnitary(theta, wires=list(range(num_wires)))])
+
+        mat = qml.SpecialUnitary.compute_matrix(theta, num_wires)
+        init_rho = np.zeros((2**num_wires, 2**num_wires))
+        init_rho[0, 0] = 1
+        target_rho = mat @ init_rho @ mat.conj().T
+
+        assert np.allclose(dev.state, target_rho, atol=tol, rtol=0)
+
     def test_apply_pauli_error(self, tol):
         """Tests that PauliError gate is correctly applied"""
         nr_wires = 3
diff --git a/tests/devices/test_default_qubit.py b/tests/devices/test_default_qubit.py
--- a/tests/devices/test_default_qubit.py
+++ b/tests/devices/test_default_qubit.py
@@ -407,6 +407,16 @@ def test_apply_operation_state_preparation(
             [cmath.exp(1j * 0.4) / 2, cmath.exp(1j * -0.4) * math.sqrt(3) / 4],
             [np.array([cmath.exp(1j * 0.4), cmath.exp(1j * -0.4)])],
         ),
+        (qml.SpecialUnitary, [1, 0], [0, 1j], [np.array([np.pi / 2, 0, 0])]),
+        (qml.SpecialUnitary, [1, 0], [0, -1], [np.array([0, np.pi / 2, 0])]),
+        (qml.SpecialUnitary, [1, 0], [1j, 0], [np.array([0, 0, np.pi / 2])]),
+        (qml.SpecialUnitary, [0.6, 0.8], [0.573 + 0.236j, 0.764 + 0.177j], [np.array([0.3, 0, 0])]),
+        (
+            qml.SpecialUnitary,
+            [0.8j, -0.6],
+            [-0.808 + 0.049j, -0.411 + 0.419j],
+            [np.array([0.4, 0.2, 1.2])],
+        ),
     ]
 
     @pytest.mark.parametrize(
@@ -536,6 +546,42 @@ def test_apply_operation_single_wire_with_parameters(
             [np.array([1, 1, 1, -1])],
         ),
         (qml.DiagonalQubitUnitary, [0, 0, 1, 0], [0, 0, 1j, 0], [np.array([-1, 1j, 1j, -1])]),
+        (
+            qml.SpecialUnitary,
+            [0.5, -0.5j, 0.5j, -0.5],
+            [0.382 - 0.322j, -0.322 - 0.382j, 0.322 + 0.382j, -0.382 + 0.322j],
+            [np.eye(15)[4] * 0.7],
+        ),
+        (
+            qml.SpecialUnitary,
+            [0.6, 0, 0, -0.8],
+            [0.553, 0.312, -0.234, -0.737],
+            [np.eye(15)[10] * 0.4],
+        ),
+        (
+            qml.SpecialUnitary,
+            [0, 0, 1, 0],
+            [0, -1j / math.sqrt(2), 1 / math.sqrt(2), 0],
+            [-np.eye(15)[4] * np.pi / 4],
+        ),  # Like Ising XX
+        (
+            qml.SpecialUnitary,
+            [0, 0, 1, 0],
+            [0, -1j / math.sqrt(2), 1 / math.sqrt(2), 0],
+            [-np.eye(15)[9] * np.pi / 4],
+        ),  # Like Ising YY
+        (
+            qml.SpecialUnitary,
+            [0, 0, 0, 1],
+            [0, 0, 0, 1 / math.sqrt(2) - 1j / math.sqrt(2)],
+            [-np.eye(15)[14] * np.pi / 4],
+        ),  # Like Ising ZZ
+        (
+            qml.SpecialUnitary,
+            [0.5, -0.5j, -0.5, -0.5],
+            [-0.616 - 0.018j, 0.316 + 0.243j, 0.427 + 0.437j, 0.294 + 0.043j],
+            [np.linspace(0.1, 3, 15)],
+        ),
         (qml.IsingXX, [0, 0, 1, 0], [0, -1j / math.sqrt(2), 1 / math.sqrt(2), 0], [math.pi / 2]),
         (qml.IsingXX, [0, 0, 0, 1], [-1j / math.sqrt(2), 0, 0, 1 / math.sqrt(2)], [math.pi / 2]),
         (qml.IsingXX, [1, 0, 0, 0], [1 / math.sqrt(2), 0, 0, -1j / math.sqrt(2)], [math.pi / 2]),
diff --git a/tests/devices/test_default_qubit_broadcasting.py b/tests/devices/test_default_qubit_broadcasting.py
--- a/tests/devices/test_default_qubit_broadcasting.py
+++ b/tests/devices/test_default_qubit_broadcasting.py
@@ -236,6 +236,18 @@ def test_apply_operation_state_preparation_broadcasted(
         for s, par in [(state_1, diag_par_5), (state_5, diag_par_1), (state_5, diag_par_5)]
     ]
 
+    # Add qml.SpecialUnitary test cases
+    theta_1_par_1 = [np.array([np.pi / 2, 0, 0])]
+    theta_1_par_5 = [
+        np.array(
+            [[np.pi / 2, 0, 0], [0, np.pi / 2, 0], [0, 0, np.pi / 2], [0.3, 0, 0], [0.4, 0.2, 1.2]]
+        )
+    ]
+    test_data_single_wire_with_parameters += [
+        (qml.SpecialUnitary, s, mat_vec(qml.SpecialUnitary.compute_matrix(par[0], 1), s), par)
+        for s, par in [(state_1, theta_1_par_5), (state_5, theta_1_par_1), (state_5, theta_1_par_5)]
+    ]
+
     # Add qml.Rot test cases
     multi_par_1 = {
         "rz_0": [0.632, 0, 0],
@@ -371,6 +383,14 @@ def test_apply_operation_single_wire_with_parameters_broadcasted(
         for s, par in [(state_1, diag_par_5), (state_5, diag_par_1), (state_5, diag_par_5)]
     ]
 
+    # Add qml.SpecialUnitary test cases
+    theta_2_par_1 = [np.linspace(0.1, 3, 15)]
+    theta_2_par_5 = [np.array([0.4, -0.2, 1.2, -0.5, 2.2])[:, np.newaxis] * np.eye(15)[2::3]]
+    test_data_two_wires_with_parameters += [
+        (qml.SpecialUnitary, s, mat_vec(qml.SpecialUnitary.compute_matrix(par[0], 2), s), par)
+        for s, par in [(state_1, theta_2_par_5), (state_5, theta_2_par_1), (state_5, theta_2_par_5)]
+    ]
+
     @pytest.mark.parametrize(
         "operation,input,expected_output,par", test_data_two_wires_with_parameters
     )
diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py
--- a/tests/devices/test_null_qubit.py
+++ b/tests/devices/test_null_qubit.py
@@ -14,9 +14,8 @@
 """
 Unit tests for the :mod:`pennylane.plugin.NullQubit` device.
 """
-import cmath
-
 # pylint: disable=protected-access,cell-var-from-loop
+from collections import defaultdict
 import math
 
 import pytest
@@ -25,12 +24,18 @@
 from pennylane.devices.null_qubit import NullQubit
 from pennylane import Tracker
 
-from collections import defaultdict
 
+@pytest.fixture(
+    name="nullqubit_device",
+    scope="function",
+    params=[(np.float32, np.complex64), (np.float64, np.complex128)],
+)
+def fixture_nullqubit_device(request):
+    """Create a NullQubit device with the indicated number of wires,
+    parametrizing over real and complex dtypes."""
 
-@pytest.fixture(scope="function", params=[(np.float32, np.complex64), (np.float64, np.complex128)])
-def nullqubit_device(request):
     def _device(wires):
+        """Create NullQubit device."""
         return qml.device(
             "null.qubit", wires=wires, r_dtype=request.param[0], c_dtype=request.param[1]
         )
@@ -62,11 +67,15 @@ def test_dtype_errors():
 
 def test_custom_op_with_matrix():
     """Test that a dummy op with a matrix is supported."""
+    # pylint: disable=too-few-public-methods
 
     class DummyOp(qml.operation.Operation):
+        """Placeholder Operation on a single wire."""
+
         num_wires = 1
 
         def compute_matrix(self):
+            """Compute the matrix of the DummyOp"""
             return np.eye(2)
 
     with qml.queuing.AnnotatedQueue() as q:
@@ -103,28 +112,6 @@ def test_apply_operation_state_preparation(self, nullqubit_device, operation, in
         dev.apply([operation(input, wires=[0, 1])])
         assert dev._state == [0.0]
 
-    test_data_single_wire_with_parameters = [
-        (qml.PhaseShift, [1 / math.sqrt(5), 2 / math.sqrt(5)], [math.pi / 4]),
-        (qml.RX, [1 / math.sqrt(5), 2 / math.sqrt(5)], [math.pi / 4]),
-        (qml.RY, [1 / math.sqrt(5), 2 / math.sqrt(5)], [math.pi / 4]),
-        (qml.RZ, [1 / math.sqrt(5), 2 / math.sqrt(5)], [math.pi / 4]),
-        (qml.MultiRZ, [1 / math.sqrt(5), 2 / math.sqrt(5)], [math.pi / 4]),
-        (qml.Rot, [1 / math.sqrt(5), 2 / math.sqrt(5)], [math.pi / 2, math.pi / 4, math.pi / 8]),
-        (
-            qml.QubitUnitary,
-            [1 / math.sqrt(5), 2 / math.sqrt(5)],
-            [
-                np.array(
-                    [
-                        [1j / math.sqrt(2), 1j / math.sqrt(2)],
-                        [1j / math.sqrt(2), -1j / math.sqrt(2)],
-                    ]
-                )
-            ],
-        ),
-        (qml.DiagonalQubitUnitary, [1 / math.sqrt(5), 2 / math.sqrt(5)], [np.array([-1, 1])]),
-    ]
-
     @pytest.mark.parametrize(
         "op",
         [
@@ -317,6 +304,8 @@ def test_var_two_wires_with_parameters(self, nullqubit_device, operation, input,
 class TestSample:
     """Tests that samples are properly (not) calculated."""
 
+    # pylint: disable=too-few-public-methods
+
     def test_sample_values(self):
         """Tests if the samples returned by sample have
         the correct values
@@ -688,6 +677,7 @@ def test_hermitian_hermitian(self, nullqubit_device, theta, phi, varphi):
 
     def test_hermitian_identity_expectation(self, nullqubit_device, theta, phi, varphi):
         """Test that a tensor product involving an Hermitian matrix and the identity works correctly"""
+        # pylint: disable=unused-argument
         dev = nullqubit_device(wires=2)
 
         A = np.array(
@@ -705,6 +695,7 @@ def test_hermitian_identity_expectation(self, nullqubit_device, theta, phi, varp
 
     def test_hermitian_two_wires_identity_expectation(self, nullqubit_device, theta, phi, varphi):
         """Test that a tensor product involving an Hermitian matrix for two wires and the identity works correctly"""
+        # pylint: disable=unused-argument
         dev = nullqubit_device(wires=3)
 
         A = np.array(
@@ -842,6 +833,7 @@ def test_hermitian_hermitian(self, nullqubit_device, theta, phi, varphi):
 
     def test_hermitian_identity_expectation(self, nullqubit_device, theta, phi, varphi):
         """Test that a tensor product involving an Hermitian matrix and the identity works correctly"""
+        # pylint: disable=unused-argument
         dev = nullqubit_device(wires=2)
 
         A = np.array(
@@ -859,6 +851,7 @@ def test_hermitian_identity_expectation(self, nullqubit_device, theta, phi, varp
 
     def test_hermitian_two_wires_identity_expectation(self, nullqubit_device, theta, phi, varphi):
         """Test that a tensor product involving an Hermitian matrix for two wires and the identity works correctly"""
+        # pylint: disable=unused-argument
         dev = nullqubit_device(wires=3)
 
         A = np.array(
@@ -876,7 +869,6 @@ def test_hermitian_two_wires_identity_expectation(self, nullqubit_device, theta,
         assert dev.var(obs) == [0.0]
 
 
-@pytest.mark.parametrize("inverse", [True, False])
 class TestApplyOps:
     """Tests special methods to apply gates in NullQubit."""
 
@@ -902,40 +894,40 @@ class TestApplyOps:
     ]
 
     @pytest.mark.parametrize("method", single_qubit_ops)
-    def test_apply_single_qubit_op(self, method, inverse):
+    def test_apply_single_qubit_op(self, method):
         """Test if the application of single qubit operations is correct."""
-        state_out = method(self.state, axes=[1], inverse=inverse)
+        state_out = method(self.state, axes=[1])
         assert state_out == [0.0]
 
     @pytest.mark.parametrize("method", two_qubit_ops)
-    def test_apply_two_qubit_op(self, method, inverse):
+    def test_apply_two_qubit_op(self, method):
         """Test if the application of two qubit operations is correct."""
         state_out = method(self.state, axes=[0, 1])
         assert state_out == [0.0]
 
     @pytest.mark.parametrize("method", two_qubit_ops)
-    def test_apply_two_qubit_op_reverse(self, method, inverse):
+    def test_apply_two_qubit_op_reverse(self, method):
         """Test if the application of two qubit operations is correct when the applied wires are
         reversed."""
         state_out = method(self.state, axes=[2, 1])
         assert state_out == [0.0]
 
     @pytest.mark.parametrize("method", three_qubit_ops)
-    def test_apply_three_qubit_op_controls_smaller(self, method, inverse):
+    def test_apply_three_qubit_op_controls_smaller(self, method):
         """Test if the application of three qubit operations is correct when both control wires are
         smaller than the target wire."""
         state_out = method(self.state, axes=[0, 2, 3])
         assert state_out == [0.0]
 
     @pytest.mark.parametrize("method", three_qubit_ops)
-    def test_apply_three_qubit_op_controls_greater(self, method, inverse):
+    def test_apply_three_qubit_op_controls_greater(self, method):
         """Test if the application of three qubit operations is correct when both control wires are
         greater than the target wire."""
         state_out = method(self.state, axes=[2, 1, 0])
         assert state_out == [0.0]
 
     @pytest.mark.parametrize("method", three_qubit_ops)
-    def test_apply_three_qubit_op_controls_split(self, method, inverse):
+    def test_apply_three_qubit_op_controls_split(self, method):
         """Test if the application of three qubit operations is correct when one control wire is smaller
         and one control wire is greater than the target wire."""
         state_out = method(self.state, axes=[3, 1, 2])
@@ -946,9 +938,9 @@ def test_apply_three_qubit_op_controls_split(self, method, inverse):
     ]
 
     @pytest.mark.parametrize("method,par", single_qubit_ops_param)
-    def test_apply_single_qubit_op_(self, method, par, inverse):
+    def test_apply_single_qubit_op_param(self, method, par):
         """Test if the application of single qubit operations (with parameter) is correct."""
-        state_out = method(self.state, axes=[1], parameters=par, inverse=inverse)
+        state_out = method(self.state, axes=[1], parameters=par)
         assert state_out == [0.0]
 
 
@@ -1059,6 +1051,7 @@ def circuit():
         (qml.RZ, [math.pi / 4], {"RZ": 1}),
         (qml.MultiRZ, [math.pi / 2], {"MultiRZ": 1}),
         (qml.DiagonalQubitUnitary, [np.array([-1, 1])], {"DiagonalQubitUnitary": 1}),
+        (qml.SpecialUnitary, [np.array([-1, 1, 0.4])], {"SpecialUnitary": 1}),
     ]
     two_qubit_ops_par = [
         (qml.CRX, [math.pi / 2], {"CRX": 1}),
@@ -1066,6 +1059,7 @@ def circuit():
         (qml.CRZ, [math.pi / 2], {"CRZ": 1}),
         (qml.MultiRZ, [math.pi / 2], {"MultiRZ": 1}),
         (qml.DiagonalQubitUnitary, [np.array([-1, 1, -1, 1])], {"DiagonalQubitUnitary": 1}),
+        (qml.SpecialUnitary, [np.linspace(0.1, 2, 15)], {"SpecialUnitary": 1}),
         (qml.IsingXX, [math.pi / 2], {"IsingXX": 1}),
         (qml.IsingYY, [math.pi / 2], {"IsingYY": 1}),
         (qml.IsingZZ, [math.pi / 2], {"IsingZZ": 1}),
@@ -1152,7 +1146,9 @@ def circuit():
 
 
 class TestState:
-    "Unit test for state and density_matrix operations."
+    """Unit test for state and density_matrix operations."""
+
+    # pylint: disable=too-few-public-methods
     dev = qml.device("null.qubit", wires=3)
 
     @pytest.mark.parametrize(
@@ -1181,9 +1177,13 @@ def circuit():
             return qml.expval(qml.PauliZ(0))
 
         class callback_wrapper:
+            """Callback wrapping class"""
+
+            # pylint: disable=too-few-public-methods
+
             @staticmethod
-            def callback(totals=dict(), history=dict(), latest=dict()):
-                pass
+            def callback(totals=None, history=None, latest=None):
+                """Wrapped callback function."""
 
         wrapper = callback_wrapper()
         spy = mocker.spy(wrapper, "callback")
@@ -1214,6 +1214,7 @@ def callback(totals=dict(), history=dict(), latest=dict()):
 
     def test_shots_execution(self, nullqubit_device, mocker):
         """Test that correct tracks shots."""
+        # pylint: disable=unexpected-keyword-arg
         dev = nullqubit_device(wires=1)
 
         @qml.qnode(dev)
@@ -1221,9 +1222,13 @@ def circuit():
             return qml.expval(qml.PauliZ(0))
 
         class callback_wrapper:
+            """Callback wrapping class"""
+
+            # pylint: disable=too-few-public-methods
+
             @staticmethod
-            def callback(totals=dict(), history=dict(), latest=dict()):
-                pass
+            def callback(totals=None, history=None, latest=None):
+                """Wrapped callback function."""
 
         wrapper = callback_wrapper()
         spy = mocker.spy(wrapper, "callback")
diff --git a/tests/tests_passing_pylint.txt b/tests/tests_passing_pylint.txt
--- a/tests/tests_passing_pylint.txt
+++ b/tests/tests_passing_pylint.txt
@@ -43,3 +43,4 @@ tests/devices/test_default_mixed_autograd.py
 tests/devices/test_default_mixed_jax.py
 tests/devices/test_default_mixed_torch.py
 tests/devices/test_default_mixed_tf.py
+tests/devices/test_null_qubit.py