Skip to content

Python 3.14 support #3595

@JCGoran

Description

@JCGoran

Python 3.14 is scheduled for release in October 2025. This issue is here to assess the support status of this release in NEURON.

The below was tested on an Ubuntu 24.04 LTS x86_64 machine using the deadsnakes repo. Python version used is 3.14.0~rc2-1+noble1.

Procedure

EDIT: the updating of requirements files has been added in https://github.com/neuronsimulator/nrn/tree/jelic/py314 (not merged yet). However, the rest of the guide should still be followed.

Create a fresh venv and install dependencies (use uv since pip is slow):

python3.14 -m venv venv_3.14
source venv_3.14/bin/activate
pip install -r ci/uv_requirements.txt

EDIT: due to recent developments regarding matplotlib compat, we need to update the requirements file:

diff --git a/nrn_requirements.txt b/nrn_requirements.txt
index 7c1d84ec3..87d039bb6 100644
--- a/nrn_requirements.txt
+++ b/nrn_requirements.txt
@@ -1,4 +1,5 @@
-matplotlib<=3.10.0
+matplotlib<=3.10.0; python_version < "3.14"
+matplotlib>=3.10.5; python_version >= "3.14"
 ipython<=8.32.0
 mpi4py<=4.0.3
 find_libpython<=0.4.0

then update the requirements file:

uv pip compile nrn_requirements.txt ci_requirements.txt docs/docs_requirements.txt packaging/python/test_requirements.txt -o ci/requirements.txt --universal --python-version 3.9 --generate-hashes

then install the requirements:

uv pip install -r ci/requirements.txt

First hurdle: scipy needs to be built from source, but is missing a required dependency:

      Run-time dependency openblas found: NO (tried pkgconfig and cmake)
      Run-time dependency openblas found: NO (tried pkgconfig and cmake)

      ../scipy/meson.build:274:9: ERROR: Dependency "OpenBLAS" not found, tried pkgconfig and cmake

The correct package to install is libopenblas-dev:

sudo apt install -y libopenblas-dev

Next up, a rather long error message from PyO3:

        --- stderr
        error: the configured Python interpreter version (3.14) is newer than PyO3's maximum supported version (3.13)
        = help: please check if an updated version of PyO3 is available. Current version: 0.24.0
        = help: set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI
      warning: build failed, waiting for other jobs to finish...
      💥 maturin failed
        Caused by: Failed to build a native library through cargo
        Caused by: Cargo build finished with "exit status: 101": `env -u CARGO PYO3_BUILD_EXTENSION_MODULE="1" PYO3_ENVIRONMENT_SIGNATURE="cpython-3.14-64bit"
      PYO3_PYTHON="/home/jelic/.cache/uv/builds-v0/.tmpKyRli1/bin/python" PYTHON_SYS_EXECUTABLE="/home/jelic/.cache/uv/builds-v0/.tmpKyRli1/bin/python"
      "cargo" "rustc" "--features" "pyo3/extension-module" "--message-format" "json-render-diagnostics" "--manifest-path"
      "/home/jelic/.cache/uv/sdists-v9/pypi/rpds-py/0.24.0/QRRtaWjeVYXbO0xubaMUV/src/Cargo.toml" "--release" "--lib"`
      Error: command ['maturin', 'pep517', 'build-wheel', '-i', '/home/jelic/.cache/uv/builds-v0/.tmpKyRli1/bin/python', '--compatibility', 'off'] returned
      non-zero exit status 1

okay, we need to do:

export PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1

and now all of the packages build successfully.

Next up, configure NEURON:

cmake -B build/temp -DNRN_ENABLE_TESTS=ON -DNRN_ENABLE_CORENEURON=ON -DNRN_ENABLE_INTERVIEWS=OFF -G Ninja

and build it:

cmake --build build/temp

We get some deprecation warnings but overall the build runs fine:

In file included from external/pybind11/include/pybind11/embed.h:13,
                 from src/nmodl/pybind/pyembed.cpp:12:
external/pybind11/include/pybind11/eval.h: In function ‘pybind11::object pybind11::eval_file(str, object, object)’:
external/pybind11/include/pybind11/eval.h:136:28: warning: ‘FILE* _Py_fopen_obj(PyObject*, const char*)’ is deprecated [-Wdepreca
ted-declarations]
  136 |     FILE *f = _Py_fopen_obj(fname.ptr(), "r");
      |               ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~
