# chempy: physical chemistry calculations in pure Python

> chempy is a Python package for physical, inorganic and analytical chemistry that covers chemical kinetics, equilibria and a set of textbook relations, with pure Python implementations of material properties from the literature. Its distinguishing feature is that the numerics sit behind interchangeable backends, so the same model can run on SciPy or on compiled ODE solvers.

**bjodah/chempy** — ⚗ A package useful for chemistry written in Python

- Repository: https://github.com/bjodah/chempy
- Stars: 659 · Forks: 87
- Language: Python
- License: BSD-2-Clause
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/bjodah-chempy

## What the package actually covers, and what it leaves out

chempy is described as a Python package useful for chemistry, mainly physical, inorganic and analytical chemistry. The README enumerates the current contents in a list, and the list is more useful for deciding fit than the marketing phrase.

For kinetics there are numerical integration routines, described as an ODE solver front-end, and integrated rate expressions with convenience fitting routines. For equilibria there is a solver that handles multiphase systems. Then there is a group of physical chemistry relations, namely Debye-Hückel expressions, the Arrhenius and Eyring equations, and the Einstein-Smoluchowski equation.

The last group is the most distinctive. Under properties, the package provides pure Python implementations taken from the literature: water density as a function of temperature, water permittivity as a function of temperature and pressure, water diffusivity and water viscosity as functions of temperature, and sulfuric acid density as a function of temperature and weight fraction of H2SO4. The README links each of these to its own module file, and the properties directory is explicitly marked as open for contributions.

What is not in the list is as informative. There is no general property database, no phase equilibrium using a thermodynamic model, and no uncertainty framework. The package is a set of correlations and solvers for kinetics and equilibria, not a certified thermodynamic suite.

## conda on Linux, pip everywhere else

The installation instructions differ by platform, and the reason is stated plainly: conda packages are only provided for Linux, so on Windows and OS X you need pip instead.

The conda route installs the package along with pytest and runs the test suite in one step:

```bash
   $ conda install -c conda-forge chempy pytest
   $ pytest -rs -W ignore::chempy.ChemPyDeprecationWarning --pyargs chempy
```

The pip equivalent for the other platforms is:

```bash
   $ python3 -m pip install chempy pytest
   $ python3 -m pytest -rs -W ignore::chempy.ChemPyDeprecationWarning --pyargs chempy
```

Running the tests after install is not ceremony. The README warns that a few tests will be skipped because optional backends are missing, in addition to those in SciPy, which is used for solving systems of non-linear equations and ordinary differential equations. So the test output tells you directly which parts of the numeric stack are present on your machine.

The pip section also notes that the `--user` flag can be skipped if you have root permissions, and it points to a Dockerfile in `scripts/environment/` if you want to see which system packages a Debian based system needs.

## The optional backends are the real architecture decision

The `chempy[all]` extra installs a set of optional solver backends, and reading the list explains how the package is designed.

`pygslodeiv2` solves initial value problems and requires GSL, version 1.16 or newer. `pyodeint` solves initial value problems and requires boost, version 1.65.0 or newer. `pycvodes` solves initial value problems and requires SUNDIALS, version 5.3.0 or newer. `pykinsol` does non-linear root-finding and also requires SUNDIALS. `pycompilation` is a Python front-end for calling compilers and requires gcc, clang or icpc, and `pycodeexport` is a code generation package used when generating C++ code.

So there are two categories. Three of them are alternative ODE solvers wrapping compiled numerical libraries, and they are what let a kinetics model run on something faster than a pure Python integrator. The last two are about generating and compiling code, which is the path from a model to a standalone executable.

The install command is short:

```bash
   $ python3 -m pip install chempy[all]
```

The README notes that some of these libraries require additional libraries to be present on your system, which is why the Debian Dockerfile exists. This is the point where a chempy user has to make a real choice. A researcher who wants to fit rate expressions to experimental data does not need SUNDIALS, and pulling it in adds a compiled dependency stack that has to be present and compatible. The default SciPy path needs nothing beyond the Python scientific stack.

## A Docker helper runs notebooks and the test suite from the same image

If Docker is installed, the repository ships a script for hosting a Jupyter notebook server from the project's own environment, and for running the test suite in that same environment.

The notebook server command is:

```bash
  $ ./scripts/host-env.sh host-notebook --port 8888
```

The first run downloads dependencies, and once the installation completes there is a link you can open in your browser. The test suite uses the same image:

```bash
  $ ./scripts/host-env.sh run-tests
```

