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, orUnicodeDecodeErrorraised 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-sequencesfor 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 — validatelen(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, orerrors='replace'for lossy recovery.
Pitfalls
- Bare
except:: catchesKeyboardInterruptandSystemExit— impossible to stop the script with Ctrl+C. Always name the exception. assertfor input validation: disabled withpython -O, so invalid input silently passes in production. Useif not condition: raise ValueError(...)for anything that must always be checked.raise NewError(...) from original: omittingfrom originalhides the root cause in tracebacks (or worse, shows a misleading "during handling of the above exception" chain).elseontry: 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.finallyruns even onreturn: useful for cleanup (file close, temp removal), but mutating results infinallycan mask real errors.- Catching
BioinformaticsErrortoo 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.