agentsclimarketplace

Python bio error handling

Skill Pavel-Kravchenko/Bioinformatics/Skills/python-bio-error-handling

208 bioinformatics skills for Claude Code — NGS, single-cell, metagenomics, structural biology, algorithms, AI for science

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.
  • 3 stars3 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.

What its author says it does

Copied from the file, not written here

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

10.4 KB, 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.

Keep looking

Skills are one crate of 328,083. 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.