# Troubleshooting Common BLAST Parsing Errors in Python

The single most common cause of a BLAST XML parse error in Python is a truncated or contaminated XML file. The BLAST+ command line tools and the Biopython parsers are both stable and well tested. The file sitting between them is usually the problem. If your script dies with `ExpatError: no element found`, an `xml.etree.ElementTree.ParseError`, or a similar message from an XML library, the first thing to do is look at the last few bytes of the file and check whether it ends with a closing `</BlastOutput>` tag. Nine times out of ten it does not, because the BLAST process was killed before it finished writing, or because error text was redirected into the same file as the results.

This guide walks through the full range of failures you will meet when parsing BLAST output in Python, ordered from the most frequent cause to the rarest. Each section explains the mechanism behind the failure, how to confirm it, and how to fix it. The core of the article is a troubleshooting table that maps symptoms to causes, followed by a diagnostic sequence you can run in about ten minutes. Anyone who has tried to process a large BLAST search by hand knows how tedious it becomes, which is why purpose-built parsing tools and scripts are a standard part of bioinformatics workflows [1]. When the parsed results feed downstream steps such as reciprocal best hit searches or annotation pipelines, a broken parse fails loudly and everything after it stops, so it pays to fix the parse properly rather than patching around it [2].

## How BLAST XML and Python Meet

XML, the Extensible Markup Language, is a text format that wraps data in nested tags such as `<Iteration>` and `<Hit>`. BLAST+ writes XML when you pass `-outfmt 5` on the command line. The National Center for Biotechnology Information (NCBI) maintains the XML schema that the BLAST+ suite emits, and the format is designed to carry every field a graphical interface needs: query deflines, hit deflines, HSPs (high-scoring segment pairs, the individual aligned regions), bit scores, e-values, and alignment coordinates.

Biopython is the standard Python library for sequence work, and it ships two interfaces for BLAST results:

1. **Bio.SearchIO**, the current object model, added in Biopython 1.68. It reads both XML (`blast-xml`) and tabular (`blast-tab`) output and exposes a uniform interface across formats.
2. **Bio.Blast.NCBIXML**, the older parser tied directly to the legacy XML DTD (Document Type Definition, the formal description of which tags are allowed where). Recent Biopython releases have deprecated this module in favor of SearchIO, so new code should use SearchIO, and older scripts should be migrated when convenient.

XML is strict. A parser must reject a document that is not well formed, meaning every tag opened must be closed, tags must nest correctly, and special characters must be escaped. This strictness is a feature: it prevents you from silently analyzing half a result file. It also means that a single byte of stray output at the start of the file, or a missing closing tag at the end, stops the entire parse. Tab-separated output does not behave this way, which is one reason tabular output makes a good fallback format.

## Step One: Is the File Complete?

Before opening Python, inspect the file itself. This single check resolves most reported BLAST XML parse errors:

```bash
ls -lh results.xml
head -c 200 results.xml
tail -c 200 results.xml
```

A healthy BLAST XML file starts with an XML declaration such as `<?xml version="1.0"?>` followed by a DOCTYPE line referencing the NCBI BLAST DTD, and ends with:

```xml
</BlastOutput>
</BlastXML>
```

Wait, that second tag needs a caveat. Depending on the BLAST+ version, the document may close with `</BlastOutput>` alone or with a wrapping element after it. The reliable check is not the exact tag sequence but the shape: the last non-whitespace content should be a closing tag, never a partial line such as `<Hsp_midline>CTG` or a raw `Segmentation fault` message. If the file ends mid-tag, the write was interrupted and no Python parsing trick will rescue it. Re-run the search.

## A Ten-Minute Diagnostic Sequence

![Decision tree that sorts a BLAST XML parse failure into empty file, contaminated stream, truncation or API misuse.](/article-images/illustrations/blast-xml-parse-error-python-biopython-triage-branches.webp)

Run these checks in order. Stop at the first one that fails, because that failure is almost always your root cause.

