<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Peptide.Express]]></title><description><![CDATA[Peptide.Express]]></description><link>https://peptideexpress.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 14:17:24 GMT</lastBuildDate><atom:link href="https://peptideexpress.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Peptide Monoisotopic and Average Mass Calculator in Python]]></title><description><![CDATA[Compute peptide monoisotopic mass, average mass and ESI m/z from first principles in Python, then cross-check against the open-source peptide-mass package.]]></description><link>https://peptideexpress.hashnode.dev/building-a-peptide-monoisotopic-and-average-mass-calculator-in-python</link><guid isPermaLink="true">https://peptideexpress.hashnode.dev/building-a-peptide-monoisotopic-and-average-mass-calculator-in-python</guid><category><![CDATA[Python]]></category><category><![CDATA[chemistry]]></category><category><![CDATA[bioinformatics]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Peptide.Express]]></dc:creator><pubDate>Thu, 15 Oct 2026 10:23:15 GMT</pubDate><content:encoded><![CDATA[<p>By Peptide.Express (<a href="https://peptide.express">peptide.express</a>)</p>
<p>If you work with synthetic peptides, sooner or later you need to know what mass to expect from a mass spectrometer. The math is simple: add up the residues, add one water, and divide by the charge. It is also easy to get slightly wrong. This post builds a small calculator from scratch, explains why monoisotopic and average masses differ, and checks the result against an open-source package.</p>
<p>Everything below was run in a fresh virtual environment with Python 3.13.</p>
<h2>Two kinds of "mass"</h2>
<ul>
<li><strong>Monoisotopic mass</strong> uses only the most abundant isotope of each element (¹²C, ¹H, ¹⁴N, ¹⁶O, ³²S). This is the mass of the first peak in a high-resolution isotope envelope, and it is the number you compare against high-resolution MS data.</li>
<li><strong>Average mass</strong> uses each element's standard atomic weight, which is the natural isotope mix averaged together. It matches the centroid of a low-resolution peak, and it is what you use for anything weighed on a balance.</li>
</ul>
<p>For small peptides the two differ by less than a dalton. For larger ones the gap grows, because every carbon atom adds a little ¹³C.</p>
<h2>Where the element masses come from</h2>
<p>I used two primary sources and opened both to check the values:</p>
<ul>
<li>Monoisotopic masses: NIST, <a href="https://physics.nist.gov/cgi-bin/Compositions/stand_alone.pl">Atomic Weights and Isotopic Compositions</a>. For example, ¹H = 1.00782503223 u and ¹⁶O = 15.99491461957 u.</li>
<li>Average masses: IUPAC CIAAW, <a href="https://www.ciaaw.org/abridged-atomic-weights.htm">Abridged Standard Atomic Weights</a>. H = 1.0080, C = 12.011, N = 14.007, O = 15.999, S = 32.06.</li>
<li>Proton mass: NIST CODATA, <a href="https://physics.nist.gov/cgi-bin/cuu/Value?mpu">proton mass in u</a>, 1.0072764665789 u.</li>
</ul>
<h2>A from-scratch calculator</h2>
<p>Rather than hard-coding residue masses, I store each residue as an elemental formula. A residue is the amino acid minus H₂O, which is the water lost when a peptide bond forms. The full peptide is the sum of its residues plus one H₂O for the free N- and C-termini.</p>
<pre><code class="language-python"># scratch.py
# Monoisotopic masses: NIST "Atomic Weights and Isotopic Compositions"
MONO = {"C": 12.0, "H": 1.00782503223, "N": 14.00307400443,
        "O": 15.99491461957, "S": 31.9720711744}
# Average masses: IUPAC/CIAAW abridged standard atomic weights
AVG = {"C": 12.011, "H": 1.0080, "N": 14.007, "O": 15.999, "S": 32.06}
PROTON = 1.0072764665789  # CODATA 2022 proton mass, u

# Residue = amino acid minus H2O, as elemental composition (C, H, N, O, S)
RESIDUES = {
    "G": (2, 3, 1, 1, 0),  "A": (3, 5, 1, 1, 0),  "S": (3, 5, 1, 2, 0),
    "P": (5, 7, 1, 1, 0),  "V": (5, 9, 1, 1, 0),  "T": (4, 7, 1, 2, 0),
    "C": (3, 5, 1, 1, 1),  "L": (6, 11, 1, 1, 0), "I": (6, 11, 1, 1, 0),
    "N": (4, 6, 2, 2, 0),  "D": (4, 5, 1, 3, 0),  "Q": (5, 8, 2, 2, 0),
    "K": (6, 12, 2, 1, 0), "E": (5, 7, 1, 3, 0),  "M": (5, 9, 1, 1, 1),
    "H": (6, 7, 3, 1, 0),  "F": (9, 9, 1, 1, 0),  "R": (6, 12, 4, 1, 0),
    "Y": (9, 9, 1, 2, 0),  "W": (11, 10, 2, 1, 0),
}
ELEMENTS = ("C", "H", "N", "O", "S")

def composition(seq):
    total = [0, 2, 0, 1, 0]          # start with one H2O for the termini
    for aa in seq.upper():
        total = [t + r for t, r in zip(total, RESIDUES[aa])]
    return dict(zip(ELEMENTS, total))

def peptide_mass(seq, table):
    return sum(n * table[el] for el, n in composition(seq).items())

def mz(mono, z):
    return (mono + z * PROTON) / z
