statistics: kde() Returns a Closure That Can't Be Pickled (tested)

statistics.kde() is new in Python 3.13 -- it fits a kernel density estimate to a dataset and hands back something you call like a function to get the estimated density at any point. We measured what that returned object actually is, and whether it survives being pickled.

1. kde() returns a plain function, not a reusable, inspectable object

import statistics as st

data = [1, 2, 3, 4, 5]
density = st.kde(data, h=1)

print(type(density))
print(density(3))
print(repr(density))
<class 'function'>
0.1981731324932191
<function kde.<locals>.pdf at 0x7f9712d15620>

Measured: kde() returns an ordinary Python function -- specifically a closure defined inside kde()'s own body (its repr literally says kde.<locals>.pdf). Calling it works exactly like the documentation shows. There's no separate model object with its own methods or attributes -- just this one callable.

2. That closure can't be pickled

import pickle
import statistics as st

data = [1, 2, 3, 4, 5]
density = st.kde(data, h=1)

try:
    pickle.dumps(density)
except AttributeError as e:
    print("pickle failed:", e)
pickle failed: Can't get local object 'kde.<locals>.pdf'

Measured: pickle.dumps() raises AttributeError because the function returned by kde() is defined inside another function (a closure), and Python's pickle module can only pickle functions it can look up by their module-level qualified name -- a locally-defined function has no such importable path. This breaks silently-expected workflows: caching a fitted density estimate to disk with pickle, sending it to a multiprocessing.Pool worker, or storing it in a joblib/Redis cache all rely on pickling, and all fail the same way.

3. The underlying data and parameters aren't recoverable from the returned function either

import statistics as st

data = [1, 2, 3, 4, 5]
density = st.kde(data, h=1)

print(density.__closure__ is not None)
print(hasattr(density, "data"))
print(hasattr(density, "h"))
True
False
False

Measured: the fitted data and bandwidth are captured in the function's __closure__ cells, but there's no public attribute exposing them -- density.data and density.h both don't exist. If a fitted kde() result needs to survive a process boundary or be written to disk, the only option is re-running kde() on the original data and h again on the other side -- which means those two values need to be saved separately, not the function kde() returns.

Frequently asked questions

Can you pickle the function returned by statistics.kde()?

No -- we measured pickle.dumps() on it raising AttributeError: Can't get local object 'kde.<locals>.pdf', because kde() returns a closure defined inside its own function body, which pickle can't look up by name.

Why can't statistics.kde()'s return value be sent to a multiprocessing worker?

Because it can't be pickled -- multiprocessing.Pool relies on pickle to send callables to worker processes, and we measured the kde() closure failing to pickle with the same AttributeError as a direct pickle.dumps() call.

Does statistics.kde()'s returned function expose the original data or bandwidth?

No -- we measured it having no public data or h attribute, even though both values are captured internally in the function's closure cells. Recovering them means keeping the original arguments and calling kde() again.

How do I cache or reuse a statistics.kde() result across processes?

Save the original data list and the h/kernel arguments instead of the returned function, and call kde() again wherever you need it -- we measured the function itself as fundamentally unpicklable, so there's no way to serialize the fitted result directly.

Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in statistics module: called the new (3.13) kde() function and tried to pickle the object it returns. PlainHeaders is our extension; Python's statistics module is built in.