1. **Check the file exists and has plausible size.** An empty file or a file of zero bytes means the BLAST command never wrote output. A few hundred bytes for a run you expected to produce megabytes suggests an early crash or an empty query set.
2. **Look at the first line.** It should begin with `<?xml`. If it begins with text such as `Searching [blastn] ...` or a shell error, your output stream is contaminated.
3. **Look at the last line.** It should be a closing tag. A mid-tag ending means truncation.
4. **Validate the whole document.** Run `xmllint, noout results.xml` (or `python -c "import xml.etree.ElementTree as ET; ET.parse('results.xml')"` if xmllint is not installed). This pinpoints the exact line and column of the first malformation, which turns a vague parser error into a specific diagnosis.
5. **Count the queries.** Run `grep -c "<Iteration>" results.xml`. Compare that number to the number of sequences in your query FASTA file. A mismatch tells you whether the file was cut short partway through the query list.
6. **Check the exit code of the original BLAST command.** If you still have the job in a pipeline or scheduler log, look for a nonzero exit status. A status of 137, for example, typically means the process was killed by the operating system, often for exceeding memory limits.
7. **Record your tool versions.** Run `blastn -version` and `python -c "import Bio; print(Bio.__version__)"`. Version mismatches between what produced the XML and what parsed it are a real class of failure, covered below.

Once you know which of these checks failed, the matching row in the table below gives you the fix.

## Troubleshooting Table

| Symptom | Likely cause | How to confirm | Fix |
|--|--|--|--|
| `ExpatError: no element found`, or an `ElementTree.ParseError` at the final line | File truncated, BLAST killed mid-write | Last bytes lack a closing tag, `echo $?` after BLAST is nonzero, exit code 137 or 143 | Re-run BLAST with stderr separated, verify exit code 0, request more memory or runtime from the scheduler |
| Parse error at line 1 with text fragments such as `Searching` or `Building a new DB` in the file | Standard error and standard output were combined, usually with `&>` or `2>&1` into one file | First line is not `<?xml`, error text interleaved among tags | Re-run with `-out results.xml 2> blast.err` so the streams are separate |
| `ExpatError` at line 1, column 0, on a file you are sure you downloaded correctly | The file is still gzip-compressed, or you fetched an HTML login or error page instead of XML | `file results.xml` reports gzip or HTML data | Decompress with `gunzip` before parsing, or re-download with the correct link or API call |
| `ValueError: Multiple records found in single input handle` | You called `NCBIXML.read()` or an equivalent single-record reader on a multi-query XML file | `grep -c "<Iteration>"` returns more than one, or the FASTA had multiple queries | Use the iterating parser: `SearchIO.parse()` or `NCBIXML.parse()` |
| Records parse but appear empty, with no hits where you expected hits | The search genuinely returned nothing above the expect value cutoff, or hit filtering limited the output | Open the raw XML and search for `<Hit>` inside the problem `<Iteration>`, or check the `<Statistics>` node | Lower the e-value threshold with `-evalue`, confirm the database is correct, confirm the query is not reverse-complemented relative to a [protein database](/blog/guides/protein-database) |
| `AttributeError` such as missing `descriptions` or unexpected object types after parsing | Mixing the old `NCBIXML` object model with new `SearchIO` code, or vice versa | Print `type(result)` and compare against the API you are coding against | Pick one API per script. SearchIO objects expose `description`, `id`, and `hits`. NCBIXML record objects expose `descriptions` and `alignments` |
| A `BiopythonParserWarning` or deprecation warning appears, then parsing proceeds | Using the deprecated `Bio.Blast.NCBIXML` module on a modern Biopython install | The warning names the module and recommends SearchIO | Migrate to `from Bio import SearchIO`. Until migration, the warning is informational, not fatal |
| `MemoryError` or the parser process is killed on a large XML file | Loading the entire document tree into RAM at once | File size in gigabytes, small machine memory, no error until deep into the parse | Stream records one query at a time with `SearchIO.parse()`, or switch to tabular output and stream lines |
| Parse works, but numeric fields are wrong or shifted in tabular mode | Column order mismatch between the `-outfmt 6` specification and your unpacking code | Compare `head -1` of the file against your expected fields | Add `-outfmt "6 qseqid sseqid pident length ..."`, listing explicit column names, and unpack by name |
| Intermittent failures across runs on identical input | Race condition: a downstream step started before BLAST finished, or the file was read while being written | Timestamps show the parser started before the writer exited | Enforce dependencies in your workflow manager, or add an explicit wait and completion check |

## Cause 1: Truncated Output, the Dominant Failure