In file included from /usr/include/python3.14/fileutils.h:55,
                 from /usr/include/python3.14/Python.h:145,
                 from external/pybind11/include/pybind11/detail/../detail/common.h:274,
                 from external/pybind11/include/pybind11/detail/../attr.h:13,
                 from external/pybind11/include/pybind11/detail/class.h:12,
                 from external/pybind11/include/pybind11/pybind11.h:13,
                 from external/pybind11/include/pybind11/embed.h:12:
/usr/include/python3.14/cpython/fileutils.h:11:1: note: declared here
   11 | _Py_fopen_obj(PyObject *path, const char *mode)
      | ^~~~~~~~~~~~~

Running the tests:

ctest --test-dir build/temp

shows that most of the tests pass, except two, which fail with:

Start testing: Sep 04 11:34 CEST
----------------------------------------------------------
22/478 Testing: unit_tests::python_unit_tests
22/478 Test: unit_tests::python_unit_tests
Command: "/usr/bin/cmake" "-E" "env" "NEURONHOME=build/temp/share/nrn" "NRNHOME=build/temp" "NMODLHOME=build/temp" "NMODL_PYLIB=/usr/lib/x86_64-linux-gnu/libpython3.14.so" "PATH=build/temp/bin:venv_3.14/bin:/.local/bin:/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin:/software/diff-so-fancy:/usr/local/go/bin:/opt/nvidia/hpc_sdk/Linux_x86_64/25.3/compilers/bin:/software/diff-so-fancy:/usr/local/go/bin:/opt/nvidia/hpc_sdk/Linux_x86_64/25.3/compilers/bin" "LD_LIBRARY_PATH=build/temp/lib" "CORENRNHOME=build/temp" "PYTHONPATH=docs/nmodl/python_scripts:build/temp/lib/python:test/rxd" "venv_3.14/bin/python3" "-m" "pytest" "--capture=tee-sys" "--cov-report=xml" "--cov=neuron" "test/unit_tests/hoc_python"
Directory: build/temp/test/unit_tests/python_unit_tests
"unit_tests::python_unit_tests" start time: Sep 04 11:34 CEST
Output:
----------------------------------------------------------
============================= test session starts ==============================
platform linux -- Python 3.14.0rc2, pytest-8.1.1, pluggy-1.5.0
rootdir: /software/nrn-clean
configfile: pyproject.toml
plugins: anyio-4.9.0, cov-6.0.0
collected 15 items

../../../../../test/unit_tests/hoc_python/test_StringFunctions.py ...... [ 40%]
...NEURON: String is not a HOC template
 near line 0
 double x[2]
            ^
        StringFunctions[0].alias_list(...)
.NULLobject has 0 references
Vector[1] has 9 references
   hoc_obj_[0]
   o
   Foo[0].o
   Foo[0].o2[0]
   List[11].object(0)
  found 1 of them
...                                                                  [ 86%]
../../../../../test/unit_tests/hoc_python/test_refcounts.py FF           [100%]

=================================== FAILURES ===================================
________________________ test_seg_from_sec_x_ref_python ________________________

    def test_seg_from_sec_x_ref_python():
        """
        A reproducer of calling `seg_from_sec_x` with a Python Section
        """
        soma1 = nrn.Section("soma1")
        nc1 = h.NetCon(soma1(0.5)._ref_v, None)
        seg1 = nc1.preseg()
        del soma1
    
>       assert sys.getrefcount(seg1) == 2
E       assert 1 == 2
E        +  where 1 = <built-in function getrefcount>(soma1(0.5))
E        +    where <built-in function getrefcount> = sys.getrefcount

../../../../../test/unit_tests/hoc_python/test_refcounts.py:15: AssertionError
________________________ test_seg_from_sec_x_ref_hocpy _________________________

    def test_seg_from_sec_x_ref_hocpy():
        """A reproducer of calling `seg_from_sec_x` without a Python Section
    
        Without the fix we would get one extra ref to sec and possibly (upon interpreter exit):
          "Fatal Python error: bool_dealloc: "deallocating True or False: bug likely caused by a
           refcount error in a C extension"
        """
        h("create soma")
        h("objref nc")
        h("objref nil")
        h("soma nc = new NetCon(&v(0.5), nil)")
        nc = h.nc
        seg = nc.preseg()  # uses `seg_from_sec_x`
    
>       assert sys.getrefcount(seg) == 2
E       assert 1 == 2
E        +  where 1 = <built-in function getrefcount>(soma(0.5))
E        +    where <built-in function getrefcount> = sys.getrefcount

../../../../../test/unit_tests/hoc_python/test_refcounts.py:45: AssertionError

-------- coverage: platform linux, python 3.14.0-candidate-2 ---------
Coverage XML written to file coverage.xml

