diff --git a/pybamm/expression_tree/binary_operators.py b/pybamm/expression_tree/binary_operators.py
index 853131326..c8c9f119c 100644
--- a/pybamm/expression_tree/binary_operators.py
+++ b/pybamm/expression_tree/binary_operators.py
@@ -110,10 +110,6 @@ class BinaryOperator(pybamm.Symbol):
                 )
             )
 
-    def _binary_evaluate(self, left, right):
-        """ Perform binary operation on nodes 'left' and 'right'. """
-        raise NotImplementedError
-
     def new_copy(self):
         """ See :meth:`pybamm.Symbol.new_copy()`. """
         # process children
@@ -147,6 +143,10 @@ class BinaryOperator(pybamm.Symbol):
         right = self.children[1].evaluate_for_shape()
         return self._binary_evaluate(left, right)
 
+    def _binary_evaluate(self, left, right):
+        """ Perform binary operation on nodes 'left' and 'right'. """
+        raise NotImplementedError
+
 
 class Power(BinaryOperator):
     """A node in the expression tree representing a `**` power operator
@@ -158,17 +158,14 @@ class Power(BinaryOperator):
         """ See :meth:`pybamm.BinaryOperator.__init__()`. """
         super().__init__("**", left, right)
 
-    def diff(self, variable):
-        """ See :meth:`pybamm.Symbol.diff()`. """
-        if variable.id == self.id:
-            return pybamm.Scalar(1)
-        else:
-            # apply chain rule and power rule
-            base, exponent = self.orphans
-            return base ** (exponent - 1) * (
-                exponent * base.diff(variable)
-                + base * pybamm.Function(np.log, base) * exponent.diff(variable)
-            )
+    def _diff(self, variable):
+        """ See :meth:`pybamm.Symbol._diff()`. """
+        # apply chain rule and power rule
+        base, exponent = self.orphans
+        return base ** (exponent - 1) * (
+            exponent * base.diff(variable)
+            + base * pybamm.Function(np.log, base) * exponent.diff(variable)
+        )
 
     def jac(self, variable):
         """ See :meth:`pybamm.Symbol.jac()`. """
@@ -219,12 +216,9 @@ class Addition(BinaryOperator):
         """ See :meth:`pybamm.BinaryOperator.__init__()`. """
         super().__init__("+", left, right)
 
-    def diff(self, variable):
-        """ See :meth:`pybamm.Symbol.diff()`. """
-        if variable.id == self.id:
-            return pybamm.Scalar(1)
-        else:
-            return self.left.diff(variable) + self.right.diff(variable)
+    def _diff(self, variable):
+        """ See :meth:`pybamm.Symbol._diff()`. """
+        return self.left.diff(variable) + self.right.diff(variable)
 
     def jac(self, variable):
         """ See :meth:`pybamm.Symbol.jac()`. """
@@ -273,12 +267,9 @@ class Subtraction(BinaryOperator):
 
         super().__init__("-", left, right)
 
-    def diff(self, variable):
-        """ See :meth:`pybamm.Symbol.diff()`. """
-        if variable.id == self.id:
-            return pybamm.Scalar(1)
-        else:
-            return self.left.diff(variable) - self.right.diff(variable)
+    def _diff(self, variable):
+        """ See :meth:`pybamm.Symbol._diff()`. """
+        return self.left.diff(variable) - self.right.diff(variable)
 
     def jac(self, variable):
         """ See :meth:`pybamm.Symbol.jac()`. """
@@ -330,14 +321,11 @@ class Multiplication(BinaryOperator):
 
         super().__init__("*", left, right)
 
-    def diff(self, variable):
-        """ See :meth:`pybamm.Symbol.diff()`. """
-        if variable.id == self.id:
-            return pybamm.Scalar(1)
-        else:
-            # apply product rule
-            left, right = self.orphans
-            return left.diff(variable) * right + left * right.diff(variable)
+    def _diff(self, variable):
+        """ See :meth:`pybamm.Symbol._diff()`. """
+        # apply product rule
+        left, right = self.orphans
+        return left.diff(variable) * right + left * right.diff(variable)
 
     def jac(self, variable):
         """ See :meth:`pybamm.Symbol.jac()`. """
@@ -449,16 +437,11 @@ class Division(BinaryOperator):
         """ See :meth:`pybamm.BinaryOperator.__init__()`. """
         super().__init__("/", left, right)
 
-    def diff(self, variable):
-        """ See :meth:`pybamm.Symbol.diff()`. """
-        if variable.id == self.id:
-            return pybamm.Scalar(1)
-        else:
-            # apply quotient rule
-            top, bottom = self.orphans
-            return (
-                top.diff(variable) * bottom - top * bottom.diff(variable)
-            ) / bottom ** 2
+    def _diff(self, variable):
+        """ See :meth:`pybamm.Symbol._diff()`. """
+        # apply quotient rule
+        top, bottom = self.orphans
+        return (top.diff(variable) * bottom - top * bottom.diff(variable)) / bottom ** 2
 
     def jac(self, variable):
         """ See :meth:`pybamm.Symbol.jac()`. """
diff --git a/pybamm/expression_tree/symbol.py b/pybamm/expression_tree/symbol.py
index 364e7d3df..eff3a2769 100644
--- a/pybamm/expression_tree/symbol.py
+++ b/pybamm/expression_tree/symbol.py
@@ -331,9 +331,10 @@ class Symbol(anytree.NodeMixin):
 
     def diff(self, variable):
         """