BLAST writes XML incrementally as it processes queries. If the process dies partway through, the file on disk ends wherever the last successful write landed. The XML is then incomplete by definition, because some element opened earlier never closed. Every standards-compliant XML parser will reject it, and the error message will point at the end of the file, which confuses people who assume the problem is at the line number reported. The line number is where the parser gave up, not where the file went wrong. The file went wrong by simply stopping early.

Truncation has several common mechanisms:

- **Job scheduler limits.** On shared clusters, a job that exceeds its requested wall time or memory gets killed. The output file keeps whatever was flushed before the kill.
- **Disk quota.** A full quota or partition stops writes silently from the user's perspective while BLAST continues running and then aborts.
- **Pipes into other commands.** A command such as `blastn ... | head -100` closes the read end after 100 lines, BLAST receives a broken pipe signal when it next writes, and the process terminates early. The same happens with poorly configured downstream consumers.
- **Manual interruption** or a terminal session dropped over SSH without a terminal multiplexer such as tmux or screen.

The fix is always to produce a complete file before parsing. Run BLAST writing directly to a file with `-out`, capture stderr separately, check the exit code, and only then hand the file to Python:

```bash
blastn -query queries.fna -db nr -outfmt 5 -out results.xml 2> blast.err
status=$?
if [ $status -ne 0 ]; then
  echo "BLAST failed with exit code $status, see blast.err"
  exit 1
fi
```

A short operational note applies here. Large BLAST database searches can consume substantial scratch disk and scheduler resources, and a killed job can leave partial files that look valid at a glance. On shared systems, request realistic time and memory limits, and never treat the presence of an output file as proof that the run finished. Verify completion before analysis.

## Cause 2: Standard Error Contaminating the Output

Unix processes write to two separate streams: standard output (stdout) for results and standard error (stderr) for diagnostics. BLAST+ prints progress messages such as `Searching [blastn] ...` and database construction notices to stderr. If you redirect both into one file, using shell syntax like `&>` or `2>&1` before the output redirection, those messages land inside your XML. The XML then has arbitrary text where the parser expects only tags, and the first malformed token raises a parse error at or near the beginning of the file.

This is the second most common cause of a BLAST XML parse error in Python, and it is entirely preventable at the command line:

```bash
# Correct: results to a file, diagnostics to a separate file
blastn -query queries.fna -db refseq_rna -outfmt 5 -out results.xml 2> blast.err

# Wrong: both streams interleaved into one file
blastn -query queries.fna -db refseq_rna -outfmt 5 &> results.xml
```

The wrong version may even appear to work sometimes, because stderr writes are infrequent for fast searches, and interleaving is timing dependent. That intermittent behavior makes this bug particularly annoying in pipelines that mostly work. The confirmation is simple: `head -1 results.xml` shows anything other than the XML declaration. The fix is to re-run with separated streams, and to encode the separation in your workflow definition so it cannot regress.

## Cause 3: Empty or Nearly Empty Files

An empty file raises the same `no element found` error as a truncated one, but the mechanism differs. Common origins:

- The query FASTA file was empty, perhaps because an upstream step that should have written it silently failed.
- The database name was misspelled, and BLAST printed an error to stderr while writing nothing to stdout. Check `blast.err` and the exit code.
- The command ran against the wrong working directory, so a stale empty file from a previous attempt was parsed.
- You downloaded results from a web BLAST session after the session expired, and the server returned a small HTML page instead.

Confirmation is a one-liner: `wc -c results.xml`. Zero or very small file sizes, combined with a non-XML first line, identify this group. The fix lies upstream of Python: repair the query production step, correct the database identifier, or re-request the download.

## Cause 4: One Query or Many, `read` Versus `parse`

Biopython provides single-record and multi-record readers, and choosing the wrong one produces an error that looks like a parse failure but is actually an API misuse. With the legacy interface, `NCBIXML.read(handle)` expects exactly one BLAST result. When your XML contains results for a multi-sequence query file, each query generates its own `<Iteration>`, and the reader raises `ValueError: Multiple records found in single input handle`.

The equivalent mistake in the other direction produces `AttributeError` messages. `NCBIXML.parse()` returns an iterator of records, so calling attribute access directly on the iterator rather than on a record fails. With SearchIO, `SearchIO.parse()` is the multi-record reader and `SearchIO.read()` is the single-record reader, and `SearchIO.read()` raises an exception if the source contains more than one result.

