# neat-python changed what a checkpoint file number means, and that is the 2.1 upgrade trap

> neat-python is a BSD-3-Clause pure Python implementation of NEAT, neuroevolution of augmenting topologies, with no dependencies beyond the standard library and an optional CuPy extra for GPU evaluation of CTRNN and Izhikevich networks. It suits researchers who need reproducible evolutionary runs and exportable network JSON, and version 2.1 moved checkpointing so that file N means generation N has been evaluated.

**CodeReclaimers/neat-python** — Python implementation of the NEAT neuroevolution algorithm

- Repository: https://github.com/CodeReclaimers/neat-python
- Stars: 1,585 · Forks: 507
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/codereclaimers-neat-python

## What NEAT actually evolves

NEAT is a method developed by Kenneth O. Stanley for evolving arbitrary neural networks, and the project is a pure-Python implementation of it with no dependencies beyond the standard library, forked from an earlier project by MattKallada.

The defining property of the method is that topology is part of the genome. Networks are not fixed graphs that you tune weights on; structures are mutated, with nodes added and connections added or pruned, and the population is divided into species so that structurally different lineages coexist. The README's 2.1 notes describe this in passing, mentioning pruning of dangling nodes after deletion mutations, connection gene matching by innovation number in the distance function with a separate excess_coefficient, and a dynamic compatibility threshold adjustment.

That last item, compatibility_threshold_adjustment, is the kind of detail that separates a faithful implementation from a simplified one. Species are formed by a compatibility distance between genomes, and a fixed threshold means a threshold that suits generation five and not generation five hundred.

Version 2.1 also adds configurable options to match Stanley and Miikkulainen (2002) more closely, while keeping backward-compatible defaults: canonical fitness sharing, proportional spawn allocation, interspecies crossover through interspecies_crossover_prob, and a fix to the 75% disable rule so that it replaces rather than layers on the inherited enabled value. Backward-compatible defaults mean an existing configuration file keeps its behaviour, so upgrading does not silently change your results.

For the underlying theory, the README points at Stanley's publications page rather than summarising it, which is the right division: this is an implementation, not a tutorial.

## fitness_criterion = min used to be half-implemented

This bug fix is worth its own section because it shows the class of mistake that hides in a library for years.

Before 2.1, setting fitness_criterion = min only honoured the setting in the termination check. Everything else hardcoded higher is better: best-genome tracking, stagnation detection, elite selection, crossover parent selection, spawn allocation and statistics reporting. The run would stop when it decided to, and everything before that point was optimising in the wrong direction.

The 2.1 release states that all fitness comparisons throughout the library now respect the configured criterion. That is the fix. If you run minimisation problems, meaning anything framed as a distance, an error or a cost, and you ran before 2.1, your results are suspect rather than merely imperfect, and the fix is worth the upgrade on its own.

The context for why it is a real option: CTRNN and recurrent experiments on tasks like the Lorenz system are naturally framed as minimising error, and the same code with a configuration flag serves both directions.

## Checkpoints moved from end_generation to post_evaluate

The second 2.1 change is the one that will bite you on upgrade, and the README describes it precisely.

Checkpoints used to be saved after reproduction, in end_generation. They are now saved after fitness evaluation, in post_evaluate. Restoring a checkpoint skips the already-completed evaluation and proceeds directly to reproduction. For an experiment with an expensive fitness function, the README says this eliminates potentially hours of redundant computation per restore.

The naming consequence is the part to memorise: checkpoint file N now means generation N has been evaluated. Old checkpoint files in the five-tuple format are still loadable, which is the compatibility promise, but the semantics differ, so a file number that used to mean one thing now means another.

Two related output fixes ship alongside. StdOutReporter previously printed the species detail table in end_generation using the post-reproduction population, which belongs to the next generation, so all output under the Running generation N banner is now internally consistent. And two double-buffer bugs in CTRNN.advance were fixed, where incorrect buffer swapping could cause state corruption during multi-step CTRNN evaluation. If you ran multi-step recurrent networks on 2.0, your state was possibly wrong and you had no way to see it.

