# Python Bio Error Handling

> Handle malformed FASTA/GFF via try/except/else/finally, custom exceptions, raise-from chaining. Use for parsers crashing on bad input, strict vs lenient FASTA parsing, KeyError/IndexError/ValueError, or batches skipping bad records.

- Skill: `pavel-kravchenko/python-bio-error-handling` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/python-bio-error-handling`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/python-bio-error-handling/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: pavel-kravchenko (https://skillmd.com/u/pavel-kravchenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pavel-kravchenko/python-bio-error-handling

---


# Error Handling for Bioinformatics

## When to Use

- A parser (FASTA/GFF/VCF/GenBank) needs to fail loudly on malformed records instead of silently producing wrong results.
- You want to distinguish "file not found" from "no permission" from "file exists but is corrupt" with different recovery paths.
- A batch job over thousands of sequences/records should skip bad entries and report them, rather than dying on the first one.
- You are debugging a `KeyError`, `IndexError`, `ValueError`, or `UnicodeDecodeError` raised while parsing a bioinformatics file format.
- You need to preserve the original traceback when re-raising a more specific, domain-level exception.

## Version Compatibility

Pure standard library — Python ≥3.10 (uses `dict[str, str]` / `str | None` type hints natively). No third-party dependencies.

## Prerequisites

- Comfortable with Python functions, `dict`/`set`, and basic file I/O (`open`, `with`).
- Familiarity with a sequence file format (FASTA/GFF) helps but isn't required — see `bio-sequence-io-read-sequences` for parsing basics.

## Custom Exception Hierarchy

**Goal:** give callers a way to catch "any bioinformatics error" or a specific failure mode, instead of bare `Exception`.
**Approach:** one base class, subclasses carry structured context (which chars, which line) so error messages are actionable.

```python
class BioinformaticsError(Exception):
    """Base for all pipeline errors."""

class InvalidSequenceError(BioinformaticsError):
    """Raised when a sequence contains characters outside the expected alphabet."""
    def __init__(self, invalid_chars, seq_type="DNA"):
        self.invalid_chars = invalid_chars
        super().__init__(f"Invalid {seq_type} characters: {invalid_chars}")

class SequenceLengthError(BioinformaticsError):
    """Raised when a sequence violates a minimum/maximum length constraint."""
    def __init__(self, actual, minimum=None, maximum=None):
        if minimum and actual < minimum:
            msg = f"Sequence too short: {actual} < {minimum}"
        elif maximum and actual > maximum:
            msg = f"Sequence too long: {actual} > {maximum}"
        else:
            msg = f"Invalid sequence length: {actual}"
        super().__init__(msg)

class FastaParseError(BioinformaticsError):
    """Raised on FASTA/GFF format violations, with file/line context for debugging."""
    def __init__(self, message, filename=None, line_number=None, line_content=None):
        parts = [message]
        if filename:
            parts.append(f"file={filename}")
        if line_number:
            parts.append(f"line={line_number}")
        if line_content:
            parts.append(f"content='{line_content[:50]}'")
        super().__init__(" | ".join(parts))

class TranslationError(BioinformaticsError):
    """Raised when DNA/RNA-to-protein translation fails."""
    pass
```

## try/except/else/finally Pattern

**Goal:** open a file, distinguishing recoverable errors (missing file) from real failures, and always clean up.
**Approach:** `except` handles specific failure classes, `else` runs only on success, `finally` always runs (cleanup).

```python
def read_fasta(filename: str) -> dict[str, str]:
    """Read a FASTA file into {id: sequence}, with per-failure-mode handling."""
    sequences: dict[str, str] = {}
    current_id = None
    f = None
    try:
        f = open(filename, 'r')
    except FileNotFoundError:
        raise  # let caller decide how to handle a missing file
    except PermissionError as e:
        raise FastaParseError(f"Cannot read '{filename}'") from e
    else:
        # Only runs if open() succeeded
        for line in f:
            line = line.strip()
            if line.startswith('>'):
                current_id = line[1:].split()[0]
                sequences[current_id] = []
            elif current_id:
                sequences[current_id].append(line)
        sequences = {k: ''.join(v) for k, v in sequences.items()}
    finally:
        if f:
            f.close()  # always runs, even if the loop above raised
    return sequences
```

## Exception Chaining for Format Parsing

**Goal:** convert a low-level `ValueError` (e.g. from `int()`) into a domain-specific error without losing the original cause.
**Approach:** `raise NewError(...) from e` attaches `e` as `__cause__`, visible in the traceback and via `e.__cause__`.

```python
def parse_gff_line(line: str, line_number: int | None = None) -> dict:
    """Parse a single tab-separated GFF3 line into a field dict."""
    fields = line.strip().split('\t')
    if len(fields) != 9:
        raise FastaParseError(
            f"Expected 9 fields, got {len(fields)}",
            line_number=line_number, line_content=line.strip()
        )
    try:
        return {
            'seqid': fields[0], 'source': fields[1], 'type': fields[2],
            'start': int(fields[3]), 'end': int(fields[4]),
            'score': fields[5], 'strand': fields[6],
            'phase': fields[7], 'attributes': fields[8],
        }
    except ValueError as e:
        raise FastaParseError(
            "Cannot parse coordinates",
            line_number=line_number, line_content=line.strip()
        ) from e   # preserves the original ValueError traceback