The rule is easy to remember. If your query FASTA has one sequence and you ran one search, use `read`. If your FASTA has multiple sequences, use `parse` and iterate. A parsing template later in this article shows the correct loop.

## Cause 5: Version Drift Between BLAST+ and Your Parser

The BLAST XML format evolves. NCBI has added fields over successive BLAST+ releases, and Biopython's parsers are written against a specific understanding of the format. Two failure modes follow:

- **New BLAST, old parser.** A parser built for an older DTD may not know about newly introduced elements. Usually unknown elements are ignored harmlessly, but occasionally a structural change, such as a new nesting level, breaks extraction and yields missing attributes or empty result objects even though the file is well formed.
- **Old BLAST, new parser.** Rare, but possible when very old XML files from an archive are parsed with current code.

The practical response is to record both versions with every result file and to pin versions in reproducible environments. Add `blastn -version` output to your run log, and record the Biopython version in your environment file. When you upgrade either tool, re-validate one known-good result file end to end before trusting the pipeline.

There is a second version-related issue on the Python side. As noted above, `Bio.Blast.NCBIXML` has been deprecated in recent Biopython releases in favor of `Bio.SearchIO`. Scripts written years ago still run but emit warnings, and future removal would break them outright. Migration is usually mechanical, and SearchIO also gives you format-independent code that works unchanged if you switch to tabular output later.

## Cause 6: Non-XML Content and Encoding Problems

Three sub-cases hide under this heading.

**Compressed input.** If the result file was compressed at any point, for example by a pipeline that gzips intermediates, and you hand the gzipped bytes to a text-mode parser, you get a decode error long before any XML logic runs. `file results.xml` settles the question instantly. Decompress, then parse.

**HTML instead of XML.** Scripts that scrape web BLAST interfaces occasionally capture a login page, a rate-limit notice, or a session-expired page. HTML is not well-formed XML in general, so the parser rejects it. The telltale sign is an error near the top of the file combined with a first line containing `<!DOCTYPE html>` or `<html>`. The reliable alternative to scraping is programmatic access through the NCBI URL API, which is the route NCBI documents for automated retrieval, but note that heavy automated querying of public web services is subject to usage policies. For serious work, a local BLAST+ installation with local databases avoids the entire class of problems.

**Unescaped characters in deflines.** User-supplied FASTA headers can contain characters that break naive XML generation. The BLAST+ writers escape these correctly, so this is rare with current tools, but files generated by older or third-party writers occasionally contain raw ampersands in deflines. `xmllint, noout` identifies the offending line precisely, and pre-processing the deflines in the query FASTA removes the problem at its source.

## Cause 7: Memory Exhaustion on Large Result Files

An all-against-all comparison of two large sequence sets produces XML measured in gigabytes. A parser that builds a complete in-memory tree of the document holds every hit, every HSP, and every copy of every alignment string in RAM simultaneously. On a machine with modest memory the process is killed by the operating system, which looks superficially like the truncation case, except here the victim is the parser rather than BLAST itself.

The fix is streaming. `SearchIO.parse()` reads one query result at a time, so peak memory stays proportional to the largest single query's hits rather than the whole file:

```python
from Bio import SearchIO

for result in SearchIO.parse("results.xml", "blast-xml"):
    for hit in result.hits:
        for hsp in hit.hsps:
            print(result.id, hit.id, hsp.evalue)
```

Never accumulate every parsed record in a list before processing unless you truly need random access to the entire result set. If you do need iterative filtering across records, the established pattern is to store distilled results rather than raw parser objects. The PARIGA server took this approach for reciprocal BLAST analyses, holding parsed BLAST output in a Python-serialized object database so that filtering operations, such as finding reciprocal best hits between two datasets, could run repeatedly without re-parsing the raw files [2]. The general lesson transfers to any pipeline: parse once, store the small distilled table, and operate on the distilled table thereafter.

Runtime is the other practical concern at scale. Parsing overhead is real, and dedicated tools built for throughput have been motivated partly by the cost of generic parser implementations. The BLAST-QC project reported better runtimes for its C and Java implementations than comparable parsers built on BioPerl or Biopython modules, reflecting the general tradeoff between convenience and raw speed in this layer of a workflow [1]. For most analyses, Biopython's convenience wins. For files measured in tens of gigabytes processed routinely, streaming plus tabular output, discussed next, closes most of the gap without leaving Python.

