A280 Protein Concentration Tutorial: ε280 for 11 Proteins, and Why A280 ≠ mg/mL
A280 Protein Concentration Tutorial: ε280 for 11 Proteins, and Why A280 ≠ mg/mL
Measuring protein concentration at 280 nm comes down to one line of Beer–Lambert:
c (M) = A280 / (ε280 × path length in cm)The line is never the part that goes wrong. What goes wrong is ε280: which number you typed in, which sequence it came from, and whether anything besides your protein absorbed at 280 nm.
This page computes ε280, molecular weight and Abs 0.1% (the A280 of a 1 mg/mL solution in a 1 cm cell) for eleven proteins that turn up in most labs, straight from UniProt. It then uses those numbers to answer three questions that come up again and again:
- How wrong is the “1 A280 = 1 mg/mL” shortcut, protein by protein?
- What happens if you paste the sequence as UniProt shows it into an extinction coefficient calculator?
- How much does nucleic-acid contamination inflate A280, and why does the answer depend on the protein?
Every number in the tables came out of a script that was run on 2026-09-28. The script is reproduced near the end, and the limits of what it shows are listed after it.
If you already have a trusted ε280 and only need the concentration, the Runcell A280 protein concentration calculator does the Beer–Lambert step in the browser, with blank, path length and dilution. The rest of this page is about getting the ε280 you feed it right.
Environment for every number on this page:
| Component | Version |
|---|---|
| Python | 3.14.2 |
| Biopython | 1.88 |
| Sequences | UniProtKB REST, fetched 2026-09-28 (accessions and sequence versions in the table) |
| Coefficients | Trp 5500, Tyr 1490, cystine 125 M⁻¹ cm⁻¹ (Pace et al. 1995, the values ExPASy ProtParam uses) |
How ε280 is estimated from a sequence
At 280 nm nearly all of a protein’s absorbance comes from three groups: tryptophan, tyrosine, and cystine (a disulfide-bonded pair of cysteines; a free cysteine barely absorbs here). The sequence estimate simply adds them up:
ε280 = 5500 × nTrp + 1490 × nTyr + 125 × nCystine (M⁻¹ cm⁻¹, protein in water)
Abs 0.1% = ε280 / MW (A280 of 1 mg/mL, 1 cm)These are the coefficients and the formula ExPASy ProtParam documents. ProtParam’s own
documentation also carries the most important caveat about them: the estimate is “quite
reliable for proteins containing Trp residues, however there may be more than 10% error
for proteins without Trp residues.” Keep that in mind when you read the rows below that
have Trp = 0.
Two conventions to keep straight:
- Abs 0.1% is the absorbance of a 1 mg/mL (0.1%) solution. E1% is the absorbance of a 10 mg/mL (1%) solution, so E1% = 10 × Abs 0.1%. Mixing them up gives a clean 10× error.
- Molar ε (M⁻¹ cm⁻¹) gives you molar concentration. To get mg/mL you still need the molecular weight, and that has to be the MW of the same polypeptide ε was computed for. That second point is where the precursor trap below comes from.
ε280 and Abs 0.1% for 11 proteins (mature chains)
Each sequence was cut to the mature protein using UniProt’s own Signal, Propeptide,
Chain and Initiator methionine annotations. Cystines are the disulfide bonds UniProt
annotates inside that chain, with alternative pairings (marked Alternate) left out.
MW is the average mass of the unmodified polypeptide from Biopython, without subtracting
the 2 H lost per disulfide (about 0.05% for BSA). Glycans, phosphates and other
modifications are not included. UniProt annotates an N-linked glycan, two phosphoserines and
an N-acetyl group on ovalbumin, for example, so its real mass is higher and its true
Abs 0.1% lower than the table shows. Molar concentration from A280 is unaffected. mg/mL is
affected, and if your protein is glycosylated you need its actual mass.
| Protein | UniProt (seq. version) | Mature range | Trp | Tyr | Cystines | ε280 (M⁻¹ cm⁻¹) | MW (Da) | Abs 0.1% |
|---|---|---|---|---|---|---|---|---|
| Hen egg-white lysozyme | P00698 (1) | 19–147 | 6 | 3 | 4 | 37,970 | 14,313.0 | 2.653 |
| Bovine α-lactalbumin | P00711 (2) | 20–142 | 4 | 4 | 4 | 28,460 | 14,185.9 | 2.006 |
| Bovine chymotrypsinogen A | P00766 (1) | 1–245 | 8 | 4 | 5 | 50,585 | 25,665.8 | 1.971 |
| Bovine carbonic anhydrase II | P00921 (3) | 2–260 | 7 | 8 | 0 | 50,420 | 28,982.2 | 1.740 |
| Bovine BPTI (aprotinin) | P00974 (2) | 36–93 | 0 | 4 | 3 | 6,335 | 6,517.5 | 0.972 |
| Bovine β-lactoglobulin | P02754 (3) | 17–178 | 2 | 4 | 2 | 17,210 | 18,281.0 | 0.941 |
| Hen ovalbumin | P01012 (2) | 2–386 | 3 | 10 | 1 | 31,525 | 42,749.6 | 0.737 |
| Bovine ribonuclease A | P61823 (1) | 27–150 | 0 | 6 | 4 | 9,440 | 13,690.2 | 0.690 |
| Bovine serum albumin (BSA) | P02769 (4) | 25–607 | 2 | 20 | 17 | 42,925 | 66,432.3 | 0.646 |
| Human serum albumin (HSA) | P02768 (2) | 25–609 | 1 | 18 | 17 | 34,445 | 66,471.5 | 0.518 |
| Human ubiquitin (one monomer) | P0CG48 (3) | 1–76 | 0 | 1 | 0 | 1,490 | 8,564.7 | 0.174 |
Two notes on specific rows:
- Chymotrypsinogen A is the whole 245-residue UniProt entry, i.e. the zymogen. Active chymotrypsin is three chains produced by cutting out two dipeptides, so its numbers differ slightly.
- Ubiquitin is residues 1–76 of the polyubiquitin-C precursor, which UniProt stores as nine back-to-back copies. That detail matters in the next section.
These are sequence-based estimates, not measurements. We did not compare them against experimentally determined coefficients. If your lab or the supplier has a measured ε280 for your protein, use that instead.
Biopython agrees, except where it has to guess the disulfides
Biopython’s ProteinAnalysis.molar_extinction_coefficient() returns the same pair of
numbers ProtParam does: one assuming every Cys is reduced, and one assuming every pair of
Cys forms a cystine (nCys // 2). On the mature chains it matched our numbers exactly for
10 of 11 proteins. The exception:
Hen ovalbumin bio_red=31400 bio_ox=31775 DIFFERS (ours ox=31525, S-S=1, C//2=3)Ovalbumin has six cysteines but UniProt annotates only one disulfide. The “all pairs”
assumption counts three. At 125 M⁻¹ cm⁻¹ per cystine that comes to 0.8% here. It is small
because cystine is a weak chromophore. The direction is predictable, but it only shows up
when two or more cysteines are free: the albumins each have 35 Cys and 17 disulfides,
so their one free Cys is simply left over by nCys // 2 and the “all pairs” number is
exactly right.
How wrong is “1 A280 = 1 mg/mL”?
The shortcut sets Abs 0.1% to 1.0 for every protein. Beer–Lambert gives
mg/mL = A280 / Abs 0.1%, so the shortcut’s answer divided by the true one is exactly
Abs 0.1%:
| Protein | Abs 0.1% | Shortcut reports | Which means |
|---|---|---|---|
| Lysozyme | 2.653 | 2.65× the true value | overestimates by 165% |
| α-Lactalbumin | 2.006 | 2.01× | overestimates by 101% |
| Chymotrypsinogen A | 1.971 | 1.97× | overestimates by 97% |
| Carbonic anhydrase II | 1.740 | 1.74× | overestimates by 74% |
| β-Lactoglobulin | 0.941 | 0.94× | underestimates by 6% |
| BSA | 0.646 | 0.65× | underestimates by 35% |
| HSA | 0.518 | 0.52× | underestimates by 48% |
| Ubiquitin | 0.174 | 0.17× | underestimates by 83% |
Across the eleven proteins, Abs 0.1% runs from 0.174 to 2.653, a 15-fold spread. The shortcut is within 10% only for β-lactoglobulin (0.941) and BPTI (0.972), and in both cases by coincidence. The shortcut does not fail at random. It tracks Trp density: the three proteins where it overestimates most (lysozyme, α-lactalbumin, chymotrypsinogen A) carry 6, 4 and 8 Trp in 129, 123 and 245 residues, while ubiquitin has no Trp and one Tyr.
A reading of A280 = 0.50 therefore means about 0.19 mg/mL of lysozyme or about
2.9 mg/mL of ubiquitin. Same number on the screen, a 15× difference in the tube.
The shortcut is fine for tracking relative amounts of the same protein, for example column fractions. It is not fine for anything that needs an absolute number: a molar ratio in a binding assay, a dose, or a comparison between two proteins.
The precursor trap: pasting the sequence UniProt shows you
The obvious workflow is: open the UniProt entry, copy the sequence, paste it into an
extinction coefficient calculator. The sequence at the top of a UniProt entry is the
precursor: it still contains the signal peptide, propeptides and, for polyproteins, every
copy. Here is what that paste does to the concentration you end up reporting.
“Naive paste” means the whole UniProt sequence with the calculator’s default of
nCys // 2 cystines:
| Protein | Residues pasted | Mature residues | Molar conc. error | mg/mL error |
|---|---|---|---|---|
| BPTI (aprotinin) | 100 | 58 | −47.0% | −11.4% |
| HSA | 609 | 585 | −16.9% | −13.2% |
| BSA | 607 | 583 | −14.0% | −10.3% |
| Ribonuclease A | 150 | 124 | +0.0% | +20.2% |
| α-Lactalbumin | 142 | 123 | +0.0% | +14.5% |
| Lysozyme | 147 | 129 | +0.0% | +13.5% |
| β-Lactoglobulin | 178 | 162 | −0.7% | +8.0% |
| Hen ovalbumin | 386 | 385 | −0.8% | −0.5% |
| Carbonic anhydrase II | 260 | 259 | +0.0% | +0.5% |
| Ubiquitin | 685 | 76 | −88.9% | −0.1% |
(Error = the concentration you would report ÷ the concentration from the mature chain − 1. Chymotrypsinogen A is left out because its entry already is the mature zymogen.)
There are two different failure modes in that table, and they point in different directions:
1. The removed segment carries a chromophore, so ε is too high and every concentration
comes out low. The albumin signal peptide starts MKWVTF…SSAYS, and that one Trp plus
one Tyr adds 6,990 M⁻¹ cm⁻¹. BPTI’s propeptides include a Trp, so the pasted ε280 is almost
double the mature one (11,960 vs 6,335). Your BPTI molarity would be 47% low.
2. The removed segment has no chromophore, so ε is right but MW is too high, and only mg/mL is off. Ribonuclease A’s 26-residue signal peptide has no Trp or Tyr, so molar concentration is unaffected. The MW, though, goes from 13,690 to 16,461, and every mg/mL figure comes out 20% high. Lysozyme and α-lactalbumin behave the same way at +13–15%.
Ubiquitin is the extreme case, and it runs the other way. The canonical UBC entry is nine ubiquitins in a row, so pasting it multiplies both ε and MW by roughly nine. The mg/mL answer is almost correct (−0.1%), because the ratio barely changes, but the molar answer is off by 9×. That is precisely the number you need for stoichiometry.
The fix is to paste the mature chain. On UniProt, the PTM / Processing section lists
Signal, Propeptide and Chain ranges. Take the Chain range, or the exact construct you
expressed, tags included. The
Runcell protein molecular weight calculator
gives the MW of whatever you paste, so you can check that the MW matches the ε you are
using. It is the same kind of mismatch as the free-acid vs sodium-salt molecular weight in
our DNA molecular weight workflow: the arithmetic is easy
and the input is where the error comes from.
Reduced or oxidized: how much the cystine assumption moves ε
Calculators return two ε280 values. Here is how far apart they are for the mature chains:
| Protein | ε reduced | ε with cystines | Increase over reduced ε | Trp |
|---|---|---|---|---|
| HSA | 32,320 | 34,445 | 6.6% | 1 |
| BPTI | 5,960 | 6,335 | 6.3% | 0 |
| Ribonuclease A | 8,940 | 9,440 | 5.6% | 0 |
| BSA | 40,800 | 42,925 | 5.2% | 2 |
| α-Lactalbumin | 27,960 | 28,460 | 1.8% | 4 |
| β-Lactoglobulin | 16,960 | 17,210 | 1.5% | 2 |
| Lysozyme | 37,470 | 37,970 | 1.3% | 6 |
| Chymotrypsinogen A | 49,960 | 50,585 | 1.3% | 8 |
| Ovalbumin | 31,400 | 31,525 | 0.4% | 3 |
| Carbonic anhydrase II | 50,420 | 50,420 | 0.0% | 7 |
| Ubiquitin | 1,490 | 1,490 | 0.0% | 0 |
The choice matters most (5–7%) for proteins with many disulfides and few Trp: the albumins, ribonuclease A, BPTI. For Trp-rich proteins it hardly matters.
The rule is to count the disulfides actually present, not the cysteines. A native
protein with one or more free Cys still has its disulfides. BSA has one free Cys and 17
disulfides, and switching it to the reduced ε because of that one free Cys would
overstate its concentration by about 5%. Use the fully reduced value only when the
disulfides really are broken, for example in DTT or TCEP, or for a protein that has no
disulfides in the first place. The number to use is UniProt’s Disulfide bond annotations
inside the chain you are measuring, which is what the script below counts.
A 5–7% choice is smaller than the precursor errors above, and smaller than the error ProtParam warns about for Trp-free proteins. BPTI, ribonuclease A and ubiquitin have no Trp, so ProtParam’s documentation warns that their sequence estimate may be off by more than 10%, whichever cystine count you use. If you need better, measure ε for your protein.
Nucleic-acid contamination: why it hurts low-Abs 0.1% proteins more
Nucleic acids absorb strongly at 280 nm, and they absorb the same amount whatever protein they are contaminating. A given contamination is therefore a larger fraction of the signal for a protein that absorbs weakly per milligram, i.e. one with a low Abs 0.1%. The molar ε alone does not decide this. Under the model below, the overestimate for 1% contamination scales with 1 / Abs 0.1%.
The model below is built from assumed conventions, not measurements. It shows the size of the effect, not what your sample contains:
- contaminating nucleic acid: A260 = 20 per mg/mL (the “50 µg/mL per A260 unit” dsDNA convention), A260/A280 = 1.8, so A280 ≈ 11.1 per mg/mL;
- pure protein: A260/A280 = 0.57, a commonly quoted figure that in reality varies from protein to protein;
- contamination expressed as % of total mass.
| Protein | Nucleic acid (% w/w) | A260/A280 you would see | A280 overestimate |
|---|---|---|---|
| Lysozyme (Abs 0.1% 2.65) | 1% | 0.62 | +4.2% |
| 5% | 0.79 | +22.0% | |
| BSA (Abs 0.1% 0.65) | 1% | 0.75 | +17.4% |
| 5% | 1.15 | +90.5% | |
| Ubiquitin (Abs 0.1% 0.17) | 0.5% | 0.87 | +32.1% |
| 1% | 1.05 | +64.5% | |
| 5% | 1.52 | +336.1% |
The same 1% contamination adds 4% to lysozyme’s A280 and 64% to ubiquitin’s. Two practical consequences follow:
- A single 260/280 cutoff means different things for different proteins. Under these assumptions a ratio of 0.75 is about 1% nucleic acid in BSA, while lysozyme does not reach 0.75 until somewhere between 2% and 5%.
- Sensitivity follows Abs 0.1%, not the presence of Trp. Having no Trp and having a low Abs 0.1% are separate problems. BPTI has no Trp but an Abs 0.1% of 0.97, which makes it less sensitive to contamination than HSA (0.52, one Trp). The proteins that get hit twice are the ones with both problems, ubiquitin (0.17) and ribonuclease A (0.69): their sequence estimate is the least reliable (ProtParam’s >10% warning), and the same contamination distorts them more. For those, check the 260/280 ratio before trusting A280, or use a dye-based assay with a standard of the same protein.
The A280 calculator linked above does not assess A260/A280 purity (its own documentation says so), so check the ratio before you enter the number.
The full workflow
- Get the right sequence. On UniProt, take the
Chainrange, or the exact construct you expressed, including tags and linkers. Do not paste the precursor. - Count the disulfides. Use the number of disulfide bonds actually present (UniProt’s
Disulfide bondannotations inside your chain). A free Cys does not make the protein “reduced”. Use the fully reduced ε only if the disulfides are broken, e.g. in DTT or TCEP. - Compute ε280 and MW from the same sequence, either with the script below or with the A280 calculator’s “From sequence” mode . Check the MW against the protein molecular weight calculator if you are unsure what you pasted.
- Check A260/A280 before trusting A280. Be strictest for proteins with a low Abs 0.1% (below about 0.7 in the table), because the same contamination inflates their A280 the most. A protein with no Trp is a separate problem: its ε280 estimate is itself uncertain.
- Measure, blank, apply Beer–Lambert with the right path length and dilution factor.
- Convert units when you need them. To go from mg/mL to µM for a stoichiometry calculation, the protein concentration calculator does the mg/mL ↔ molar conversion. Give it the MW of the mature chain, not the precursor.
Reproduce the table
This is the core of the script that produced every number above (the full version also
prints the precursor, cystine and contamination blocks). It needs Biopython and network
access to rest.uniprot.org:
python -m pip install biopythonIf import Bio works in a terminal but fails in a notebook cell, the notebook is running a
different interpreter. That failure mode is covered in
ModuleNotFoundError in Jupyter: A Reproduced Kernel Failure Matrix.
import json, urllib.request
from Bio.SeqUtils.ProtParam import ProteinAnalysis
W, Y, CYSTINE = 5500, 1490, 125
def fetch(acc):
url = f"https://rest.uniprot.org/uniprotkb/{acc}.json"
with urllib.request.urlopen(url, timeout=60) as r:
return json.load(r)
def mature_a280(acc, start, end):
"""epsilon280, MW and Abs 0.1% for residues start..end (1-based, inclusive)."""
d = fetch(acc)
seq = d["sequence"]["value"][start - 1:end]
# count only disulfides UniProt annotates inside this chain, skipping alternative pairings
ss = [f for f in d["features"]
if f["type"] == "Disulfide bond"
and "alternate" not in f.get("description", "").lower()
and start <= f["location"]["start"]["value"] <= end
and start <= f["location"]["end"]["value"] <= end]
eps = seq.count("W") * W + seq.count("Y") * Y + len(ss) * CYSTINE
mw = ProteinAnalysis(seq).molecular_weight()
return eps, mw, eps / mw
for acc, name, (s, e) in [
("P00698", "lysozyme", (19, 147)),
("P02769", "BSA", (25, 607)),
("P0CG48", "ubiquitin", (1, 76)),
]:
eps, mw, abs01 = mature_a280(acc, s, e)
print(f"{name:<10} eps280={eps:>6} MW={mw:>9.1f} Abs0.1%={abs01:.3f}")Output of that snippet on 2026-09-28:
lysozyme eps280= 37970 MW= 14313.0 Abs0.1%=2.653
BSA eps280= 42925 MW= 66432.3 Abs0.1%=0.646
ubiquitin eps280= 1490 MW= 8564.7 Abs0.1%=0.174Note that the mature ranges are typed by hand from each entry’s PTM / Processing section. The script does not work them out itself, because the right range depends on what you actually have in the tube.
What this page does not show
- No measured coefficients. Every ε280 here is a sequence estimate. We did not measure any of these proteins or compare the estimates against published experimental values.
- Water, 280 nm, native state. The coefficients are for proteins in water. Denaturant, pH extremes, and ligands or prosthetic groups that absorb at 280 nm (hemes, flavins, fluorescent-protein chromophores) are outside the formula. That is why no heme or fluorescent proteins are in the panel.
- The contamination table is a model with assumed inputs (nucleic acid A260 = 20 per mg/mL and A260/A280 = 1.8; protein A260/A280 = 0.57). Change the assumptions and the numbers move. The ordering (lowest Abs 0.1% hit hardest) does not.
- Mature ranges come from UniProt annotations at the sequence versions listed. A recombinant construct with a tag, a truncation or a different processing site needs its own sequence.
- MW is the unmodified chain. It ignores disulfide hydrogens (−2.016 Da per bond, below 0.1% here) and, more importantly, glycosylation and other modifications. For a glycoprotein such as ovalbumin these can matter more; we did not quantify them. The mg/mL numbers (Abs 0.1%) inherit that error. The molar ε does not.
Related
- DNA Molecular Weight in Python: A Runnable ng/µL → nM Workflow: the same “which molecular weight?” problem on the nucleic-acid side, with real NCBI sequences.
- BCA vs Bradford: How Far Off Each Assay Reads 13 Proteins: when the sample is a mixture and A280 is not an option, how far each colorimetric assay is off with a BSA standard.
- ModuleNotFoundError in Jupyter: for when the script above works in a terminal and not in a notebook.
- Tools: A280 protein concentration calculator · Protein concentration (mg/mL ↔ µM) · Protein molecular weight