=========================== short test summary info ============================
FAILED ../../../../../test/unit_tests/hoc_python/test_refcounts.py::test_seg_from_sec_x_ref_python
FAILED ../../../../../test/unit_tests/hoc_python/test_refcounts.py::test_seg_from_sec_x_ref_hocpy
========================= 2 failed, 13 passed in 1.01s =========================
<end of output>
Test time =   1.34 sec
----------------------------------------------------------
Test Failed.
"unit_tests::python_unit_tests" end time: Sep 04 11:34 CEST
"unit_tests::python_unit_tests" time elapsed: 00:00:01
----------------------------------------------------------

457/478 Testing: usecase_longitudinal_diffusion
457/478 Test: usecase_longitudinal_diffusion
Command: "/usr/bin/cmake" "-E" "env" "PATH=build/temp/bin:venv_3.14/bin:/.local/bin:/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin:/software/diff-so-fancy:/usr/local/go/bin:/opt/nvidia/hpc_sdk/Linux_x86_64/25.3/compilers/bin:/software/diff-so-fancy:/usr/local/go/bin:/opt/nvidia/hpc_sdk/Linux_x86_64/25.3/compilers/bin" "NRNHOME=build/temp" "NEURONHOME=build/temp/share/nrn" "CORENRNHOME=build/temp" "NMODL_PYLIB=/usr/lib/x86_64-linux-gnu/libpython3.14.so" "NMODLHOME=build/temp" "PYTHONPATH=build/temp/lib/python:" "test/nmodl/transpiler/usecases/run_test.sh" "build/temp/bin/nmodl" "test/nmodl/transpiler/usecases/longitudinal_diffusion"
Directory: build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion
"usecase_longitudinal_diffusion" start time: Sep 04 11:34 CEST
Output:
----------------------------------------------------------
-- Running NRN+nocmodl ------
rm: cannot remove 'tmp': No such file or directory
build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion
cfiles =
Mod files: "test/nmodl/transpiler/usecases/longitudinal_diffusion/heat_eqn_array.mod" "test/nmodl/transpiler/usecases/longitudinal_diffusion/heat_eqn_function.mod" "test/nmodl/transpiler/usecases/longitudinal_diffusion/heat_eqn_global.mod" "test/nmodl/transpiler/usecases/longitudinal_diffusion/heat_eqn_scalar.mod" "test/nmodl/transpiler/usecases/longitudinal_diffusion/heat_eqn_thread_vars.mod"

Creating 'x86_64' directory for .o files.