## Cause 8: Logic Errors That Masquerade as Parse Errors

Some complaints that arrive described as "BLAST XML parse error python biopython" are actually correct parses followed by wrong assumptions.

**Fewer hits than expected.** BLAST applies an expect value cutoff, 10 by default, and reports only hits passing it. A query against a distant database can legitimately return zero hits, and BLAST still writes a well-formed `<Iteration>` with no `<Hit>` children. Users sometimes read this as a parsing failure. Confirm by opening the raw XML at the offending query and checking whether `<Hit>` elements exist. Adjust `-evalue`, or reconsider the database and strand assumptions.

**Hit counts lower than the web interface.** The command line `-max_target_seqs` option interacts with the BLAST heuristic search process in ways that have been discussed extensively in the community, and its use is considered questionable for controlling the number of reported hits without affecting the search itself [1]. If you are comparing command-line hit counts against web output or older runs, parameter differences, particularly around this option, are a more likely explanation than a parser defect.

**Attribute assumptions.** NCBIXML record objects expose a list of `descriptions` and a parallel list of `alignments`. SearchIO result objects expose a `hits` collection where each hit carries `hsps`. Mixing vocabulary from tutorials written for the other API produces `AttributeError` or `KeyError` immediately after a successful parse. Print one record interactively, inspect its actual structure, and write against what you see.

## A Robust Parsing Template

The following script bundles the defensive practices described above. It validates the file, streams records, handles both zero-hit queries and structural surprises, and reports failures clearly:

```python
#!/usr/bin/env python3
"""Stream BLAST XML results safely with Biopython SearchIO."""

import os
import sys
from Bio import SearchIO
from Bio.File import as_handle

def validate_xml(path):
    """Fail fast on truncation before the main parse begins."""
    with open(path, "rb") as fh:
        fh.seek(0, os.SEEK_END)
        size = fh.tell()
        if size == 0:
            sys.exit(f"{path} is empty. Did BLAST actually run?")
        fh.seek(max(0, size - 200))
        tail = fh.read().decode("utf-8", errors="replace").strip()
        if not tail.endswith(">"):
            sys.exit(
                f"{path} appears truncated. Last bytes: {tail[-80:]!r}. "
                "Re-run BLAST and verify its exit code."
            )

def main(xml_path):
    validate_xml(xml_path)
    n_queries = n_hits = 0
    try:
        with as_handle(xml_path) as handle:
            for result in SearchIO.parse(handle, "blast-xml"):
                n_queries += 1
                hits = list(result.hits)
                n_hits += len(hits)
                if not hits:
                    print(f"no hits: {result.id}", file=sys.stderr)
                    continue
                top = hits
                best = min(top.hsps, key=lambda h: h.evalue)
                print(f"{result.id}\t{top.id}\t{best.evalue:.3g}")
    except Exception as exc:
        sys.exit(f"parse failed after {n_queries} queries: {exc}")
    print(f"parsed {n_queries} queries, {n_hits} hits", file=sys.stderr)

if __name__ == "__main__":
    main(sys.argv[1])
```

Points worth internalizing from this template:

- Validation happens before parsing, so a truncated file produces a clear message instead of a stack trace from an XML library.
- Records are streamed, so memory stays bounded.
- Queries with no hits are logged, not silently skipped, so you can distinguish a weak search from a broken file.
- A single summary line to stderr confirms the parse covered every query, which you can cross-check against `grep -c "<Iteration>"`.

The `tail.endswith(">")` heuristic is deliberately loose. It catches the mid-tag truncation that causes most `no element found` errors without making brittle assumptions about the exact closing tag sequence of a particular BLAST+ version.

## When to Skip XML Entirely: Tabular Output

![Side by side comparison of BLAST XML and tabular output on truncation, memory, alignment strings and column handling.](/article-images/illustrations/blast-xml-parse-error-python-biopython-xml-vs-tabular.webp)

For many pipelines, XML is more format than you need. BLAST's tabular mode, selected with `-outfmt 6`, emits one line per HSP with named columns you choose explicitly:

