diff --git a/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt b/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt
index 3152ac2317a76d6881d3dfb0123e25c114cf9bda..f87d7cef0377e0d8052f878083935847813f5e70 100644
--- a/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt
+++ b/bindings/pyroot_experimental/PyROOT/test/CMakeLists.txt
@@ -71,6 +71,9 @@ ROOT_ADD_PYUNITTEST(pyroot_pyz_rvec_asrvec rvec_asrvec.py)
 # RDataFrame and subclasses pythonizations
 ROOT_ADD_PYUNITTEST(pyroot_pyz_rdataframe_asnumpy rdataframe_asnumpy.py)
 
+# Passing Python callables to ROOT.TF
+ROOT_ADD_PYUNITTEST(pyroot_pyz_tf_pycallables tf_pycallables.py)
+
 if(roofit)
   # RooAbsCollection and subclasses pythonizations
   ROOT_ADD_PYUNITTEST(pyroot_pyz_rooabscollection_len rooabscollection_len.py)
diff --git a/bindings/pyroot_experimental/PyROOT/test/tf_pycallables.py b/bindings/pyroot_experimental/PyROOT/test/tf_pycallables.py
new file mode 100644
index 0000000000000000000000000000000000000000..5d6db3f8347976e6205fb4af705567dac6f735aa
--- /dev/null
+++ b/bindings/pyroot_experimental/PyROOT/test/tf_pycallables.py
@@ -0,0 +1,152 @@
+"""
+Tests for passing Python callables when constructing TFX classes.
+
+This feature is not implemented by a PyROOT pythonization, but by a converter of
+Cppyy that creates a C++ wrapper to invoke the Python callable.
+"""
+
+import unittest
+import math
+
+import ROOT
+
+
+def pyf_tf1_identity(x, p):
+    return x[0]
+
+
+def pyf_tf1_params(x, p):
+    return p[0] * x[0] + p[1]
+
+
+class pyf_tf1_callable:
+    def __call__(self, x, p):
+        return p[0] * x[0] + p[1]
+
+
+def pyf_tf1_gauss(x, p):
+    return p[0] * 1.0 / math.sqrt(2.0 * math.pi * p[2]**2) * math.exp(-(x[0] - p[1])**2 / 2.0 / p[2]**2)
+
+
+class TF1(unittest.TestCase):
+    """
+    Test passing Python callables to ROOT::TF1
+    """
+
+    def test_identity(self):
+        """
+        Test simple function without parameters
+        """
+        f = ROOT.TF1("tf1_identity", pyf_tf1_identity, 0.0, 1.0)
+        for x in [0.0, -1.0, 42.0]:
+            self.assertEqual(f.Eval(x), x)
+
+    def test_params(self):
+        """
+        Test function with parameters
+        """
+        npars = 2
+        f = ROOT.TF1("tf1_params", pyf_tf1_params, 0.0, 1.0, npars)
+        par1 = 2.0
+        par2 = -1.0
+        f.SetParameter(0, par1)
+        f.SetParameter(1, par2)
+        for x in [0.0, -1.0, 42.0]:
+            self.assertEqual(f.Eval(x), pyf_tf1_params([x], [par1, par2]))
+
+    def test_callable(self):
+        """
+        Test function provided as callable
+        """
+        npars = 2
+        pycallable = pyf_tf1_callable()
+        f = ROOT.TF1("tf1_callable", pycallable, 0.0, 1.0, npars)
+        par1 = 2.0
+        par2 = -1.0
+        f.SetParameter(0, par1)
+        f.SetParameter(1, par2)
+        for x in [0.0, -1.0, 42.0]:
+            self.assertEqual(f.Eval(x), pycallable([x], [par1, par2]))
+
+
+    def test_fitgauss(self):
+        """
+        Test fitting a histogram to a Python function
+        """
+        # Gaus function
+        f = ROOT.TF1("tf1_fitgauss", pyf_tf1_gauss, -4, 4, 3)
+        f.SetParameter(0, 10.0) # scale
+        f.SetParameter(1, -1.0) # mean
+        f.SetParameter(2, 2.0) # standard deviation
+
+        # Sample gauss in histogram
+        h = ROOT.TH1F("h", "test", 100, -4, 4)
+        h.FillRandom("gaus", 100000)
+        h.Scale(1.0 / 100000.0 * 100.0 / 8.0) # Normalize as density
+
+        # Fit to histogram and get parameters
+        h.Fit( f, "0Q" )
+        scale = f.GetParameter(0)
+        mean = f.GetParameter(1)
+        std = f.GetParameter(2)
+
+        self.assertAlmostEqual(scale, 1.0, 2)
+        self.assertAlmostEqual(mean, 0.0, 2)
+        self.assertAlmostEqual(abs(std), 1.0, 2)
+
+
+def pyf_tf2_params(x, p):
+    return p[0] * x[0] + p[1] * x[1] + p[2]
+
+
+class TF2(unittest.TestCase):
+    """
+    Test passing Python callables to ROOT::TF2
+    """
+
+    def test_params(self):
+        """
+        Test function with parameters
+        """
+        npars = 3
+        f = ROOT.TF1("tf2_params", pyf_tf2_params, 0.0, 1.0, npars)
+        par1 = 2.0
+        par2 = -1.0
+        par3 = 1.0
+        f.SetParameter(0, par1)
+        f.SetParameter(1, par2)
+        f.SetParameter(2, par3)
+        for x in [(0.0, 0.0), (-1.0, 1.0), (42.0, 0.0)]:
+            self.assertEqual(f.Eval(*x), pyf_tf2_params(x, [par1, par2, par3]))
+
+
+def pyf_tf3_params(x, p):
+    return p[0] * x[0] + p[1] * x[1] + p[2] * x[2] + p[3]
+
+
+class TF3(unittest.TestCase):
+    """
+    Test passing Python callables to ROOT::TF3
+    """
+
+    def test_params(self):
+        """
+        Test function with parameters
+        """
+        npars = 4
+        f = ROOT.TF1("tf2_params", pyf_tf2_params, 0.0, 1.0, npars)
+        par1 = 2.0
+        par2 = -1.0
+        par3 = 1.0
+        par4 = 3.0
+        f.SetParameter(0, par1)
+        f.SetParameter(1, par2)
+        f.SetParameter(2, par3)
+        f.SetParameter(3, par4)
+        for x in [(0.0, 0.0, 0.0), (-1.0, 1.0, 2.0), (42.0, 0.0, -10.0)]:
+            self.assertEqual(f.Eval(*x), pyf_tf2_params(x, [par1, par2, par3, par4]))
+
+
+if __name__ == '__main__':
+    unittest.main()
+