-        Differentiate a symbol with respect to a variable. Default behaviour is to
-        return `1` if differentiating with respect to yourself and zero otherwise.
-        Binary and Unary Operators override this.
+        Differentiate a symbol with respect to a variable. For any symbol that can be
+        differentiated, return `1` if differentiating with respect to yourself,
+        `self._diff(variable)` if `variable` is in the expression tree of the symbol,
+        and zero otherwise.
 
         Parameters
         ----------
@@ -343,9 +344,15 @@ class Symbol(anytree.NodeMixin):
         """
         if variable.id == self.id:
             return pybamm.Scalar(1)
+        elif any(variable.id == x.id for x in self.pre_order()):
+            return self._diff(variable)
         else:
             return pybamm.Scalar(0)
 
+    def _diff(self, variable):
+        "Default behaviour for differentiation, overriden by Binary and Unary Operators"
+        raise NotImplementedError
+
     def jac(self, variable):
         """
         Differentiate a symbol with respect to a (slice of) a State Vector.
diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py
index daf1b36ed..6563d9462 100644
--- a/pybamm/expression_tree/unary_operators.py
+++ b/pybamm/expression_tree/unary_operators.py
@@ -88,12 +88,9 @@ class Negate(UnaryOperator):
         """ See :meth:`pybamm.Symbol.__str__()`. """
         return "{}{!s}".format(self.name, self.child)
 
-    def diff(self, variable):
-        """ See :meth:`pybamm.Symbol.diff()`. """
-        if variable.id == self.id:
-            return pybamm.Scalar(1)
-        else:
-            return -self.child.diff(variable)
+    def _diff(self, variable):
+        """ See :meth:`pybamm.Symbol._diff()`. """
+        return -self.child.diff(variable)
 
     def jac(self, variable):
         """ See :meth:`pybamm.Symbol.jac()`. """
@@ -159,19 +156,10 @@ class Function(UnaryOperator):
         else:
             self.takes_no_params = len(signature(func).parameters) == 0
 
-    def diff(self, variable):
-        """ See :meth:`pybamm.Symbol.diff()`. """
-        if variable.id == self.id:
-            return pybamm.Scalar(1)
-        else:
-            child = self.orphans[0]
-            if variable.id in [symbol.id for symbol in child.pre_order()]:
-                # if variable appears in the function,use autograd to differentiate
-                # function, and apply chain rule
-                return child.diff(variable) * Function(autograd.grad(self.func), child)
-            else:
-                # otherwise the derivative of the function is zero
-                return pybamm.Scalar(0)
+    def _diff(self, variable):
+        """ See :meth:`pybamm.Symbol._diff()`. """
+        child = self.orphans[0]
+        return child.diff(variable) * Function(autograd.grad(self.func), child)
 
     def jac(self, variable):
         """ See :meth:`pybamm.Symbol.jac()`. """

