Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ignore HTML files
*.html linguist-vendored
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.10", "3.12"]
python-version: ["3.10", "3.12", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Changelog

## 1.5.1 (2025-11-01)

### New Features
- Add `+` operator to combine two ChemFormula instances by summing element counts and charges (see [example6.py](https://github.com/molshape/ChemFormula/tree/main/examples/example6.py))
- Add `-` operator to subtract one ChemFormula instance from another one by subtracting element counts and charges (see [example6.py](https://github.com/molshape/ChemFormula/tree/main/examples/example6.py))
- Add `*` operator to multiply element counts and charge with a positive integer from left and right

### Example
```python
from chemformula import ChemFormula

water = ChemFormula("H2O")
proton = ChemFormula("H", 1)

oxonium = water + proton # => ChemFormula("H3O", 1)
hydroxonium = oxonium + 3 * water # => ChemFormula("H9O4", 1)

print(oxonium.hill_formula.unicode) # => H₃O⁺
print(oxonium - proton == water) # True
print(hydroxonium.hill_formula.unicode) # => H₉O₄⁺
```

---

## 1.5.0 (2025-09-14)

### New Feature
- Add support for hydrogen isotopes (deuterium "D" and tritium "T") via a global `AllowHydrogenIsotopes` flag in `chemformula.config` (see [example5.py](https://github.com/molshape/ChemFormula/tree/main/examples/example5.py))
- Implement `.contains_isotopes` attribute to the `ChemFormula` class for detecting specific isotopes in formulas

### Deprecated Feature
- Replace `.radioactive` property with `.is_radioactive`
- `.radioactive` can still be used, but is flagged as deprecated and emits a `DeprecationWarning`

### Example
```python
import chemformula.config
from chemformula import ChemFormula

chemformula.config.AllowHydrogenIsotopes = True

heavy_water = ChemFormula("D2O")
print(f"{heavy_water.formula_weight:.2f} g/mol") # => 20.03 g/mol

super_heavy_water = ChemFormula("T2O")
print(super_heavy_water.is_radioactive) # => True
```
94 changes: 61 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,43 @@
![License](https://img.shields.io/github/license/molshape/ChemFormula) \
![GitHub stars](https://img.shields.io/github/stars/molshape/ChemFormula)

---

<details>
<summary>Table of Contents</summary>
## Table of Contents

1. [Description](#description)
2. [How to install and uninstall?](#how-to-install-and-uninstall)
3. [Dependencies](#dependencies)
4. [How to use?](#how-to-use)
5. [Examples](#examples)
6. [Comparing and Sorting](#comparing-and-sorting-of-chemical-formulas)
7. [Atomic Weight Data](#atomic-weight-data)

</details>
5. [Examples and Formula Formatting](#examples-and-formula-formatting)
6. [Formula Arithmetics (Addition, Subtraction, Multiplication)](#arithmetics-with-chemical-formulas-addition-subtraction-multiplication)
7. [Comparing and Sorting](#comparing-and-sorting-of-chemical-formulas)
8. [Atomic Weight Data](#atomic-weight-data)

---

## Description
**ChemFormula** is a Python class for working with chemical formulas. It allows parsing chemical formulas and generating predefined (LaTeX, HTML) or customized formatted output strings, e. g. <span>[Cu(NH<sub>3</sub>)<sub>4</sub>]SO<sub>4</sub>&sdot;H<sub>2</sub>O</span>. **ChemFormula** is also calculating the formula weight and thus enabling stoichiometric calculations with chemical formula objects. Atomic weights are based on IUPAC recommendations (see [Atomic Weight Data](#atomic-weight-data)).
**ChemFormula** is a Python library for working with chemical formulas. It allows parsing chemical formulas and generating predefined (LaTeX, HTML) or customized formatted output strings, e.g. <span>[Cu(NH<sub>3</sub>)<sub>4</sub>]SO<sub>4</sub>&sdot;H<sub>2</sub>O</span>. **ChemFormula** also calculates formula weights and weight distributions and enables stoichiometric calculations with chemical formula objects. Arithmetic operations (`+`, `-`, `*`) between formula objects are supported for combining and modifying chemical compositions. Atomic weights are based on IUPAC recommendations (see [Atomic Weight Data](#atomic-weight-data)).

---

## How to install and uninstall
**ChemFormula** can be installed from the [Python Package Index (PyPI)](https://pypi.org/) repository by calling

pip install chemformula

or

uv add chemformula
**ChemFormula** can be installed from the [Python Package Index (PyPI)](https://pypi.org/) repository by calling `pip install chemformula` or `uv add chemformula`.

In order to uninstall **ChemFormula** from your local environment use
In order to uninstall **ChemFormula** from your local environment use `pip uninstall chemformula` or `uv remove chemformula`.

pip uninstall chemformula

or

uv remove chemformula
---

## Dependencies
**ChemFormula** uses the [casregnum package](https://pypi.org/project/casregnum/) to manage CAS Registry Numbers®. The corresponding properties of the `CAS` class are therefore inherited to the ```ChemFormula``` class.
**ChemFormula** uses the [casregnum package](https://pypi.org/project/casregnum/) to manage CAS Registry Numbers®. The corresponding properties of the `CAS` class are therefore inherited to the `ChemFormula` class.


---

## How to use
**ChemFormula** provides the `ChemFormula` class for creating a chemical formula object:

```Python
```python
from chemformula import ChemFormula

chemical_formula = ChemFormula(formula,
Expand All @@ -60,7 +54,7 @@ chemical_formula = ChemFormula(formula,

*Examples:*

```Python
```python
ethylcinnamate = ChemFormula("(C6H5)CHCHCOOC2H5")
tetraamminecoppersulfate = ChemFormula("[Cu(NH3)4]SO4.H2O")
uranophane = ChemFormula("Ca(UO2)2(SiO3OH)2.(H2O)5")
Expand All @@ -74,7 +68,7 @@ theine = ChemFormula("(C5N4H)O2(CH3)3", name = "theine", cas = "58-08-2")

The `ChemFormula` class offers the following attributes/functions

```Python
```python
.formula # original chemical formula used to create the chemical formula object

.text_formula # formula including charge as text output
Expand Down Expand Up @@ -133,11 +127,12 @@ The `ChemFormula` class offers the following attributes/functions
.cas.check_digit # CAS number check digit, inherited property from casregnum.CAS
```

---

## Examples
## Examples and Formula Formatting
The following python sample script

```Python
```python
from chemformula import ChemFormula

tetraamminecoppersulfate = ChemFormula("[Cu(NH3)4]SO4.H2O")
Expand Down Expand Up @@ -170,8 +165,8 @@ print(f" {mole:.1f} mol of {ethylcinnamate.name} weighs {mole * ethylcinnamate.f
mass = 24
print(f" {mass:.1f} g of {ethylcinnamate.name} corresponds to {mass/ethylcinnamate.formula_weight * 1000:.1f} mmol.")
print(f" The elemental composition of {ethylcinnamate.name} is as follows:")
for stringElementSymbol, floatElementFraction in ethylcinnamate.mass_fraction.items():
print(f" {stringElementSymbol:<2}: {floatElementFraction * 100:>5.2f} %")
for stringElementSymbol, floatElementFraction in ethylcinnamate.mass_fractions.items():
print(f" {stringElementSymbol:<2}: {floatElementFraction * 100:>5.2f} %")

print(f"\n--- {uranophane.name} and {muscarine.name} ---")
print(f" Yes, {uranophane.name} is radioactive.") if uranophane.is_radioactive else print(f" No, {uranophane.name} is not radioactive.")
Expand Down Expand Up @@ -205,7 +200,7 @@ generates the following output

--- Formula Weights Calculations with Ethyl Cinnamate ---
The formula weight of ethyl cinnamate (C₁₁H₁₂O₂) is 176.21 g/mol.
1.4 mol of ethyl cinnamate weight 246.7 g.
1.4 mol of ethyl cinnamate weighs 246.7 g.
24.0 g of ethyl cinnamate corresponds to 136.2 mmol.
The elemental composition of ethyl cinnamate is as follows:
C : 74.98 %
Expand All @@ -224,8 +219,36 @@ generates the following output
--- CAS Registry Number ---
Caffeine has the CAS RN 58-08-2 (or as an integer: 58082).

More examples can be found at [/examples/](https://github.com/molshape/ChemFormula/blob/main/examples/).
More examples can be found in the folder [examples/](https://github.com/molshape/ChemFormula/tree/main/examples/).

---

## Arithmetics with Chemical Formulas (Addition, Subtraction, Multiplication)

`ChemFormula` instances can be added and subtracted with each other and can be multiplied with a positive integer factor to create a new `ChemFormula` instance by summing, subtracting or multiplying element counts and charges:

```python
ATP = ChemFormula("C10H12N5O13P3", -4) # Adenosine triphosphate
water = ChemFormula("H2O")
dihydrogen_phosphate = ChemFormula("H2PO4", -1)

AMP = ATP + 2 * water - 2 * dihydrogen_phosphate # Adenosine monophosphate

print("\n--- Arithmetics with ChemFormula Objects ---")
print(f" ATP ({ATP.hill_formula.unicode}) hydrolyzes with two water molecules"
f" to AMP ({AMP.hill_formula.unicode}) and two inorganic phosphates ({dihydrogen_phosphate.unicode})\n"
f" releasing energy for cellular processes.\n")
```

creates the following output:

--- Arithmetics with ChemFormula Objects ---
ATP (C₁₀H₁₂N₅O₁₃P₃⁴⁻) hydrolyzes with two water molecules to AMP (C₁₀H₁₂N₅O₇P²⁻) and two inorganic phosphates (H₂PO₄⁻)
releasing energy for cellular processes.

[example6.py](https://github.com/molshape/ChemFormula/tree/main/examples/example6.py) shows more examples for formula arithmetics.

---

## Comparing and Sorting of Chemical Formulas

Expand Down Expand Up @@ -271,6 +294,9 @@ generates the following output
6. C₆H₁₂O₆
7. C₆H₁₂S₆

[example4.py](https://github.com/molshape/ChemFormula/tree/main/examples/example4.py) provides detailed examples for sorting and comparing `ChemFormula` instances.

---

## Using Isotopes like Deuterium or Tritium

Expand Down Expand Up @@ -304,7 +330,7 @@ creates the following output:
No, H₂O contains no specific isotopes.
Yes, D₂O contains specific isotopes.


---

## Atomic Weight Data

Expand All @@ -326,4 +352,6 @@ Quoted atomic weights are those suggested for materials where the origin of the

Data for hydrogen isotopes are taken from the **AME2020 Atomic Mass Evaluation** by Meng Wang *et al.*:

- [Chinese Phys. C, 2021, (45), 030003](https://doi.org/10.1088/1674-1137/abddaf)
- [*Chinese Phys. C*, **2021**, *45*(3), 030003](https://doi.org/10.1088/1674-1137/abddaf)

---
4 changes: 2 additions & 2 deletions examples/example1.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
print(f" {stringElementSymbol:<2}: {floatElementFraction * 100:>5.2f} %")

print("\n--- Is Uranophane Radioactive and Charged? ---")
print(f" Yes, {uranophane.name} is radioactive.") if uranophane.is_radioactive else print(f" No, {uranophane.name} is not radioactive.") # noqa: E501
print(f" Yes, {uranophane.name} is radioactive.") if uranophane.is_radioactive else print(f" No, {uranophane.name} is not radioactive.")
print(f" Yes, {uranophane.name} is charged.") if uranophane.charged else print(f" No, {uranophane.name} is not charged.")

print("\n--- Accessing Single Elements through FormulaObject.Element['Element_Symbol'] ---")
Expand All @@ -39,7 +39,7 @@
# Original: [Cu(NH3)4]SO4.H2O
# Charged: False
# Charge (int): 0
# LaTeX: \[\textnormal{Cu}\(\textnormal{N}\textnormal{H}_{3}\)_{4}\]\textnormal{S}\textnormal{O}_{4}\cdot\textnormal{H}_{2}\textnormal{O} # noqa: E501
# LaTeX: \[\textnormal{Cu}\(\textnormal{N}\textnormal{H}_{3}\)_{4}\]\textnormal{S}\textnormal{O}_{4}\cdot\textnormal{H}_{2}\textnormal{O}
# HTML: <span class='ChemFormula'>[Cu(NH<sub>3</sub>)<sub>4</sub>]SO<sub>4</sub>&sdot;H<sub>2</sub>O</span>
# Custom format: --> [Cu(NH_<3>)_<4>]SO_<4> * H_<2>O <--
# Sum formula: CuN4H14SO5
Expand Down
10 changes: 5 additions & 5 deletions examples/example2.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
print(f" LaTeX: {muscarine.latex}")
print(f" HTML: {muscarine.html}")
print(f" Unicode: {muscarine.unicode}")
print(f" Custom format: {muscarine.format_formula('--> ', '', '', '_<', '>', ' <--', '', '', ' * ', '^^', '^^', '(+)', '(-)')}") # noqa: E501
print(f" Custom format: {muscarine.format_formula('--> ', '', '', '_<', '>', ' <--', '', '', ' * ', '^^', '^^', '(+)', '(-)')}")
print(f" Sum formula: {muscarine.sum_formula}")
print(f" Hill formula: {muscarine.hill_formula}")

Expand All @@ -25,7 +25,7 @@
print(f" LaTeX: {pyrophosphate.latex}")
print(f" HTML: {pyrophosphate.html}")
print(f" Unicode: {pyrophosphate.unicode}")
print(f" Custom format: {pyrophosphate.format_formula('--> ', '', '', '_<', '>', ' <--', '', '', ' * ', '^^', '^^', '(+)', '(-)')}") # noqa: E501
print(f" Custom format: {pyrophosphate.format_formula('--> ', '', '', '_<', '>', ' <--', '', '', ' * ', '^^', '^^', '(+)', '(-)')}")
print(f" Sum formula: {pyrophosphate.sum_formula}")
print(f" Hill formula: {pyrophosphate.hill_formula}")

Expand All @@ -40,7 +40,7 @@
print(f" {stringElementSymbol:<2}: {floatElementFraction * 100:>5.2f} %")

print("\n--- Is L-(+)-Muscarine Radioactive and Charged? ---")
print(f" Yes, {muscarine.name} is radioactive.") if muscarine.is_radioactive else print(f" No, {muscarine.name} is not radioactive.") # noqa: E501
print(f" Yes, {muscarine.name} is radioactive.") if muscarine.is_radioactive else print(f" No, {muscarine.name} is not radioactive.")
print(f" Yes, {muscarine.name} is charged.") if muscarine.charged else print(f" No, {muscarine.name} is not charged.")

print("\n--- Accessing Single Elements through FormulaObject.Element['Element_Symbol'] ---")
Expand All @@ -54,8 +54,8 @@
# Charged: True
# Charge (int): 1
# Charge (str): +
# LaTeX: \(\(\textnormal{C}\textnormal{H}_{3}\)_{3}\textnormal{N}\)\(\textnormal{C}_{6}\textnormal{H}_{11}\textnormal{O}_{2}\)^{+} # noqa: E501
# HTML: <span class='ChemFormula'>((CH<sub>3</sub>)<sub>3</sub>N)(C<sub>6</sub>H<sub>11</sub>O<sub>2</sub>)<sup>+</sup></span> # noqa: E501
# LaTeX: \(\(\textnormal{C}\textnormal{H}_{3}\)_{3}\textnormal{N}\)\(\textnormal{C}_{6}\textnormal{H}_{11}\textnormal{O}_{2}\)^{+}
# HTML: <span class='ChemFormula'>((CH<sub>3</sub>)<sub>3</sub>N)(C<sub>6</sub>H<sub>11</sub>O<sub>2</sub>)<sup>+</sup></span>
# Unicode: ((CH₃)₃N)(C₆H₁₁O₂)⁺
# Custom format: --> ((CH_<3>)_<3>N)(C_<6>H_<11>O_<2>)^^+^^ <--
# Sum formula: C9H20NO2
Expand Down
4 changes: 2 additions & 2 deletions examples/example5.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
heavy_water = ChemFormula("D2O")

print("\n--- Isotopes in ChemFormula Objects ---")
print(f" Yes, {water.unicode} contains specific isotopes.") if water.contains_isotopes else print(f" No, {water.unicode} contains no specific isotopes.") # noqa: E501
print(f" Yes, {heavy_water.unicode} contains specific isotopes.\n") if heavy_water.contains_isotopes else print(f" No, {heavy_water.unicode} contains no specific isotopes.\n") # noqa: E501
print(f" Yes, {water.unicode} contains specific isotopes.") if water.contains_isotopes else print(f" No, {water.unicode} contains no specific isotopes.")
print(f" Yes, {heavy_water.unicode} contains specific isotopes.\n") if heavy_water.contains_isotopes else print(f" No, {heavy_water.unicode} contains no specific isotopes.\n")

# OUTPUT:
#
Expand Down
69 changes: 69 additions & 0 deletions examples/example6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from chemformula import ChemFormula

conjugated_diene = ChemFormula("C4H6")
dienophile = ChemFormula("C2H4")

diels_alder_adduct = conjugated_diene + dienophile

print("\n--- Adding ChemFormula Objects ---")
print(f" Butadiene {conjugated_diene.unicode} and ethylene {dienophile.unicode}"
f" undergo a Diels-Alder reaction to form {diels_alder_adduct.unicode}.")
print(f" Molecular weight: {diels_alder_adduct.formula_weight:.2f} g/mol.")

# OUTPUT:
#
# --- Adding ChemFormula Objects ---
# Butadiene C₄H₆ and ethylene C₂H₄ undergo a Diels-Alder reaction to form C₆H₁₀.
# Molecular weight: 82.15 g/mol.
#


dichloroethane = ChemFormula("ClH2CCH2Cl")
hydrogen_chloride = ChemFormula("HCl")
vinyl_chloride = dichloroethane - hydrogen_chloride

print("\n--- Subtracting ChemFormula Objects ---")
print(f" Vinyl chloride {vinyl_chloride.hill_formula.unicode} is synthesized from dichloroethane"
f" {dichloroethane.hill_formula.unicode} by elimination of hydrogen chloride"
f" {hydrogen_chloride.hill_formula.unicode}.")
print(f" Molecular weight: {vinyl_chloride.formula_weight:.2f} g/mol.")

# OUTPUT:
#
# --- Subtracting ChemFormula Objects ---
# Vinyl chloride C₂H₃Cl is synthesized from dichloroethane C₂H₄Cl₂ by elimination of hydrogen chloride ClH.
# Molecular weight: 62.50 g/mol.
#


borane = ChemFormula("BH3")
diborane = 2 * borane
print("\n--- Multiplying ChemFormula Objects ---")
print(f" Diborane {diborane.hill_formula.unicode} is formed by the dimerization of two borane"
f" {borane.hill_formula.unicode} molecules.")
print(f" Molecular weight of diborane: {diborane.formula_weight:.2f} g/mol.")

# OUTPUT:
#
# --- Multiplying ChemFormula Objects ---
# Diborane B₂H₆ is formed by the dimerization of two borane BH₃ molecules.
# Molecular weight of diborane: 27.67 g/mol.
#


ATP = ChemFormula("C10H12N5O13P3", -4)
water = ChemFormula("H2O")
dihydrogen_phosphate = ChemFormula("H2PO4", -1)

AMP = ATP + 2 * water - 2 * dihydrogen_phosphate

print("\n--- Arithmetics with ChemFormula Objects ---")
print(f" ATP ({ATP.hill_formula.unicode}) hydrolyzes with two water molecules"
f" to AMP ({AMP.hill_formula.unicode}) and two inorganic phosphates ({dihydrogen_phosphate.unicode})"
f" releasing energy for cellular processes.\n")

# OUTPUT:
#
# --- Arithmetics with ChemFormula Objects ---
# ATP (C₁₀H₁₂N₅O₁₃P₃⁴⁻) hydrolyzes to AMP (C₁₀H₁₂N₅O₇P²⁻) and two inorganic phosphates (H₂PO₄⁻) releasing energy for cellular processes.
#
Loading