pythonforbio.
[WASM idle]
Biopython03/15

Bio.Entrez

Starting sandbox…
Beginnerlesson

Bio.Entrez

In one line: programmatic access to NCBI's databases — set your email, respect the rate limits, and cache everything.

Setup — not optional

from Bio import Entrez

Entrez.email = "you@institution.edu"      # REQUIRED by NCBI's terms
Entrez.tool = "my_analysis_pipeline"       # identifies your software
Entrez.api_key = "your_key_here"           # optional; raises the rate limit

NCBI requires an email so they can contact you before blocking your IP if a script misbehaves. Without it Biopython warns and NCBI may throttle you.

Get an API key. It is free from your NCBI account and raises your limit from 3 to 10 requests per second. Store it in an environment variable, not in the script:

import os
Entrez.api_key = os.environ.get("NCBI_API_KEY")

The main calls

Entrez.esearch(db, term, retmax)    # search → list of IDs
Entrez.efetch(db, id, rettype, retmode)   # retrieve records
Entrez.esummary(db, id)             # brief summaries
Entrez.elink(dbfrom, db, id)        # cross-database links
Entrez.einfo(db)                    # what fields does this database have?
Entrez.epost(db, id)                # upload IDs to the history server
Entrez.read(handle)                 # parse XML → nested dicts/lists

Always close the handle, or use a with block:

with Entrez.efetch(db="nucleotide", id="NM_000546.6",
                   rettype="gb", retmode="text") as h:
    rec = SeqIO.read(h, "genbank")

Common patterns

# search then fetch
with Entrez.esearch(db="nucleotide", term="TP53[Gene] AND human[Organism]",
                    retmax=20) as h:
    ids = Entrez.read(h)["IdList"]

with Entrez.efetch(db="nucleotide", id=",".join(ids),
                   rettype="gb", retmode="text") as h:
    records = list(SeqIO.parse(h, "genbank"))

# one accession as GenBank
with Entrez.efetch(db="nucleotide", id="NM_000546.6",
                   rettype="gb", retmode="text") as h:
    rec = SeqIO.read(h, "genbank")

# protein FASTA
with Entrez.efetch(db="protein", id="NP_000537.3",
                   rettype="fasta", retmode="text") as h:
    prot = SeqIO.read(h, "fasta")

# PubMed abstracts
with Entrez.efetch(db="pubmed", id="12345678", rettype="abstract",
                   retmode="text") as h:
    print(h.read())

# gene record as structured XML
with Entrez.efetch(db="gene", id="7157", retmode="xml") as h:
    gene = Entrez.read(h)

# ClinVar for a gene
with Entrez.esearch(db="clinvar", term="TP53[gene] AND pathogenic",
                    retmax=100) as h:
    ids = Entrez.read(h)["IdList"]

db="gene" with id="7157" uses the Entrez Gene ID, not the symbol. Use esearch on db="gene" to go from symbol to ID.

Batching

Never fetch one ID at a time in a loop.

def fetch_batched(ids, db="nucleotide", rettype="fasta", batch=200):
    for i in range(0, len(ids), batch):
        chunk = ids[i:i + batch]
        with Entrez.efetch(db=db, id=",".join(chunk),
                           rettype=rettype, retmode="text") as h:
            yield from SeqIO.parse(h, rettype)
        time.sleep(0.15)

For very large sets, use the history server so you never send the ID list back:

with Entrez.esearch(db="nucleotide", term=query, usehistory="y") as h:
    r = Entrez.read(h)
count, webenv, key = int(r["Count"]), r["WebEnv"], r["QueryKey"]

for start in range(0, count, 500):
    with Entrez.efetch(db="nucleotide", rettype="fasta", retmode="text",
                       retstart=start, retmax=500,
                       webenv=webenv, query_key=key) as h:
        yield from SeqIO.parse(h, "fasta")