Faster `diff`
Taking `diff` can be a bit slow. Might be able to speed up by first checking that the symbol with respect to which we want to differentiate is not in the expression tree of the symbol being differentiated (in which case return 0)
diff --git a/tests/unit/test_expression_tree/test_symbolic_diff.py b/tests/unit/test_expression_tree/test_symbolic_diff.py
index 97af4e539..75d06d614 100644
--- a/tests/unit/test_expression_tree/test_symbolic_diff.py
+++ b/tests/unit/test_expression_tree/test_symbolic_diff.py
@@ -57,6 +57,19 @@ class TestSymbolicDifferentiation(unittest.TestCase):
         func = pybamm.Function(np.sin, a * b)
         self.assertEqual(func.diff(a).evaluate(y=y), 3 * np.cos(15))
 
+    def test_diff_zero(self):
+        a = pybamm.StateVector(slice(0, 1))
+        b = pybamm.StateVector(slice(1, 2))
+        func = (a * 2 + 5 * (-a)) / (a * a)
+        self.assertEqual(func.diff(b).id, pybamm.Scalar(0).id)
+        self.assertNotEqual(func.diff(a).id, pybamm.Scalar(0).id)
+
+    def test_exceptions(self):
+        a = pybamm.Symbol("a")
+        b = pybamm.Symbol("b")
+        with self.assertRaises(NotImplementedError):
+            a._diff(b)
+
 
 if __name__ == "__main__":
     print("Add -v for more debug output")

