| name | python-bio-error-handling |
| description | 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. |
| tool_type | python |
| primary_tool | Python |
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."""
():
parts = [message]
filename:
parts.append()
line_number:
parts.append()
line_content:
parts.append()
().__init__(.join(parts))
():
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
except PermissionError as e:
raise FastaParseError(f"Cannot read '{filename}'") from e
else:
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()
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
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.-')
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[:].split()[]
current_seq = []
:
current_id :
FastaParseError(, filename, line_num, line)
invalid = (line.upper()) - VALID
invalid:
FastaParseError(, filename, line_num, line)
current_seq.append(line.upper())
current_id :
seq = .join(current_seq)
seq:
FastaParseError(, filename)
sequences[current_id] = seq
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.