## The 2.0 CTRNN change is a breaking API edit

The headline feature of 2.0 is per-node evolvable time constants for the continuous-time recurrent network.

In 1.x, all nodes shared a single fixed time constant passed at network creation time. In 2.0, each node carries its own time constant as an evolved gene attribute, which lets one network operate across multiple timescales simultaneously. The README motivates this with the Lorenz example under examples/lorenz-ctrnn, where different variables change on different timescales and a shared constant is a limitation imposed by the implementation rather than by the problem.

The cost is a breaking API change. CTRNN.create(genome, config, time_constant) is now CTRNN.create(genome, config). Existing feedforward and discrete-time recurrent configurations need no changes, so only CTRNN users are affected, and they have to update one call site.

Two further changes come with it. The CTRNN integration switched from forward Euler to exponential Euler (ETD1) for improved numerical stability, which changes the numerical results of existing CTRNN experiments without changing any code. And GPU evaluation for CTRNN and Izhikevich networks is available through an optional extra, pip install 'neat-python[gpu]', using CuPy with lazy imports so that import neat never triggers a GPU dependency. That last detail matters: a lazy import keeps the extra genuinely optional for the users who are not using it.

## Exporting a trained network as JSON

The export path is the most practically useful feature for a researcher, because it decouples the artifact from the library.

Networks export to a JSON format that is framework-agnostic and human-readable, through neat.export.export_network_json:

```python
import neat
from neat.export import export_network_json

# After training...
winner_net = neat.nn.FeedForwardNetwork.create(winner, config)

export_network_json(
    winner_net,
    filepath='my_network.json',
    metadata={'fitness': winner.fitness, 'generation': 42}
)
```

The README lists four reasons this is worth doing: converting to other formats such as ONNX, TensorFlow or PyTorch with third-party tools, inspecting and debugging network structure, sharing networks across platforms and languages, and archiving trained networks independently of neat-python. The beginnings of a conversion system live in the examples/export directory, and docs/network-json-format.md holds the complete format documentation with guidance for writing converters to other frameworks.

The metadata argument is the part to use. Fitness and generation travel with the file, so an archived network carries the provenance of the run that produced it without a separate log.

## Reproducibility, parallelism and the packaging

Reproducible evolution is listed as a feature: set random seeds for deterministic, repeatable experiments. Combined with the checkpoint change and the JSON export, that gives you a defensible experimental record rather than a number in a notebook.

Parallel fitness evaluation using multiprocessing is also a listed feature, and examples/parallel-reproducible exists as an example, which suggests the interaction between seeding and parallelism is handled rather than left to chance. That combination is exactly where most evolutionary libraries go wrong.

Packaging is in pyproject.toml with setuptools 61 or later as the build backend. The project requires Python 3.8 or newer and classifies support for 3.8 through 3.14 and PyPy, matching the README's statement that it is supported on Python 3.8 through 3.14 and pypy3. Author and maintainer metadata name four people, with Alan McIntyre listed as both an author and the maintainer.

The repository layout separates concerns cleanly: neat/ for the library, tests/ for the suite, examples/ for runnable experiments, docs/ for the JSON format documentation, benchmarks/ for performance work, and AGENTS.md and CITATION.cff at the root for automated agents and citation metadata. The README asks contributors to submit an issue if they want their name added to the citation, since it lists the originators and maintainers of all iterations of the project.

Documentation is hosted on Read the Docs, and the README also points at an experimental support agent, which is unusual enough to mention.

## Where this is the wrong tool

Four boundaries.

If your problem is gradient descent, this is the wrong family. NEAT evolves genomes without gradients, which is its strength on small networks and non-differentiable objectives and its weakness when you have millions of parameters and a differentiable loss.

If you need speed, pure Python is the constraint. The GPU extra covers only CTRNN and Izhikevich evaluation through CuPy, and the fitness function itself, which is where most NEAT compute goes, is yours to parallelise or vectorise. The benchmarks/ directory is where the authors measure this.

