AboutBlogMediaTags

A280 Protein Concentration Tutorial: ε280 for 11 Proteins, and Why A280 ≠ mg/mL

Runcell Team,

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:

  1. How wrong is the “1 A280 = 1 mg/mL” shortcut, protein by protein?
  2. What happens if you paste the sequence as UniProt shows it into an extinction coefficient calculator?
  3. 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:

ComponentVersion
Python3.14.2
Biopython1.88
SequencesUniProtKB REST, fetched 2026-09-28 (accessions and sequence versions in the table)
CoefficientsTrp 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:


ε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.

ProteinUniProt (seq. version)Mature rangeTrpTyrCystinesε280 (M⁻¹ cm⁻¹)MW (Da)Abs 0.1%
Hen egg-white lysozymeP00698 (1)19–14763437,97014,313.02.653
Bovine α-lactalbuminP00711 (2)20–14244428,46014,185.92.006
Bovine chymotrypsinogen AP00766 (1)1–24584550,58525,665.81.971
Bovine carbonic anhydrase IIP00921 (3)2–26078050,42028,982.21.740
Bovine BPTI (aprotinin)P00974 (2)36–930436,3356,517.50.972
Bovine β-lactoglobulinP02754 (3)17–17824217,21018,281.00.941
Hen ovalbuminP01012 (2)2–386310131,52542,749.60.737
Bovine ribonuclease AP61823 (1)27–1500649,44013,690.20.690
Bovine serum albumin (BSA)P02769 (4)25–6072201742,92566,432.30.646
Human serum albumin (HSA)P02768 (2)25–6091181734,44566,471.50.518
Human ubiquitin (one monomer)P0CG48 (3)1–760101,4908,564.70.174

Two notes on specific rows:

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%:

ProteinAbs 0.1%Shortcut reportsWhich means
Lysozyme2.6532.65× the true valueoverestimates by 165%
α-Lactalbumin2.0062.01×overestimates by 101%
Chymotrypsinogen A1.9711.97×overestimates by 97%
Carbonic anhydrase II1.7401.74×overestimates by 74%
β-Lactoglobulin0.9410.94×underestimates by 6%
BSA0.6460.65×underestimates by 35%
HSA0.5180.52×underestimates by 48%
Ubiquitin0.1740.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:

ProteinResidues pastedMature residuesMolar conc. errormg/mL error
BPTI (aprotinin)10058−47.0%−11.4%
HSA609585−16.9%−13.2%
BSA607583−14.0%−10.3%
Ribonuclease A150124+0.0%+20.2%
α-Lactalbumin142123+0.0%+14.5%
Lysozyme147129+0.0%+13.5%
β-Lactoglobulin178162−0.7%+8.0%
Hen ovalbumin386385−0.8%−0.5%
Carbonic anhydrase II260259+0.0%+0.5%
Ubiquitin68576−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 cystinesIncrease over reduced εTrp
HSA32,32034,4456.6%1
BPTI5,9606,3356.3%0
Ribonuclease A8,9409,4405.6%0
BSA40,80042,9255.2%2
α-Lactalbumin27,96028,4601.8%4
β-Lactoglobulin16,96017,2101.5%2
Lysozyme37,47037,9701.3%6
Chymotrypsinogen A49,96050,5851.3%8
Ovalbumin31,40031,5250.4%3
Carbonic anhydrase II50,42050,4200.0%7
Ubiquitin1,4901,4900.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:

ProteinNucleic acid (% w/w)A260/A280 you would seeA280 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:

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

  1. Get the right sequence. On UniProt, take the Chain range, or the exact construct you expressed, including tags and linkers. Do not paste the precursor.
  2. Count the disulfides. Use the number of disulfide bonds actually present (UniProt’s Disulfide bond annotations 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.
  3. 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.
  4. 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.
  5. Measure, blank, apply Beer–Lambert with the right path length and dilution factor.
  6. 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 biopython

If 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.174

Note 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


© Runcell.RSS