pip install -e .[dev,docs]
parse_log_pytest
pytest
flake8 sphinx pytest
apt-get update apt-get install -y gcc
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
anyio==3.6.2
anytree==2.8.0
argon2-cffi==21.3.0
argon2-cffi-bindings==21.2.0
async-generator==1.10
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
autograd==1.6.2
Babel==2.11.0
backcall==0.2.0
bleach==4.1.0
certifi==2021.5.30
cffi==1.15.1
charset-normalizer==2.0.12
comm==0.1.4
contextvars==2.4
cycler==0.11.0
dataclasses==0.8
decorator==5.1.1
defusedxml==0.7.1
docutils==0.17.1
entrypoints==0.4
flake8==5.0.4
future==1.0.0
guzzle-sphinx-theme==0.7.11
idna==3.10
imagesize==1.4.1
immutables==0.19
importlib-metadata==4.2.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
ipykernel==5.5.6
ipython==7.16.3
ipython-genutils==0.2.0
ipywidgets==7.8.5
jedi==0.17.2
Jinja2==3.0.3
json5==0.9.16
jsonschema==3.2.0
jupyter==1.1.1
jupyter-client==7.1.2
jupyter-console==6.4.3
jupyter-core==4.9.2
jupyter-server==1.13.1
jupyterlab==3.2.9
jupyterlab-pygments==0.1.2
jupyterlab-server==2.10.3
jupyterlab_widgets==1.1.11
kiwisolver==1.3.1
MarkupSafe==2.0.1
matplotlib==3.3.4
mccabe==0.7.0
mistune==0.8.4
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
nbclassic==0.3.5
nbclient==0.5.9
nbconvert==6.0.7
nbformat==5.1.3
nest-asyncio==1.6.0
notebook==6.4.10
numpy==1.19.5
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pandas==1.1.5
pandocfilters==1.5.1
parso==0.7.1
pexpect==4.9.0
pickleshare==0.7.5
Pillow==8.4.0
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
prometheus-client==0.17.1
prompt-toolkit==3.0.36
ptyprocess==0.7.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
-e git+https://github.com/pybamm-team/PyBaMM.git@eea60233e78571d7995f6766991482f61c54a040#egg=pybamm
pycodestyle==2.9.1
pycparser==2.21
pyflakes==2.5.0
Pygments==2.14.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pyrsistent==0.18.0
pytest==6.2.4
python-dateutil==2.9.0.post0
pytz==2025.2
pyzmq==25.1.2
requests==2.27.1
scipy==1.5.4
Send2Trash==1.8.3
six==1.17.0
sniffio==1.2.0
snowballstemmer==2.2.0
Sphinx==4.3.2
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
terminado==0.12.1
testpath==0.6.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tornado==6.1
traitlets==4.3.3
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
wcwidth==0.2.13
webencodings==0.5.1
websocket-client==1.3.1
widgetsnbextension==3.6.10
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: PyBaMM
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - anyio==3.6.2
      - anytree==2.8.0
      - argon2-cffi==21.3.0
      - argon2-cffi-bindings==21.2.0
      - async-generator==1.10
      - autograd==1.6.2
      - babel==2.11.0
      - backcall==0.2.0
      - bleach==4.1.0
      - cffi==1.15.1
      - charset-normalizer==2.0.12
      - comm==0.1.4
      - contextvars==2.4
      - cycler==0.11.0
      - dataclasses==0.8
      - decorator==5.1.1
      - defusedxml==0.7.1
      - docutils==0.17.1
      - entrypoints==0.4
      - flake8==5.0.4
      - future==1.0.0
      - guzzle-sphinx-theme==0.7.11
      - idna==3.10
      - imagesize==1.4.1
      - immutables==0.19
      - importlib-metadata==4.2.0
      - ipykernel==5.5.6
      - ipython==7.16.3
      - ipython-genutils==0.2.0
      - ipywidgets==7.8.5
      - jedi==0.17.2
      - jinja2==3.0.3
      - json5==0.9.16
      - jsonschema==3.2.0
      - jupyter==1.1.1
      - jupyter-client==7.1.2
      - jupyter-console==6.4.3
      - jupyter-core==4.9.2
      - jupyter-server==1.13.1
      - jupyterlab==3.2.9
      - jupyterlab-pygments==0.1.2
      - jupyterlab-server==2.10.3
      - jupyterlab-widgets==1.1.11
      - kiwisolver==1.3.1
      - markupsafe==2.0.1
      - matplotlib==3.3.4
      - mccabe==0.7.0
      - mistune==0.8.4
      - nbclassic==0.3.5
      - nbclient==0.5.9
      - nbconvert==6.0.7
      - nbformat==5.1.3
      - nest-asyncio==1.6.0
      - notebook==6.4.10
      - numpy==1.19.5
      - pandas==1.1.5
      - pandocfilters==1.5.1
      - parso==0.7.1
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - pillow==8.4.0
      - prometheus-client==0.17.1
      - prompt-toolkit==3.0.36
      - ptyprocess==0.7.0
      - pycodestyle==2.9.1
      - pycparser==2.21
      - pyflakes==2.5.0
      - pygments==2.14.0
      - pyrsistent==0.18.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyzmq==25.1.2
      - requests==2.27.1
      - scipy==1.5.4
      - send2trash==1.8.3
      - six==1.17.0
      - sniffio==1.2.0
      - snowballstemmer==2.2.0
      - sphinx==4.3.2
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - terminado==0.12.1
      - testpath==0.6.0
      - tornado==6.1
      - traitlets==4.3.3
      - urllib3==1.26.20
      - wcwidth==0.2.13
      - webencodings==0.5.1
      - websocket-client==1.3.1
      - widgetsnbextension==3.6.10
prefix: /opt/conda/envs/PyBaMM

tests/unit/test_expression_tree/test_symbolic_diff.py::TestSymbolicDifferentiation::test_diff_zero tests/unit/test_expression_tree/test_symbolic_diff.py::TestSymbolicDifferentiation::test_exceptions
tests/unit/test_expression_tree/test_symbolic_diff.py::TestSymbolicDifferentiation::test_advanced tests/unit/test_expression_tree/test_symbolic_diff.py::TestSymbolicDifferentiation::test_advanced_functions
pybamm/expression_tree/unary_operators.py pybamm/expression_tree/binary_operators.py pybamm/expression_tree/symbol.py
pybamm/expression_tree/unary_operators.py pybamm/expression_tree/binary_operators.py pybamm/expression_tree/symbol.py