If you need raw stability guarantees on recurrent dynamics, note that the 2.1 double-buffer bug in CTRNN.advance existed in 2.0, and the switch to exponential Euler changed numerics. If your published results depend on a specific integrator, reproduce them on 2.1 before citing them.

If you want a batteries-included framework, there is none. There is no tensor library, no autodiff and no training loop; there is an algorithm implementation, and you write the environment and the fitness function.

On licence and citation: the project is BSD-3-Clause, a permissive licence with the three-clause notice and disclaimer obligations. For publication the README asks you to cite both the software, with a Zenodo DOI listed, and the original NEAT paper. Releases v1.1.0 in December 2025, v2.0.0 on 2026-03-02 and v2.0.1 on 2026-03-14 show an active line, with the last push on 2026-05-23. Note that the README describes what is new in 2.1 while the latest tag is v2.0.1 and pyproject reads 2.1.0, so the released artefacts and the documentation are not quite aligned.

## How it differs from a genetic algorithm library like DEAP

The comparison that matters is with a general purpose evolutionary library such as DEAP. The difference in approach is scope and default.

neat-python provides a pure-Python implementation of NEAT, including species, compatibility distance, innovation-number matching for connection genes, and the 75% disable rule. If you want to run NEAT, using this saves you from reimplementing those parts.

The JSON export is what makes either comparison survivable. Because a trained network leaves the library as a plain file, the choice of evolutionary library does not constrain the framework you deploy the result in, which is the point of the export feature.

## Conclusion

Adopt neat-python if you are running neuroevolution experiments in Python and want a dependency-free implementation you can reproduce, checkpoint and inspect, since the library ships 618 unit tests, seedable randomness and multiprocessing fitness evaluation without a framework in the way. Do not adopt it for gradient-trained networks, and note that the 2.0 CTRNN change is a breaking API change where CTRNN.create(genome, config, time_constant) became CTRNN.create(genome, config), so per-node evolvable time constants cost you that argument. Verify first whether your saved checkpoints predate 2.1: old five-tuple checkpoint files are still loadable, but the meaning of the file number changed, and reading a 2.0 checkpoint with 2.1 code means generation N is evaluated rather than reproduced.

## FAQ

### What changed in neat-python 2.1?

fitness_criterion = min is now honoured throughout the library rather than only in the termination check, checkpoints are saved after fitness evaluation in post_evaluate instead of after reproduction in end_generation, and StdOutReporter output no longer mixes generation boundaries. Two double-buffer bugs in CTRNN.advance were also fixed.

### How do I resume a neat-python run from a checkpoint?

Restoring a checkpoint now skips the already-completed fitness evaluation and proceeds directly to reproduction, and checkpoint file N means generation N has been evaluated. Checkpoint files in the old five-tuple format are still loadable.

### What broke in neat-python 2.0 for CTRNN users?

CTRNN.create(genome, config, time_constant) became CTRNN.create(genome, config), because each node now carries its own evolvable time constant as a gene attribute instead of sharing one fixed constant. Feedforward and discrete-time recurrent configurations need no changes.

### Does neat-python need any third-party packages?

The core library is pure Python with no dependencies beyond the standard library. GPU-accelerated evaluation for CTRNN and Izhikevich networks is an optional extra installed with pip install 'neat-python[gpu]', which uses CuPy through lazy imports so that import neat never pulls in a GPU dependency.

### How do I export a trained neat-python network?

Use export_network_json from neat.export, passing the created network, a filepath and optional metadata such as fitness and generation. The output is framework-agnostic JSON, with the format documented in docs/network-json-format.md and early conversion examples in the examples/export directory.

## Sources

- [CodeReclaimers/neat-python on GitHub](https://github.com/CodeReclaimers/neat-python)
- [Issues](https://github.com/CodeReclaimers/neat-python/issues)
- [License: BSD-3-Clause](https://github.com/CodeReclaimers/neat-python/blob/master/LICENSE)
- [README](https://github.com/CodeReclaimers/neat-python/blob/master/README.md)
- [Releases](https://github.com/CodeReclaimers/neat-python/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/codereclaimers-neat-python
