diff --git a/docs/src/en/blast.md b/docs/src/en/blast.md
index 414019e25..b35767972 100644
--- a/docs/src/en/blast.md
+++ b/docs/src/en/blast.md
@@ -25,6 +25,12 @@ Limits number of hits to return. Default: 50.
`-e` `--expect`
Defines the [expect value](https://blast.ncbi.nlm.nih.gov/Blast.cgi?CMD=Web&PAGE_TYPE=BlastDocs&DOC_TYPE=FAQ#expect) cutoff. Default: 10.0.
+`-tax` `--taxid`
+One or more NCBI taxonomy IDs to restrict the search to, e.g. `9606` (human) or `txid9606`. On the command line, pass several space-separated IDs (`--taxid 9606 10090`); in Python, pass an `int`/`str` or a list (`taxid=[9606, 10090]`). Multiple IDs are combined with OR. This mirrors the "Organism" field of the [NCBI web BLAST](https://blast.ncbi.nlm.nih.gov/Blast.cgi) app. Default: None.
+
+`-eq` `--entrez_query`
+Raw [NCBI Entrez query](https://www.ncbi.nlm.nih.gov/books/NBK3837/) used to limit the search, e.g. `"Homo sapiens[ORGN] NOT predicted[Title]"`. Combined with `--taxid` using AND when both are provided. Default: None.
+
`-o` `--out`
Path to the file the results will be saved in, e.g. path/to/directory/results.csv (or .json). Default: Standard out.
Python: `save=True` will save the output in the current working directory.
@@ -73,6 +79,22 @@ gget.blast("fasta.fa")
```
→ Returns the BLAST results of the first sequence contained in the fasta.fa file.
+
+**Restrict the search to one or more organisms (taxonomy filtering):**
+```bash
+# Limit to human (taxid 9606)
+gget blast --taxid 9606 MKWMFKEDHSLEHRCVESAKIRAKYPDRVPVIVEKVSGSQIVDIDKRKYLVPSDITVAQFMWIIRKRIQLPSEKAIFLFVDKTVPQSR
+```
+```python
+# Python: limit to human and mouse, excluding predicted records
+gget.blast(
+ "MKWMFKEDHSLEHRCVESAKIRAKYPDRVPVIVEKVSGSQIVDIDKRKYLVPSDITVAQFMWIIRKRIQLPSEKAIFLFVDKTVPQSR",
+ taxid=[9606, 10090],
+ entrez_query="NOT predicted[Title]",
+)
+```
+→ Returns only BLAST hits matching the requested organism(s)/Entrez query, mirroring the "Organism" filter of the NCBI web BLAST app.
+
#### [More examples](https://github.com/pachterlab/gget_examples)
# References
diff --git a/docs/src/en/updates.md b/docs/src/en/updates.md
index 89be22281..4882768b2 100644
--- a/docs/src/en/updates.md
+++ b/docs/src/en/updates.md
@@ -5,6 +5,9 @@
#### *gget* officially became part of [*scverse*](https://scverse.org/) on June 9, 2026. 🥳🥳🥳
**Version ≥ 0.30.9** (XXX XX, 2026):
+- [`gget blast`](blast.md): Added taxonomy/organism filtering via the new `taxid` and `entrez_query` arguments (CLI: `--taxid`/`-tax` and `--entrez_query`/`-eq`), mirroring the "Organism" field of the NCBI web BLAST app (resolves [issue 71](https://github.com/scverse/gget/issues/71)).
+ - `taxid` accepts a single NCBI taxonomy ID or a list (e.g. `9606`, `"txid9606"`, or `[9606, 10090]`); multiple IDs are combined with OR and a `txid[ORGN]` Entrez term is built automatically.
+ - `entrez_query` passes a raw NCBI Entrez query through to BLAST (`ENTREZ_QUERY`); when both are given they are combined with AND. Backward compatible — both default to `None` (no filtering).
**Version ≥ 0.30.8** (Jun 28, 2026):
- [`gget g2p`](g2p.md): Either `gene` or `--uniprot_id` is now sufficient — whichever is missing is resolved via UniProt and cached. Gene→UniProt picks the canonical reviewed human Swiss-Prot entry; the resolution and its limitations are logged. The canonical pair is **always** prepended to the result as `gene_name` / `uniprot_id` columns (and stored on `df.attrs`), so the output schema is invariant regardless of input mode. Existing call sites continue to work.
diff --git a/gget/gget_blast.py b/gget/gget_blast.py
index c072820d3..68620ee5f 100644
--- a/gget/gget_blast.py
+++ b/gget/gget_blast.py
@@ -25,6 +25,47 @@
)
+def _build_entrez_query(
+ taxid: int | str | list[int | str] | None = None,
+ entrez_query: str | None = None,
+) -> str | None:
+ """Build an NCBI Entrez query string to limit a BLAST search by taxonomy.
+
+ Args:
+ - taxid A single NCBI taxonomy ID or a list of taxonomy IDs to
+ restrict the search to (e.g. 9606 or "txid9606" for human).
+ Multiple IDs are combined with OR.
+ - entrez_query A raw NCBI Entrez query string passed through as-is
+ (e.g. "Homo sapiens[ORGN] NOT predicted[Title]").
+
+ When both are provided they are combined with AND. Returns None if neither
+ is provided. Raises ValueError if a taxid is not a valid (numeric) ID.
+ """
+ entrez_terms = []
+
+ if taxid is not None:
+ # Accept a single taxid or an iterable of taxids
+ taxid_list = list(taxid) if isinstance(taxid, (list, tuple, set)) else [taxid]
+ taxid_terms = []
+ for tid in taxid_list:
+ tid_str = str(tid).strip().lower().removeprefix("txid")
+ if not tid_str.isdigit():
+ raise ValueError(
+ f"Invalid taxid {tid!r}. Expected a numeric NCBI taxonomy ID (e.g. 9606 or 'txid9606')."
+ )
+ taxid_terms.append(f"txid{tid_str}[ORGN]")
+
+ if len(taxid_terms) == 1:
+ entrez_terms.append(taxid_terms[0])
+ else:
+ entrez_terms.append("(" + " OR ".join(taxid_terms) + ")")
+
+ if entrez_query is not None and str(entrez_query).strip() != "":
+ entrez_terms.append(str(entrez_query).strip())
+
+ return " AND ".join(entrez_terms) if entrez_terms else None
+
+
@overload
def blast(
sequence: str,
@@ -39,6 +80,8 @@ def blast(
*,
json: Literal[True],
save: bool = False,
+ entrez_query: str | None = None,
+ taxid: int | str | list[int | str] | None = None,
) -> list[dict[str, Any]] | None: ...
@@ -55,6 +98,8 @@ def blast(
wrap_text: bool = False,
json: Literal[False] = False,
save: bool = False,
+ entrez_query: str | None = None,
+ taxid: int | str | list[int | str] | None = None,
) -> pd.DataFrame | None: ...
@@ -70,6 +115,8 @@ def blast(
wrap_text: bool = False,
json: bool = False,
save: bool = False,
+ entrez_query: str | None = None,
+ taxid: int | str | list[int | str] | None = None,
) -> pd.DataFrame | list[dict[str, Any]] | None:
"""BLAST a nucleotide or amino acid sequence against any BLAST DB.
@@ -89,6 +136,12 @@ def blast(
- wrap_text If True, displays data frame with wrapped text for easy reading. Default: False.
- json If True, returns results in json format instead of data frame. Default: False.
- save If True, the data frame is saved as a csv in the current directory (default: False).
+ - entrez_query Raw NCBI Entrez query string used to limit the BLAST search
+ (e.g. "Homo sapiens[ORGN] NOT predicted[Title]"). Default: None.
+ - taxid NCBI taxonomy ID, or list of taxonomy IDs, to restrict the search to
+ (e.g. 9606 or "txid9606" for human; ["9606", "10090"] for human and mouse).
+ Multiple IDs are combined with OR. Combined with 'entrez_query' using AND
+ when both are provided. Default: None.
- verbose True/False whether to print progress information. Default True.
Returns a data frame with the BLAST results.
@@ -230,6 +283,12 @@ def blast(
else:
megablast = "on"
+ ## Build Entrez query for taxonomy / organism filtering (issue #71)
+ # Validates taxid(s) and combines with any raw entrez_query before submission.
+ entrez_query_final = _build_entrez_query(taxid, entrez_query)
+ if entrez_query_final is not None and verbose:
+ logger.info(f"Limiting BLAST search to Entrez query: {entrez_query_final}")
+
## Submit search
# The following code was partly adapted from the Biopython BLAST NCBIWWW project written
# by Jeffrey Chang (Copyright 1999), Brad Chapman, and Chris Wroe distributed under the
@@ -247,6 +306,7 @@ def blast(
("EXPECT", expect),
("FILTER", low_comp_filt),
("MEGABLAST", megablast),
+ ("ENTREZ_QUERY", entrez_query_final),
("CMD", "Put"),
]
diff --git a/gget/main.py b/gget/main.py
index 7a2944b09..1bd317b7a 100644
--- a/gget/main.py
+++ b/gget/main.py
@@ -892,6 +892,29 @@ def main() -> None:
required=False,
help="Returns results in csv format instead of json.",
)
+ parser_blast.add_argument(
+ "-tax",
+ "--taxid",
+ type=str,
+ nargs="+",
+ default=None,
+ required=False,
+ help=(
+ "One or more NCBI taxonomy IDs to restrict the search to, e.g. 9606 (human) "
+ "or 'txid9606'. Multiple IDs (space-separated) are combined with OR."
+ ),
+ )
+ parser_blast.add_argument(
+ "-eq",
+ "--entrez_query",
+ type=str,
+ default=None,
+ required=False,
+ help=(
+ 'Raw NCBI Entrez query to limit the search, e.g. "Homo sapiens[ORGN] NOT predicted[Title]". '
+ "Combined with --taxid using AND when both are provided."
+ ),
+ )
parser_blast.add_argument(
"-o",
"--out",
@@ -3111,6 +3134,8 @@ def main() -> None:
megablast=args.megablast_off,
verbose=args.quiet,
json=args.csv,
+ entrez_query=args.entrez_query,
+ taxid=args.taxid,
)
# Check if the function returned something
diff --git a/tests/fixtures/test_blast.json b/tests/fixtures/test_blast.json
index 4c2e3292f..a1955fb26 100644
--- a/tests/fixtures/test_blast.json
+++ b/tests/fixtures/test_blast.json
@@ -70,5 +70,15 @@
"database": "banana"
},
"expected_result": "ValueError"
+ },
+ "test_blast_bad_taxid": {
+ "type": "error",
+ "args": {
+ "sequence": "MSKGEELFTGVVPILVELDGDVNGQKFSVSGEGEGDATYGKL",
+ "limit": 1,
+ "taxid": "banana"
+ },
+ "expected_result": "ValueError",
+ "expected_msg": "Invalid taxid 'banana'. Expected a numeric NCBI taxonomy ID (e.g. 9606 or 'txid9606')."
}
}
diff --git a/tests/test_blast.py b/tests/test_blast.py
index 8fea295e6..0f7bbf7e0 100644
--- a/tests/test_blast.py
+++ b/tests/test_blast.py
@@ -1,7 +1,7 @@
import json
import unittest
-from gget.gget_blast import blast
+from gget.gget_blast import _build_entrez_query, blast
from .from_json import from_json
@@ -12,3 +12,44 @@
class TestBlast(unittest.TestCase, metaclass=from_json(blast_dict, blast)):
pass # all tests are loaded from json
+
+
+class TestBuildEntrezQuery(unittest.TestCase):
+ """Network-free tests for the taxonomy/Entrez query builder (issue #71)."""
+
+ def test_none(self):
+ self.assertIsNone(_build_entrez_query())
+ self.assertIsNone(_build_entrez_query(taxid=None, entrez_query=None))
+ self.assertIsNone(_build_entrez_query(entrez_query=" "))
+
+ def test_single_taxid_int(self):
+ self.assertEqual(_build_entrez_query(taxid=9606), "txid9606[ORGN]")
+
+ def test_single_taxid_str_and_prefix(self):
+ self.assertEqual(_build_entrez_query(taxid="9606"), "txid9606[ORGN]")
+ self.assertEqual(_build_entrez_query(taxid="txid9606"), "txid9606[ORGN]")
+ self.assertEqual(_build_entrez_query(taxid="TXID9606"), "txid9606[ORGN]")
+
+ def test_multiple_taxids_or(self):
+ self.assertEqual(
+ _build_entrez_query(taxid=[9606, "10090"]),
+ "(txid9606[ORGN] OR txid10090[ORGN])",
+ )
+
+ def test_entrez_query_only(self):
+ self.assertEqual(
+ _build_entrez_query(entrez_query="Homo sapiens[ORGN]"),
+ "Homo sapiens[ORGN]",
+ )
+
+ def test_taxid_and_entrez_query_combined(self):
+ self.assertEqual(
+ _build_entrez_query(taxid=9606, entrez_query="NOT predicted[Title]"),
+ "txid9606[ORGN] AND NOT predicted[Title]",
+ )
+
+ def test_invalid_taxid_raises(self):
+ with self.assertRaises(ValueError):
+ _build_entrez_query(taxid="banana")
+ with self.assertRaises(ValueError):
+ _build_entrez_query(taxid=[9606, "banana"])