MODOBJS= ./heat_eqn_array.o ./heat_eqn_function.o ./heat_eqn_global.o ./heat_eqn_scalar.o ./heat_eqn_thread_vars.o
 -> �[32mCompiling�[0m mod_func.cpp
 -> �[32mNMODL�[0m test/nmodl/transpiler/usecases/longitudinal_diffusion/heat_eqn_array.mod
 -> �[32mNMODL�[0m test/nmodl/transpiler/usecases/longitudinal_diffusion/heat_eqn_function.mod
 -> �[32mNMODL�[0m test/nmodl/transpiler/usecases/longitudinal_diffusion/heat_eqn_global.mod
Translating heat_eqn_array.mod into build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/heat_eqn_array.cpp
Translating heat_eqn_function.mod into build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/heat_eqn_function.cpp
Thread Safe
Translating heat_eqn_global.mod into build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/heat_eqn_global.cpp
Thread Safe
Thread Safe
 -> �[32mNMODL�[0m test/nmodl/transpiler/usecases/longitudinal_diffusion/heat_eqn_scalar.mod
 -> �[32mNMODL�[0m test/nmodl/transpiler/usecases/longitudinal_diffusion/heat_eqn_thread_vars.mod
 -> �[32mCompiling�[0m build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/heat_eqn_array.cpp
Translating heat_eqn_scalar.mod into build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/heat_eqn_scalar.cpp
Thread Safe
Translating heat_eqn_thread_vars.mod into build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/heat_eqn_thread_vars.cpp
Thread Safe
 -> �[32mCompiling�[0m build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/heat_eqn_function.cpp
 -> �[32mCompiling�[0m build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/heat_eqn_global.cpp
 -> �[32mCompiling�[0m build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/heat_eqn_scalar.cpp
 -> �[32mCompiling�[0m build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/heat_eqn_thread_vars.cpp
 => �[32mLINKING�[0m shared library "build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/./libnrnmech.so"
 => �[32mLINKING�[0m executable "build/temp/test/nmodl/transpiler/usecases/longitudinal_diffusion/x86_64/./special" LDFLAGS are:     
Successfully created x86_64/special

Running tests:
test/nmodl/transpiler/usecases/longitudinal_diffusion/test/nmodl/transpiler/usecases/longitudinal_diffusion/test_heat_eqn.py: started.
Traceback (most recent call last):
  File "test/nmodl/transpiler/usecases/longitudinal_diffusion/test_heat_eqn.py", line 141, in <module>
    test_heat_equation_scalar()
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "test/nmodl/transpiler/usecases/longitudinal_diffusion/test_heat_eqn.py", line 119, in test_heat_equation_scalar
    check_heat_equation_scalar("heat_eqn_scalar")
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
  File "test/nmodl/transpiler/usecases/longitudinal_diffusion/test_heat_eqn.py", line 113, in check_heat_equation_scalar
    check_heat_equation(
    ~~~~~~~~~~~~~~~~~~~^
        mech_name, record_states_factory(1, lambda inst, k: inst._ref_X)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "test/nmodl/transpiler/usecases/longitudinal_diffusion/test_heat_eqn.py", line 96, in check_heat_equation
    plot_timeseries(mech_name, t, X, i_state)
    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "test/nmodl/transpiler/usecases/longitudinal_diffusion/test_heat_eqn.py", line 87, in plot_timeseries
    plt.legend()
    ~~~~~~~~~~^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/pyplot.py", line 3619, in legend
    return gca().legend(*args, **kwargs)
           ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/axes/_axes.py", line 337, in legend
    self.legend_ = mlegend.Legend(self, handles, labels, **kwargs)
                   ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/legend.py", line 549, in __init__
    self._init_legend_box(handles, labels, markerfirst)
    ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/legend.py", line 896, in _init_legend_box
    handle_list.append(handler.legend_artist(self, orig_handle,
                       ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
                                             fontsize, handlebox))
                                             ^^^^^^^^^^^^^^^^^^^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/legend_handler.py", line 129, in legend_artist
    artists = self.create_artists(legend, orig_handle,
                                  xdescent, ydescent, width, height,
                                  fontsize, handlebox.get_transform())
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/legend_handler.py", line 303, in create_artists
    self.update_prop(legline, orig_handle, legend)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/legend_handler.py", line 88, in update_prop
    self._update_prop(legend_handle, orig_handle)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/legend_handler.py", line 79, in _update_prop
    self._default_update_prop(legend_handle, orig_handle)
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/legend_handler.py", line 84, in _default_update_prop
    legend_handle.update_from(orig_handle)
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/lines.py", line 1358, in update_from
    self._marker = MarkerStyle(marker=other._marker)
                   ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/markers.py", line 248, in __init__
    self._set_marker(marker)
    ~~~~~~~~~~~~~~~~^^^^^^^^
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/markers.py", line 323, in _set_marker
    self.__dict__ = copy.deepcopy(marker.__dict__)
                    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/copy.py", line 131, in deepcopy
    y = copier(x, memo)
  File "/usr/lib/python3.14/copy.py", line 202, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ~~~~~~~~^^^^^^^^^^^^^
  File "/usr/lib/python3.14/copy.py", line 138, in deepcopy
    y = copier(memo)
  File "venv_3.14/lib/python3.14/site-packages/matplotlib/path.py", line 285, in __deepcopy__
    p = copy.deepcopy(super(), memo)
  File "/usr/lib/python3.14/copy.py", line 157, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "/usr/lib/python3.14/copy.py", line 234, in _reconstruct
    y = func(*args)
  File "/usr/lib/python3.14/copy.py", line 233, in <genexpr>
    args = (deepcopy(arg, memo) for arg in args)
            ~~~~~~~~^^^^^^^^^^^
  File "/usr/lib/python3.14/copy.py", line 138, in deepcopy
    y = copier(memo)
# thousands of the above __deepcopy__ later...
RecursionError: maximum recursion depth exceeded
<end of output>
Test time =   3.11 sec
----------------------------------------------------------
Test Failed.
"usecase_longitudinal_diffusion" end time: Sep 04 11:34 CEST
"usecase_longitudinal_diffusion" time elapsed: 00:00:03
----------------------------------------------------------

End testing: Sep 04 11:34 CEST

The first failure appears to have something to do with the refcounting, and should most likely be fixed on our end, while the second one is probably an instance of matplotlib/matplotlib#30198, which means we will need to update the version of matplotlib for Python 3.14 (note that the PR is not part of any release yet, as of Matplotlib 3.10.6).

EDIT: I was mistaken, it seems to be implemented in Matplotlib 3.10.5 (see matplotlib/matplotlib@22b8286).

EDIT: relevant PR which modified the Python refcounting test last: #3039

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions