agentsclimarketplace

Python bio error handling

Skill Pavel-Kravchenko/Bioinformatics/Skills/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.From its SKILL.md

Install
npx -y skills add Pavel-Kravchenko/Bioinformatics --skill python-bio-error-handling

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

10.4 KB, ~2.4k tokens by cl100k_base, as published. Nobody here has run it

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.

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).

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__.

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.

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.

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.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most error diagnosis skills give in ~2.4k tokens

Counted across 135 of the 162 authors here whose files we hold, read 2026-09-06

  • Handle, re-throw, or log in every catch blockin 12 of 135, across 7 files
  • Use typed error classes over string messagesin 11 of 135, across 6 files
  • Log full error context server-sidein 10 of 135, across 5 files
  • Document every error code clients may receivein 9 of 135, across 4 files
  • Surface errors at the boundary where they occurin 9 of 135, across 4 files
  • Wrap React components in an ErrorBoundaryin 9 of 135, across 4 files
  • Wrap errors with context, never lose the originalin 9 of 135, across 4 files
  • Use the standard error envelope for API responsesin 9 of 135, across 4 files
  • Retry only retriable errors, never 4xx client errorsin 8 of 135, across 3 files
  • Retry transient failures with exponential backoff and jitterin 8 of 135
  • Show users friendly messages without technical detailsin 7 of 135, across 3 files
  • Use the Result pattern for expected failuresin 7 of 135, across 5 files

Said here and by no other author read

  • Fail loudly on malformed records
  • Define a base exception with subclasses per failure mode
  • Attach file and line context to raised errors
  • Chain domain errors with raise-from to preserve tracebacks
  • Use else blocks for success-only code
  • Use finally blocks for cleanup

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.