```bash
blastn -query queries.fna -db nr \
  -outfmt "6 qseqid sseqid pident length mismatch gapopen qstart qend sstart send evalue bitscore" \
  -out hits.tsv
```

Reading this in Python needs no XML machinery at all:

```python
import csv

with open("hits.tsv") as fh:
    for row in csv.reader(fh, delimiter="\t"):
        qseqid, sseqid, pident, evalue = row, row[1], row[2], row
```

Tabular output is line-oriented, so a truncated file loses only its last line, streaming is trivial, and memory use is negligible. Two caveats keep this honest. First, tabular output omits alignment strings and some annotation fields, so if you need the actual aligned sequences, stay with XML. Second, tabular output is subject to the same search-side parameters as XML, so the `-max_target_seqs` considerations noted above apply identically [1]. Third, always specify columns explicitly rather than relying on the default column order, because relying on implicit order is how shifted-column bugs are born.

A sensible default for new pipelines is tabular for filtering and scoring work, XML only when alignment strings or interface-level detail are required.

## Prevention Practices for BLAST Parsing in Python

Failures cluster into recognizable patterns, and a few habits prevent nearly all of them:

1. **Separate streams at the command line, always.** `-out results.xml 2> blast.err` is a two-word change that eliminates the second most common parse error class.
2. **Gate parsing on exit codes.** In shell, check `$?` after BLAST. In workflow managers such as Snakemake or Nextflow, declare the BLAST step as an explicit dependency of the parsing step so the parser cannot start early.
3. **Validate before parsing.** A byte-size check plus a tail check costs milliseconds and turns the most common failure into a clear diagnostic.
4. **Pin versions.** Record BLAST+ and Biopython versions in logs and environment files. Re-validate one known-good file after any upgrade.
5. **Stream, do not load.** Use `SearchIO.parse()` in a loop. Reserve whole-file strategies for files you know are small.
6. **Count queries and reconcile.** Compare the number of `<Iteration>` elements against the query FASTA sequence count as an automated integrity check.
7. **Store distilled results.** Persist a compact table of the fields downstream steps need, in the spirit of tools that keep parsed results in serialized form for repeated filtering without re-parsing [2]. This also future-proofs the pipeline against parser API changes.
8. **Migrate off deprecated parsers.** Move legacy `NCBIXML` scripts to `Bio.SearchIO` during ordinary maintenance, not during an outage.

## Common Mistakes and Limitations

- **Reading the error line number literally.** The line and column in an XML parse error mark where the parser gave up. For truncation, that is the end of the file. The defect may be a missing closing tag for an element opened thousands of lines earlier.
- **Assuming a present file means a complete file.** Partial writes produce files that exist, have plausible names, and cannot be parsed. Completion must be verified, not assumed.
- **Combining streams for convenience.** `&>` saves typing and destroys XML reproducibly. The intermittent nature of the corruption makes it worse, because the pipeline works on small tests and fails on the real run.
- **Diagnosing hit-count questions as parser bugs.** If the parse succeeds and the numbers look wrong, the search parameters are the place to look. Hit reporting behavior depends on parameters such as `-max_target_seqs` in ways that are actively debated in the community [1].
- **Mixing parser APIs in one script.** Copying a snippet from an old NCBIXML tutorial into a SearchIO script produces attribute errors that look like parsing problems. Pick one API and read its objects interactively before writing extraction code.
- **Keeping results in raw XML long term.** Re-parsing gigabyte XML files on every analysis pass wastes time and re-exposes you to every failure mode above. Parse once, store the distilled table, and treat the raw XML as an archive.
- **Over-trusting downloaded result files.** Files from web interfaces can be HTML error pages or expired-session responses. Check the first line before parsing anything fetched over HTTP.
- **Individual research cases vary.** The right thresholds, databases, and output formats depend on your specific biological question, and no general troubleshooting guide substitutes for evaluating your own search parameters against your own data.

## Frequently Asked Questions

### What does "ExpatError: no element found" mean in Biopython?

It means the XML parser reached the end of the file before the document was complete, which almost always indicates a truncated or empty BLAST output file. Check the last bytes of the file for a closing tag and re-run BLAST if the file ends mid-element.

### How do I fix "ValueError: Multiple records found in single input handle"?