The README is candid that this path produces a few skipped tests because some dependencies are not installed by default, and quite a few warnings. That is consistent with the earlier note about optional backends, and it is a reasonable trade for a reproducible environment: the set of packages present is fixed by the Dockerfile rather than by whatever happened to be on the machine.

The examples directory is where this environment earns its keep. It contains notebooks covering ammonia, sodium chloride precipitation, Robertson kinetics, carbon dioxide equilibrium, FeSCN equilibrium, Arrhenius and Eyring kinetics in several forms including second order and reversible cases, temperature-ramped kinetics, integrated rate expressions, kinetic model fitting, regression, surface reactions, and switching between reduced ODE systems. There is also a `scripts/` directory with a Python progress bar script, and the repository carries a `.jupyter/` directory and an `index.ipynb` at the top level.

## Composition is keyed by atomic number, with zero reserved for charge

The formula parsing example is worth reading closely because it reveals a design decision that affects how you write downstream code.

A substance is built from a formula string, and the resulting `composition` attribute is a dictionary keyed by atomic number, with 0 used for the charge:

```python
   >>> from chempy import Substance
   >>> ferricyanide = Substance.from_formula('Fe(CN)6-3')
   >>> ferricyanide.composition == {0: -3, 26: 1, 6: 6, 7: 6}  # 0 for charge
   True
```

So iron is 26, carbon is 6, nitrogen is 7, and the entry with key 0 carries the -3 charge. The README explains that in composition the atomic numbers, with 0 for charge, are used as keys and the count of each kind becomes the respective value.

The same object carries several renderings of the formula, which is the practical reason to use it at all:

```python
   >>> print(ferricyanide.unicode_name)
   Fe(CN)₆³⁻
   >>> print(ferricyanide.latex_name + ", " + ferricyanide.html_name)
   Fe(CN)_{6}^{3-}, Fe(CN)<sub>6</sub><sup>3-</sup>
   >>> print('%.3f' % ferricyanide.mass)
   211.955
```

The mass comes out at 211.955 for the ferricyanide ion, and the name properties give you Unicode subscripts and superscripts for terminal output, a LaTeX form for a paper, and an HTML form with sub and sup tags for a web page.

The trade-off in keying by atomic number is that composition is compact and machine-friendly, but it is not directly human-readable. If you are doing bookkeeping over many species, you will end up maintaining a number-to-symbol mapping yourself.

## Stoichiometry balancing handles the kind of reaction textbooks use

The second example shows reaction balancing, and the README picks a reaction that gives the scale away: the main reaction in NASA's booster rockets.

```python
   >>> from chempy import balance_stoichiometry  # Main reaction in NASA's booster rockets:
   >>> reac, prod = balance_stoichiometry({'NH4ClO4', 'Al'}, {'Al2O3', 'HCl', 'H2O', 'N2'})
   >>> from pprint import pprint
   >>> pprint(dict(reac))
   {'Al': 10, 'NH4ClO4': 6}
   >>> pprint(dict(prod))
   {'Al2O3': 5, 'H2O': 9, 'HCl': 6, 'N2': 3}
```

The interface takes two sets, reactants and products, and returns the two sets with integer stoichiometric coefficients attached. The inputs are formula strings, and the output is the same formulas keyed to their coefficients.

Two observations. The coefficients come out as the smallest whole numbers, so the result is the conventional balanced equation rather than a scaled one. And the input is unordered sets, which means the algorithm does the algebra over the set rather than matching a template, so a reaction with a species appearing on both sides would be handled as a set operation rather than a list comparison.

The same module also provides `mass_fractions`, which the README reaches for immediately after balancing, since knowing the mass share of each species in a reactant mixture is usually the next thing you want.

## Versioning is handled in setup.py, with a pinned setuptools and git describe

The packaging is worth a look if you are going to fork or vendor the project, because it is unusual in a revealing way.

`pyproject.toml` declares the project metadata as dynamic, with version, optional dependencies, classifiers, description, readme, requires-python, authors, keywords and dependencies all resolved at build time, and the licence given as BSD-2-Clause. The build system requires `setuptools<=72.1.0` and `wheel`, and the comment next to the setuptools pin points at a specific setuptools issue.

`setup.py` then does the work. It reads the release version from an environment variable named after the package, `chempy_RELEASE_VERSION`, and if that is present it strips a leading v and treats the build as a tagged release. If it is absent, the script executes the `chempy/_release.py` file to read the `__version__` attribute, and if that version string ends with git, it shells out to `git describe --dirty` to derive a version, rewriting the tag into a post-release or development form depending on whether the build is happening inside a conda build.

That is a lot of machinery for a version number, and it explains a behavior you will notice in practice. A git archive install, with no git metadata, produces a warning that version information is incomplete, and a development branch install warns that using git to derive the version may compete with dev branches.