```

## Strict FASTA Parser

**Goal:** reject malformed FASTA (empty IDs, sequence before header, illegal IUPAC codes) with precise line-level errors.
**Approach:** track the current record while scanning; validate on every header switch and at EOF for the last record.

```python
def strict_fasta_parser(filename: str) -> dict[str, str]:
    """Parse FASTA with strict validation; raises FastaParseError on any violation."""
    sequences: dict[str, str] = {}
    current_id: str | None = None
    current_seq: list[str] = []
    VALID = set('ATGCNRYSWKMBDHV.-')  # IUPAC nucleotide codes + gap chars

    with open(filename, 'r') as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            if line.startswith('>'):
                if current_id is not None:
                    seq = ''.join(current_seq)
                    if not seq:
                        raise FastaParseError("Empty sequence", filename, line_num, f">{current_id}")
                    sequences[current_id] = seq
                if len(line) == 1:
                    raise FastaParseError("Empty sequence ID", filename, line_num, line)
                current_id = line[1:].split()[0]
                current_seq = []
            else:
                if current_id is None:
                    raise FastaParseError("Sequence data before first header", filename, line_num, line)
                invalid = set(line.upper()) - VALID
                if invalid:
                    raise FastaParseError(f"Invalid characters: {invalid}", filename, line_num, line)
                current_seq.append(line.upper())

    if current_id is not None:
        seq = ''.join(current_seq)
        if not seq:
            raise FastaParseError("Empty sequence for last record", filename)
        sequences[current_id] = seq

    return sequences
```

## Batch Processing with Graceful Degradation

**Goal:** run an analysis over thousands of records without one bad record aborting the whole batch.
**Approach:** catch the base `BioinformaticsError`, collect failures alongside results, and expose a `strict` switch for fail-fast use.

```python
def batch_gc_analysis(sequences: dict[str, str], strict: bool = False):
    """Compute GC% per sequence. strict=True raises on first bad record;
    strict=False skips bad records and returns (results, errors)."""
    results, errors = {}, []
    for seq_id, seq in sequences.items():
        try:
            if not seq:
                raise SequenceLengthError(0, minimum=1)
            invalid = set(seq.upper()) - set('ATGCN')
            if invalid:
                raise InvalidSequenceError(invalid)
            s = seq.upper()
            results[seq_id] = round((s.count('G') + s.count('C')) / len(s) * 100, 2)
        except BioinformaticsError as e:
            if strict:
                raise
            errors.append((seq_id, str(e)))
    return results, errors
```

## Common Built-in Exceptions in Parsing Code

- **`KeyError`**: looking up a gene/sample ID missing from a dict (`gene_info['TP53']`) — catch and report the missing key, or use `.get()` with a default.
- **`IndexError`**: a GFF/BED/VCF line has fewer tab-separated fields than expected — validate `len(fields)` before indexing.
- **`ValueError`**: `int()`/`float()` on a malformed coordinate (e.g. `"12,345"`) — catch and chain into a domain error (see above).
- **`UnicodeDecodeError`**: reading a binary file (e.g. BAM, gzip) as text — open with the correct mode/encoding, or `errors='replace'` for lossy recovery.

## Pitfalls

- **Bare `except:`**: catches `KeyboardInterrupt` and `SystemExit` — impossible to stop the script with Ctrl+C. Always name the exception.
- **`assert` for input validation**: disabled with `python -O`, so invalid input silently passes in production. Use `if not condition: raise ValueError(...)` for anything that must always be checked.
- **`raise NewError(...) from original`**: omitting `from original` hides the root cause in tracebacks (or worse, shows a misleading "during handling of the above exception" chain).
- **`else` on `try`**: runs only when *no* exception occurred — separates "might fail" from "should only run on success". Commonly forgotten, leading to code that runs even when the risky operation failed.
- **`finally` runs even on `return`**: useful for cleanup (file close, temp removal), but mutating results in `finally` can mask real errors.
- **Catching `BioinformaticsError` too broadly**: catch the most specific subclass first (`InvalidSequenceError`, `SequenceLengthError`, ...); put the base-class catch last as a fallback.

## See Also

- `bio-sequence-io-read-sequences` — parsing FASTA/FASTQ/GenBank without reinventing the wrapped parsers here.
- `bio-variant-calling-vcf-basics` — VCF-specific parsing/validation where similar exception-chaining patterns apply.
- `bio-genome-intervals-gtf-gff-handling` — robust GFF/GTF field parsing beyond the single-line example above.

