| name | python-bio-context-managers |
| description | Build Python context managers (__enter__/__exit__, @contextmanager, sqlite3) for safe FASTA I/O, temp cleanup, DB transactions. Use for leaked file handles, temp files surviving crashes, or with-compatible readers/writers. |
| tool_type | python |
| primary_tool | contextlib |
Context Managers for Bioinformatics
When to Use
- A pipeline crashes mid-run and leaves open file handles, locked SQLite databases, or orphaned temp files.
- You need a
with-compatible FASTA/VCF/BAM writer or reader that always closes on exit, even on exception.
- You want auto-cleanup of a temporary file (e.g., a scratch FASTA for a subprocess call) regardless of success/failure.
- You're wrapping a multi-step database insert (variant calls, sample metadata) that must commit atomically or roll back.
- You want to time or instrument a block of pipeline code without cluttering it with try/finally boilerplate.
Version Compatibility
Pure standard library — contextlib and sqlite3 are part of CPython. Applies to Python ≥3.8 (3.10+ recommended for dict[str, str]-style built-in generics used below).
Prerequisites
- No third-party packages required (stdlib only:
contextlib, tempfile, sqlite3, os, time).
- Familiarity with Python classes and generators helps (
__enter__/__exit__, yield).
- Related:
bio-sequence-io-write-sequences, bio-sequence-io-read-sequences for FASTA I/O without a custom context manager.
Lifecycle
with ctx as resource:
__enter__() → acquire
(body)
__exit__() → release (always runs, even on exception)
__exit__(exc_type, exc_val, tb) — return True to suppress the exception, False/None to re-raise.
Class-Based Context Manager
Goal: build a with-compatible FASTA writer that always closes its file handle, even if the caller's code raises mid-write.
Approach: implement __enter__ (acquire) and __exit__ (release, never suppress), track state as instance attributes.
class FastaWriter:
"""Context manager for writing line-wrapped FASTA records."""
def __init__(self, filename: str, line_width: int = 80):
self.filename = filename
self.line_width = line_width
self.file = None
self.record_count = 0
def __enter__(self):
self.file = open(self.filename, 'w')
return self
def __exit__(self, exc_type, exc_val, tb):
if self.file:
self.file.close()
if exc_type is not None:
print(f"Error occurred while writing: {exc_val}")
print(f"Wrote {self.record_count} records to {self.filename}")
return False
def write_record(self, seq_id: str, sequence: str, description: = ) -> :
header = + ( description )
.file.write(header + )
i (, (sequence), .line_width):
.file.write(sequence[i:i + .line_width] + )
.record_count +=
FastaWriter() writer:
writer.write_record(, * , )
@contextmanager (generator style)
Goal: auto-delete a scratch FASTA file after use (e.g., input to a subprocess aligner call), regardless of success or failure.
Approach: code before yield is setup, code after is teardown — always wrap teardown in try/finally so cleanup runs even if the body raises.
from contextlib import contextmanager
import tempfile
import os
@contextmanager
def temp_fasta(sequences: dict[str, str]):
"""Write sequences to a temp FASTA file, yield its path, delete on exit."""
fd, path = tempfile.mkstemp(suffix=".fasta")
try:
with os.fdopen(fd, 'w') as f:
for name, seq in sequences.items():
f.write(f">{name}\n{seq}\n")
yield path
finally:
os.unlink(path)
with temp_fasta({"seq1": "ATGC", "seq2": "TTAA"}) as path:
with open(path) as f:
print(f.read())
Timed Section + Stats-Tracking Processor
Goal: time an expensive block, and separately track running stats (records parsed, bases seen) across a FASTA parse without polluting the parsing loop with timing code.
Approach: a @contextmanager for ad-hoc timing; a class-based manager when you need both iteration (a generator method) and end-of-run summary stats in __exit__.
import time
from contextlib import contextmanager
from dataclasses import dataclass
@contextmanager
def timed_section(name: str):
"""Print elapsed wall-clock time for the wrapped block."""
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"[{name}] {elapsed:.4f}s")
@dataclass
class FastaRecord:
id: str
description: str
sequence: str
@property
def gc_content(self) -> float:
seq = self.sequence.upper()
return (seq.count('G') + seq.count('C')) / len(seq) * 100
class FastaProcessor:
"""Context manager that parses FASTA and reports summary stats on exit."""
def __init__(self, filename: str):
self.filename = filename
self.file = None
self.records_processed =
.total_bases =
():
.file = (.filename, )
():
.file:
.file.close()
()
():
current_id, current_desc, parts = , , []
line .file:
line = line.strip()
line.startswith():
current_id:
seq = .join(parts)
.records_processed +=
.total_bases += (seq)
FastaRecord(current_id, current_desc, seq)
header_parts = line[:].split(, )
current_id = header_parts[]
current_desc = header_parts[] (header_parts) >
parts = []
current_id:
parts.append(line)
current_id:
seq = .join(parts)
.records_processed +=
.total_bases += (seq)
FastaRecord(current_id, current_desc, seq)
timed_section():
FastaProcessor() processor:
record processor.records():
()
SQLite Transactions
import sqlite3
with sqlite3.connect("variants.db") as conn:
conn.execute("INSERT INTO variants VALUES (?, ?, ?)", ("chr1", 100, "A"))
Multi-resource with
with open("genome.fasta") as fasta, open("variants.vcf") as vcf:
...
Pitfalls
yield position matters: code after yield is teardown — an exception in the body skips cleanup unless wrapped in try/finally.
__exit__ receives the exception, not __enter__: if the with block raises, Python calls __exit__ with exception info; forgetting to return False can accidentally suppress exceptions.
with conn: on SQLite commits, not closes: conn.close() is separate; use with contextlib.closing(conn): if you also want auto-close.
- Nesting vs stacking:
with A() as a, B() as b: is equivalent to two nested with statements; B.__exit__ runs before A.__exit__.
- Reusable vs reentrant: a class-based context manager instance is reusable across separate
with blocks by default, but is not reentrant (don't nest the same instance inside itself) unless you explicitly design for it.
See Also
bio-sequence-io-write-sequences — writing FASTA/FASTQ without a custom context manager (Biopython SeqIO.write).
bio-sequence-io-read-sequences — streaming FASTA/FASTQ parsing.
bio-database-access-batch-downloads — retry/backoff patterns for unreliable network fetches, complementary to resource cleanup here.