Around that there is a proper research software layout: a `CHANGES.rst` changelog, `CITATION.cff` for citation metadata, a `joss-paper/` directory and a DOI badge for a Journal of Open Source Software paper, an `AUTHORS` file, `CONTRIBUTING.rst`, a `benchmarks/` directory, and a `pyproject.toml` alongside a `setup.py` for the parts setuptools still has to resolve.

The licence is BSD-2-Clause, stated in both the packaging metadata and the LICENSE file. The repository is not archived and the last push was on 2026-10-20, with v0.10.2 released on 2026-09-19.

## Compared with writing the solver yourself on SciPy

The alternative is to build the kinetics model directly on SciPy, and the difference in approach is about how much of the chemistry you write yourself.

A direct approach means writing the ODE system yourself, describing the rate laws as functions of the state vector, passing them to `scipy.integrate.solve_ivp`, and then writing the parameter fitting yourself around a least squares call. For a single reaction with a known rate law, that is a page of code and no dependency questions. The same code then has to be reworked for a multiphase equilibrium problem or for a set of coupled Arrhenius expressions, and the unit handling, which is a constant source of errors in this domain, has to be built from scratch each time.

chempy's approach is to supply that scaffolding. The ODE solver front-end, the integrated rate expressions, the convenience fitting routines, the equilibrium solver including multiphase systems, and the Arrhenius, Eyring, Debye-Hückel and Einstein-Smoluchowski relations are all present, with the unit handling left to the package rather than reimplemented per project.

The reason to accept that scaffolding is the backend switching. Because the model is described once and the numerical integration is delegated, the same model can be solved with SciPy's integrator, with `pyodeint` over boost, with `pycvodes` over SUNDIALS, or with `pygslodeiv2` over GSL. For a study where the same model has to run many times over long integration horizons, that is a real option rather than a theoretical one, and `pycodeexport` and `pycompilation` extend the idea to generating a standalone program.

The reason to decline it is scope. chempy is a kinetics and equilibria toolkit, and if what you need is a validated thermodynamic property database with uncertainty bounds, a dedicated thermochemistry package serves that better, because chempy's property implementations are correlations from named literature sources rather than a maintained data set.

## Conclusion

Adopt chempy when your work in Python needs chemical kinetics, equilibrium calculations or material property values from the literature, and you want one model description that can run on SciPy or on a compiled solver backend. Do not adopt it if you need a validated thermodynamics suite with uncertainty propagation, since the package positions itself around kinetics, equilibria and correlations rather than a certified property database. Verify first which optional backends you install, because the default SciPy path is slower and the extra solvers need GSL, boost or SUNDIALS present on your system.

## FAQ

### How do I install chempy?

On Linux the README recommends conda with `conda install -c conda-forge chempy pytest`. On Windows and OS X, where conda packages are not provided, use `python3 -m pip install chempy pytest`. The extras `chempy[all]` add the optional compiled solver backends.

### What optional backends does chempy support for solving ODE systems?

The `chempy[all]` extra installs pygslodeiv2 for initial value problems requiring GSL, pyodeint requiring boost, pycvodes requiring SUNDIALS, pykinsol for non-linear root-finding requiring SUNDIALS, plus pycompilation and pycodeexport for compiling and generating code. The default path uses SciPy.

### How does chempy represent the composition of a chemical formula?

The composition is a dictionary keyed by atomic number, with 0 reserved for the charge, and the count of each element as the value. For `Fe(CN)6-3` the README shows the composition as `{0: -3, 26: 1, 6: 6, 7: 6}`.

### What is the license of chempy?

chempy is BSD-2-Clause, declared both in the pyproject.toml metadata as `license = {'text' = "BSD-2-Clause"}` and in the setup.py variable, with the full text in the LICENSE file at the repository root.

### How do I balance a chemical reaction with chempy?

Call `balance_stoichiometry` with a set of reactant formulas and a set of product formulas, and it returns the two sets with integer coefficients. The README uses the booster rocket reaction `{'NH4ClO4', 'Al'}` and `{'Al2O3', 'HCl', 'H2O', 'N2'}` as its example.

## Sources

- [bjodah/chempy on GitHub](https://github.com/bjodah/chempy)
- [Issues](https://github.com/bjodah/chempy/issues)
- [License: BSD-2-Clause](https://github.com/bjodah/chempy/blob/master/LICENSE)
- [README](https://github.com/bjodah/chempy/blob/master/README.md)
- [Releases](https://github.com/bjodah/chempy/releases)

---

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