Design PCR/qPCR primers with primer3-py design_primers/calc_hairpin, Bio.SeqUtils Tm, and blastn specificity checks. Use when designing PCR, qPCR, cloning, or genotyping primers, or checking Tm/dimers/specificity.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add Pavel-Kravchenko/Bioinformatics --skill bio-applied-primer-design --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Bio Applied Primer Design?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/pavel-kravchenko-bio-applied-primer-design)More formats (shields.io, HTML) on the badges page.
---
name: bio-applied-primer-design
description: "Design PCR/qPCR primers with primer3-py design_primers/calc_hairpin, Bio.SeqUtils Tm, and blastn specificity checks. Use when designing PCR, qPCR, cloning, or genotyping primers, or checking Tm/dimers/specificity."
tool_type: python
primary_tool: Primer3
---
# PCR & qPCR Primer Design
## When to Use
- Designing a forward/reverse primer pair for cloning a target region, with restriction sites to append
- Building a qPCR (SYBR or TaqMan) assay that needs a small amplicon and tightly matched primer Tms
- Designing genotyping primers (knockout/knock-in confirmation, allele-specific PCR, colony PCR screening)
- Cross-checking a candidate primer's Tm against a second model before ordering
- Screening a primer pair for hairpins, self-dimers, cross-dimers, or off-target genome binding
## Version Compatibility
- `primer3-py` >= 2.0 (`primer3.bindings.design_primers`, `calc_tm`/`calc_hairpin`/`calc_homodimer`/`calc_heterodimer`); wraps Primer3 core 2.6.1. The older `designPrimers`/camelCase API is deprecated — use the snake_case bindings.
- `biopython` >= 1.83 (`Bio.SeqUtils.MeltingTemp` for an independent nearest-neighbor Tm cross-check)
- `blastn` (BLAST+ >= 2.15) for specificity screening against a local or NCBI database
- Python >= 3.9
## Prerequisites
```bash
pip install primer3-py biopython
# for local specificity screening:
makeblastdb -in genome.fa -dbtype nucl -out genome_db
```
- Familiarity with primer Tm/GC/amplicon-size tradeoffs and IUPAC/restriction-site basics
- Related skills: `bio-applied-genetic-engineering-in-silico` (from-scratch NN Tm math, restriction-site appending), `bio-core-blast-searching` (full BLAST workflow)
## Cloning / Standard PCR Primers
**Goal:** design a primer pair flanking a target region with explicit Tm/GC/size constraints, then optionally append restriction sites for directional cloning.
**Approach:** call `primer3.bindings.design_primers` with `SEQUENCE_TEMPLATE` + `SEQUENCE_TARGET` (region that must be inside the amplicon) and a `PRIMER_PRODUCT_SIZE_RANGE`; read back `PRIMER_PAIR_NUM_RETURNED` ranked candidates.
```python
import primer3
def design_cloning_primers(template: str, target_start: int, target_len: int,
product_size_range: tuple = (150, 600),
re_site_fwd: str = '', re_site_rev: str = '',
target_tm: float = 60.0) -> list:
"""Design ranked forward/reverse primer pairs that amplify a product containing
template[target_start:target_start+target_len], appending restriction sites for cloning.
"""
seq_args = {
'SEQUENCE_ID': 'insert',
'SEQUENCE_TEMPLATE': template,
'SEQUENCE_TARGET': [target_start, target_len],
}
global_args = {
'PRIMER_TASK': 'generic',
'PRIMER_PICK_LEFT_PRIMER': 1,
'PRIMER_PICK_RIGHT_PRIMER': 1,
'PRIMER_OPT_SIZE': 20, 'PRIMER_MIN_SIZE': 18, 'PRIMER_MAX_SIZE': 27,
'PRIMER_OPT_TM': target_tm, 'PRIMER_MIN_TM': target_tm - 3, 'PRIMER_MAX_TM': target_tm + 3,
'PRIMER_MIN_GC': 40.0, 'PRIMER_MAX_GC': 60.0,
'PRIMER_MAX_POLY_X': 4,
'PRIMER_PRODUCT_SIZE_RANGE': [list(product_size_range)],
'PRIMER_NUM_RETURN': 5,
}
result = primer3.bindings.design_primers(seq_args, global_args)
pairs = []
for i in range(result.get('PRIMER_PAIR_NUM_RETURNED', 0)):
pairs.append({
'forward': re_site_fwd + result[f'PRIMER_LEFT_{i}_SEQUENCE'],
'reverse': re_site_rev + result[f'PRIMER_RIGHT_{i}_SEQUENCE'],
'product_size': result[f'PRIMER_PAIR_{i}_PRODUCT_SIZE'],
'fwd_tm': result[f'PRIMER_LEFT_{i}_TM'],
'rev_tm': result[f'PRIMER_RIGHT_{i}_TM'],
})
return pairs
if __name__ == '__main__':
tmpl = 'GCTTGCATGCCTGCAGGTCGACTCTAGAGGATCCCCCTACATTTTAGCATCAGTGAGTACAGCATGCTTACTGGAAGAGAGGGTCATGCAACAGATTAGGAGGTAAGTTTGCAAAGGCAGGCTAAGGAGGAGACGCACTGAATGCCATGGTAAGAACTCTGGAC'
pairs = design_cloning_primers(tmpl, target_start=40, target_len=60, re_site_fwd='GGATCC', re_site_rev='GAATTC')
assert len(pairs) > 0
assert pairs[0]['forward'].startswith('GGATCC')
print(f"best pair: fwd={pairs[0]['forward']} rev={pairs[0]['reverse']} product={pairs[0]['product_size']}bp")
```
## qPCR Assay Design (with optional TaqMan probe)
**Goal:** design a qPCR-appropriate primer pair (small amplicon, narrow Tm window) and, optionally, an internal hydrolysis probe.
**Approach:** shrink `PRIMER_PRODUCT_SIZE_RANGE` to 70-150 bp, tighten the Tm window to 59-61C, and set `PRIMER_PICK_INTERNAL_OLIGO=1` with a probe Tm ~8-10C above the primers (standard TaqMan design rule) so the probe binds before the primers extend.
```python
import primer3
def design_qpcr_assay(template: str, target_start: int, target_len: int,
max_amplicon: int = 150, want_probe: bool = False) -> list:
"""Design qPCR primer pairs (amplicon <= max_amplicon bp) spanning the target region,
with an optional TaqMan-style internal probe (Tm ~10C hotter than the primers).
"""
seq_args = {
'SEQUENCE_ID': 'qpcr_target',
'SEQUENCE_TEMPLATE': template,
'SEQUENCE_TARGET': [target_start, target_len],
}
global_args = {
'PRIMER_TASK': 'generic',
'PRIMER_PICK_LEFT_PRIMER': 1, 'PRIMER_PICK_RIGHT_PRIMER': 1,
'PRIMER_PICK_INTERNAL_OLIGO': 1 if want_probe else 0,
'PRIMER_OPT_SIZE': 20, 'PRIMER_MIN_SIZE': 18, 'PRIMER_MAX_SIZE': 24,
'PRIMER_OPT_TM': 60.0, 'PRIMER_MIN_TM': 59.0, 'PRIMER_MAX_TM': 61.0,
'PRIMER_MIN_GC': 30.0, 'PRIMER_MAX_GC': 70.0,
'PRIMER_INTERNAL_OPT_TM': 70.0, 'PRIMER_INTERNAL_MIN_TM': 68.0, 'PRIMER_INTERNAL_MAX_TM': 72.0,
'PRIMER_INTERNAL_MIN_SIZE': 18, 'PRIMER_INTERNAL_MAX_SIZE': 27,
'PRIMER_PRODUCT_SIZE_RANGE': [[70, max_amplicon]],
'PRIMER_NUM_RETURN': 3,
}
result = primer3.bindings.design_primers(seq_args, global_args)
assays = []
for i in range(result.get('PRIMER_PAIR_NUM_RETURNED', 0)):
assay = {
'forward': result[f'PRIMER_LEFT_{i}_SEQUENCE'],
'reverse': result[f'PRIMER_RIGHT_{i}_SEQUENCE'],
'product_size': result[f'PRIMER_PAIR_{i}_PRODUCT_SIZE'],
}
if want_probe and f'PRIMER_INTERNAL_{i}_SEQUENCE' in result:
assay['probe'] = result[f'PRIMER_INTERNAL_{i}_SEQUENCE']
assays.append(assay)
return assays
if __name__ == '__main__':
tmpl = 'GCTTGCATGCCTGCAGGTCGACTCTAGAGGATCCCCCTACATTTTAGCATCAGTGAGTACAGCATGCTTACTGGAAGAGAGGGTCATGCAACAGATTAGGAGGTAAGTTTGCAAAGGCAGGCTAAGGAGGAGACGCACTGAATGCCATGGTAAGAACTCTGGAC'
assays = design_qpcr_assay(tmpl, target_start=50, target_len=20, want_probe=True)
assert all(a['product_size'] <= 150 for a in assays)
print(f"qPCR assay: {assays[0]}")
```
## Primer QC: Tm Cross-Check, Dimers/Hairpins, Specificity
**Goal:** before ordering, verify Tm agreement across models, flag hairpin/dimer risk, and confirm the primers won't bind off-target elsewhere in the genome.
**Approach:** get a second Tm opinion from `Bio.SeqUtils.MeltingTemp.Tm_NN`, use `calc_hairpin`/`calc_homodimer`/`calc_heterodimer` (dG in **cal/mol** — below -9000 cal/mol at 3'-adjacent structures is a common "likely problematic" cutoff), and run `blastn -task blastn-short` against a local db to count near-perfect hits.
```python
import subprocess
import tempfile
import os
import primer3
from Bio.SeqUtils import MeltingTemp as mt
def qc_primer_pair(fwd: str, rev: str, mv_conc: float = 50.0, dna_conc: float = 250.0,
dg_threshold: float = -9000.0) -> dict:
"""QC a primer pair: NN Tm cross-check, hairpin/homodimer/heterodimer risk (dG in cal/mol)."""
fwd_tm = mt.Tm_NN(fwd, Na=mv_conc)
rev_tm = mt.Tm_NN(rev, Na=mv_conc)
hp = [primer3.bindings.calc_hairpin(p, mv_conc=mv_conc, dna_conc=dna_conc) for p in (fwd, rev)]
homo = [primer3.bindings.calc_homodimer(p, mv_conc=mv_conc, dna_conc=dna_conc) for p in (fwd, rev)]
hetero = primer3.bindings.calc_heterodimer(fwd, rev, mv_conc=mv_conc, dna_conc=dna_conc)
hairpin_risk = any(r.structure_found and r.dg < dg_threshold for r in hp)
homodimer_risk = any(r.structure_found and r.dg < dg_threshold for r in homo)
heterodimer_risk = hetero.structure_found and hetero.dg < dg_threshold
return {
'fwd_tm_nn': fwd_tm, 'rev_tm_nn': rev_tm, 'tm_diff': abs(fwd_tm - rev_tm),
'hairpin_risk': hairpin_risk, 'homodimer_risk': homodimer_risk, 'heterodimer_risk': heterodimer_risk,
'flagged': abs(fwd_tm - rev_tm) > 2.0 or hairpin_risk or homodimer_risk or heterodimer_risk,
}
def check_specificity_blastn(primer_seq: str, db_path: str, word_size: int = 7) -> int:
"""Count blastn-short hits for `primer_seq` against a local nucleotide db with
>=90% identity over the full primer length (>1 hit => off-target binding risk).
Requires `makeblastdb -in genome.fa -dbtype nucl -out db_path` beforehand.
"""
with tempfile.NamedTemporaryFile('w', suffix='.fasta', delete=False) as f:
f.write(f'>primer\n{primer_seq}\n')
query_path = f.name
try:
cmd = ['blastn', '-task', 'blastn-short', '-query', query_path, '-db', db_path,
'-word_size', str(word_size), '-perc_identity', '90', '-outfmt', '6 length']
out = subprocess.run(cmd, capture_output=True, text=True, check=True).stdout
return sum(1 for line in out.strip().splitlines() if int(line) >= 0.9 * len(primer_seq))
finally:
os.remove(query_path)
if __name__ == '__main__':
fwd, rev = 'ATGCGTACGATCGATCGATG', 'CATCGATCGATCGTACGCAT' # deliberately near-perfect revcomp -> flags
report = qc_primer_pair(fwd, rev)
assert report['flagged'] is True and report['heterodimer_risk'] is True
print(report)
```
## Pitfalls
- **Primer3 Tm and Biopython Tm rarely match exactly** — different NN parameter sets and salt-correction formulas give Tm within ~1-2C of each other, not identical values; treat them as a sanity cross-check, not a contradiction to chase to zero.
- **`SEQUENCE_TARGET` is not the amplicon** — it marks a region that must fall *inside* the designed product, not the primer binding sites; forgetting to set `PRIMER_PRODUCT_SIZE_RANGE` around it can return primers that never actually flank your region of interest.
- **`dg` from `calc_hairpin`/`calc_heterodimer` is in cal/mol, not kcal/mol** — comparing against a "-9" threshold intended for kcal/mol silently disables the check; always use the cal/mol scale (-9000) shown above.
- **qPCR amplicons that are too long or GC-rich hurt efficiency** — keep amplicons 70-150 bp and avoid designing across a strong secondary-structure region; for RT-qPCR, set `SEQUENCE_TARGET` to force one primer across an exon-exon junction so genomic DNA isn't co-amplified.
- **BLAST specificity checks need `blastn-short`**, not default `blastn` — the default word size (11) misses valid 18-25 bp primer matches, giving false confidence that a primer is unique.
## See Also
- `bio-applied-genetic-engineering-in-silico` — from-scratch Tm models (Wallace, SantaLucia NN) and restriction-site/cloning primer logic without primer3
- `bio-core-blast-searching` — full BLAST+ workflow (local blastdb setup, qblast, E-value/identity parsing) for deeper specificity screening
- `bio-core-biopython-essentials` — `Bio.Seq`/`Bio.SeqUtils` fundamentals used for Tm and reverse-complement operations
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!