Be a good citizen

  • ≤3 requests/second without an API key, ≤10 with one. Biopython does some throttling; do not rely on it entirely.
  • Bulk downloads outside US peak hours (weekends, or 21:00–06:00 Eastern).
  • For anything genuinely large, use the FTP site (ftp.ncbi.nlm.nih.gov) or the datasets CLI. E-utilities is an API for queries, not a bulk transfer mechanism.
  • Retry with backoff — the service returns transient 500s under load.
import time
from urllib.error import HTTPError

def robust_fetch(fn, *args, retries=4, **kw):
    for attempt in range(retries):
        try:
            return fn(*args, **kw)
        except HTTPError as e:
            if attempt == retries - 1 or e.code < 500:
                raise
            time.sleep(2 ** attempt)

Cache aggressively

Network calls are slow, rate-limited, and not reproducible — the record you fetch today may differ from the one you fetch next year, because sequences get revised.

from pathlib import Path

def cached_genbank(acc, cache_dir="ncbi_cache"):
    cache = Path(cache_dir); cache.mkdir(exist_ok=True)
    path = cache / f"{acc}.gb"
    if not path.exists():
        with Entrez.efetch(db="nucleotide", id=acc,
                           rettype="gb", retmode="text") as h:
            path.write_text(h.read())
    return SeqIO.read(path, "genbank")

Cache the accession with its version (NM_000546.6, not NM_000546). An unversioned accession returns whatever is current, so your pipeline's behaviour changes silently when NCBI updates the record. This is the same reproducibility principle as pinning your UTA snapshot — see UTA and Data Providers.

Security note

Entrez.read parses XML. Biopython 1.87 fixed CVE-2025-68463 in Bio.Entrez.Parser. Keep Biopython up to date, especially if you parse XML from anywhere other than NCBI itself.

Bioinformatics examples

# all RefSeq transcripts for a gene
with Entrez.esearch(db="nucleotide",
                    term="TP53[Gene] AND human[Organism] AND refseq[Filter] AND mRNA[Filter]",
                    retmax=50) as h:
    ids = Entrez.read(h)["IdList"]

# gene → protein via elink
with Entrez.elink(dbfrom="gene", db="protein", id="7157") as h:
    links = Entrez.read(h)
protein_ids = [l["Id"] for l in links[0]["LinkSetDb"][0]["Link"]]

# taxonomy lineage
with Entrez.efetch(db="taxonomy", id="9606", retmode="xml") as h:
    tax = Entrez.read(h)
print(tax[0]["Lineage"])

# assembly summary
with Entrez.esummary(db="assembly", id="GCF_000001405.40") as h:
    print(Entrez.read(h))

# build a local reference set for a gene panel
for gene in PANEL:
    with Entrez.esearch(db="nucleotide",
                        term=f"{gene}[Gene] AND human[Organism] AND refseq_select[Filter]") as h:
        ids = Entrez.read(h)["IdList"]
    if ids:
        rec = cached_genbank(ids[0])
        SeqIO.write(rec, f"refs/{gene}.gb", "genbank")
    time.sleep(0.15)

refseq_select[Filter] restricts to the single representative transcript per gene, which is usually what you want for a panel.

Alternatives

Need Better tool
Bulk genome/annotation download NCBI datasets CLI, or FTP
Ensembl data Ensembl REST API, or pyensembl
UniProt UniProt REST API
Variant annotation Ensembl VEP, ClinVar VCF downloads
Transcript–genome alignments UTA — see UTA and Data Providers
Sequences by accession SeqRepo — see SeqRepo

For variant work specifically, SeqRepo and UTA are far better than Entrez: local, versioned, fast, and reproducible.

Common mistakes

  • Not setting Entrez.email.
  • No API key, so you are limited to 3 req/s.
  • One request per ID in a loop.
  • No caching, making the pipeline slow and irreproducible.
  • Unversioned accessions.
  • No retry logic for transient 500s.
  • Bulk downloading through E-utilities instead of FTP.
  • Not closing handles.
  • Hardcoding the API key in a committed script.

See also

SeqIO · SeqRecord · Bio.Blast · SeqRepo · UTA and Data Providers · Applied - Reproducible Environment

scratch

No output yet — run the code to populate this drawer.

Bio.BlastBio.PDB