</code></pre>
<p>Working at the formula level has a side benefit: you get the molecular formula for free, and switching element tables is a one-argument change.</p>
<p>The <code>mz</code> function gives the electrospray ion <code>[M+zH]z+</code>. You add <em>z</em> protons, not <em>z</em> hydrogen atoms, since the ion has gained protons without their electrons, and then divide by the charge.</p>
<h2>The open-source <code>peptide-mass</code> package</h2>
<p><a href="https://pypi.org/project/peptide-mass/"><code>peptide-mass</code></a> is a small, dependency-free MIT-licensed package on PyPI:</p>
<pre><code class="language-bash">python3 -m venv venv
./venv/bin/pip install peptide-mass
</code></pre>
<p>Version 0.1.0 installs a single module. I read its source, and its public API (<code>__all__</code>) is four names: <code>mass</code>, <code>mz</code>, <code>RESIDUE</code> and <code>MODS</code>.</p>
<ul>
<li><code>mass(seq, n_acetyl=False, c_amide=False, pyroglu=False, disulfides=0)</code> returns a <code>(monoisotopic, average)</code> tuple for the neutral free peptide. It raises <code>ValueError</code> for anything outside the 20 standard residues.</li>
<li><code>mz(mono, z)</code> returns <code>(mono + z * PROTON) / z</code>.</li>
<li><code>RESIDUE</code> maps one-letter codes to <code>(monoisotopic, average)</code> residue masses. These are hard-coded numbers, not formulas.</li>
<li><code>MODS</code> holds mass deltas for N-acetylation, C-terminal amidation, pyroglutamate (from Q or E), and disulfide bonds (−2H each).</li>
</ul>
<p>It also installs a <code>peptide-mass</code> command-line tool, which can process a CSV through <code>--csv</code>.</p>
<h2>Worked example and cross-check</h2>
<p>The test sequence is a generic 8-mer, <code>GASVLPTK</code>:</p>
<pre><code class="language-python"># demo.py
from scratch import composition, peptide_mass, mz, MONO, AVG
import peptide_mass as pm

seq = "GASVLPTK"
print("formula:", composition(seq))
mono, avg = peptide_mass(seq, MONO), peptide_mass(seq, AVG)
print(f"scratch  mono {mono:.4f}  avg {avg:.4f}")
pmono, pavg = pm.mass(seq)
print(f"package  mono {pmono:.4f}  avg {pavg:.4f}")
print(f"delta    mono {mono-pmono:+.6f}  avg {avg-pavg:+.4f}")
for z in (1, 2, 3):
    print(f"[M+{z}H]{z}+  scratch {mz(mono, z):.4f}  package {pm.mz(pmono, z):.4f}")
</code></pre>
<p>Output:</p>
<pre><code class="language-text">formula: {'C': 34, 'H': 61, 'N': 9, 'O': 11, 'S': 0}
scratch  mono 771.4491  avg 771.9140
package  mono 771.4491  avg 771.9019
delta    mono -0.000001  avg +0.0121
[M+1H]1+  scratch 772.4563  package 772.4563
[M+2H]2+  scratch 386.7318  package 386.7318
[M+3H]3+  scratch 258.1570  package 258.1570
</code></pre>
<p>The CLI gives the same answer:</p>
<pre><code class="language-text">$ peptide-mass GASVLPTK
monoisotopic 771.4491 Da | average 771.90 Da
  [M+1H]1+  m/z 772.4563
  [M+2H]2+  m/z 386.7318
  [M+3H]3+  m/z 258.1570
  [M+4H]4+  m/z 193.8695
  [M+5H]5+  m/z 155.2971
</code></pre>
<p><strong>Monoisotopic masses and m/z agree to four decimal places.</strong> The difference of about 1 µDa comes from the package rounding its residue masses to six decimals.</p>
<p><strong>Average masses differ by 0.012 Da.</strong> That is expected and not a bug. The package's residue table and my CIAAW abridged weights (for example H = 1.0080, C = 12.011) are not the same element table, and with 61 hydrogens and 34 carbons the small per-atom differences add up. The package's own documentation warns about ±0.01–0.05 Da differences between calculators for this reason. When you compare average masses, write down which table you used.</p>
<h2>The reference dataset</h2>
<p>The package's home page is the GitHub repo <a href="https://github.com/Peptide-Express/peptide-reference-data">Peptide-Express/peptide-reference-data</a>, which is also archived on Zenodo as <a href="https://doi.org/10.5281/zenodo.23284819">10.5281/zenodo.23284819</a> ("Peptide reference masses and CoA identity calculator"). It contains:</p>
<ul>
<li><code>data/peptides.csv</code>: 23 peptides with sequence, terminal modifications, disulfide count, a source reference, and notes</li>
<li><code>data/masses.csv</code>: computed monoisotopic and average masses, plus <code>[M+H]+</code> through <code>[M+5H]5+</code> m/z</li>
<li><code>scripts/peptide_mass.py</code>: the same standalone calculator</li>
<li><code>docs/reading-the-identity-section.md</code>: a short guide to comparing a measured mass against the theoretical value</li>
</ul>
<p>It deliberately leaves out peptides with non-standard residues or lipidation instead of approximating them. Data is licensed CC BY 4.0 and code is MIT. That makes it handy as a regression fixture: run your own calculator over <code>peptides.csv</code> and diff the results against <code>masses.csv</code>.</p>
<h2>Extending it</h2>
<p>Some easy next steps:</p>
<ul>
<li>Add modifications as formula deltas, for example amidation as <code>{"O": -1, "N": +1, "H": +1}</code>.</li>
<li>Generate the full isotope envelope by convolving the element isotope distributions.</li>
<li>Add input validation that rejects non-standard letters with a clear error, as the package does.</li>
</ul>
<p>The key lesson is to keep monoisotopic and average masses clearly apart, and to state which element table you used.</p>
<p>For laboratory research use only. Not for human or veterinary use.</p>
]]></content:encoded></item></channel></rss>