You called a single-record reader such as `NCBIXML.read()` on a file containing results for multiple queries. Switch to the iterating reader, `SearchIO.parse()` or `NCBIXML.parse()`, and loop over the records.

### Should I use NCBIXML or Bio.SearchIO for BLAST XML?

Use Bio.SearchIO for new code. It is the current Biopython interface, it handles both XML and tabular formats with one API, and NCBIXML has been deprecated in recent Biopython releases.

### Why is my BLAST XML output file empty?

The most common reasons are an empty query FASTA, a misspelled database name, or an error that BLAST printed to stderr while writing nothing to stdout. Check the file size, the stderr log, and the BLAST exit code.

### Can I parse BLAST output without Biopython?

Yes. Python's built-in `xml.etree.ElementTree` or `lxml` can parse BLAST XML, and tabular output can be read with the `csv` module or plain string splitting. Biopython simply packages the object model so you do not reimplement field mapping.

### How do I get BLAST to write XML from the command line?

Pass `-outfmt 5` and an output file: `blastn -query queries.fna -db mydb -outfmt 5 -out results.xml`. Redirect stderr to a separate file so progress messages cannot contaminate the XML.

### Why does my parser report fewer hits than the BLAST website?

Command-line and web searches can differ in parameters, and options such as `-max_target_seqs` interact with BLAST's heuristics in ways that affect reported results. Compare your command line against the web parameters before suspecting the parser.

### How do I handle very large BLAST XML files without running out of memory?

Stream one query at a time with `SearchIO.parse()` instead of loading the whole document, and consider tabular `-outfmt 6` output for scoring and filtering tasks since line-oriented parsing keeps memory use minimal.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What does \"ExpatError: no element found\" mean in Biopython?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "It means the XML parser reached the end of the file before the document was complete, which almost always indicates a truncated or empty BLAST output file. Check the last bytes of the file for a closing tag and re-run BLAST if the file ends mid-element."
      }
    },
    {
      "@type": "Question",
      "name": "How do I fix \"ValueError: Multiple records found in single input handle\"?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "You called a single-record reader such as NCBIXML.read() on a file containing results for multiple queries. Switch to the iterating reader, SearchIO.parse() or NCBIXML.parse(), and loop over the records."
      }
    },
    {
      "@type": "Question",
      "name": "Should I use NCBIXML or Bio.SearchIO for BLAST XML?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Use Bio.SearchIO for new code. It is the current Biopython interface, it handles both XML and tabular formats with one API, and NCBIXML has been deprecated in recent Biopython releases."
      }
    },
    {
      "@type": "Question",
      "name": "Why is my BLAST XML output file empty?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "The most common reasons are an empty query FASTA, a misspelled database name, or an error that BLAST printed to stderr while writing nothing to stdout. Check the file size, the stderr log, and the BLAST exit code."
      }
    },
    {
      "@type": "Question",
      "name": "Can I parse BLAST output without Biopython?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. Python's built-in xml.etree.ElementTree or lxml can parse BLAST XML, and tabular output can be read with the csv module or plain string splitting. Biopython simply packages the object model so you do not reimplement field mapping."
      }
    },
    {
      "@type": "Question",
      "name": "How do I get BLAST to write XML from the command line?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Pass -outfmt 5 and an output file: blastn -query queries.fna -db mydb -outfmt 5 -out results.xml. Redirect stderr to a separate file so progress messages cannot contaminate the XML."
      }
    },
    {
      "@type": "Question",
      "name": "Why does my parser report fewer hits than the BLAST website?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Command-line and web searches can differ in parameters, and options such as -max_target_seqs interact with BLAST's heuristics in ways that affect reported results. Compare your command line against the web parameters before suspecting the parser."
      }
    },
    {
      "@type": "Question",
      "name": "How do I handle very large BLAST XML files without running out of memory?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Stream one query at a time with SearchIO.parse() instead of loading the whole document, and consider tabular -outfmt 6 output for scoring and filtering tasks since line-oriented parsing keeps memory use minimal."
      }
    }
  ]
}
</script>

## Sources

1. [BLAST-QC: automated analysis of BLAST results.](https://pubmed.ncbi.nlm.nih.gov/33902722/)
2. [The PARIGA server for real time filtering and analysis of reciprocal BLAST results.](https://pubmed.ncbi.nlm.nih.gov/23667459/)