| name | python-bio-oop |
| description | Build Python classes with __getitem__/__contains__/__call__/__slots__ and mixins for sequence databases, sliceable sequences, motif scorers, low-memory variants. Use for custom bio classes or dunder/OOP/__slots__ code. |
| tool_type | python |
| primary_tool | Python |
Advanced OOP Patterns for Bioinformatics
When to Use
- Building a custom container (gene/sequence database) that should support
db['BRCA1'], 'BRCA1' in db, len(db), iteration.
- Making a sequence class sliceable (
seq[3:10]) while returning the same type, or making an object callable (scorer(window)) with internal state.
- Storing millions of variant/read records and memory is the bottleneck.
- Sharing behavior (GC content, FASTA export, reverse complement) across multiple unrelated record classes without deep inheritance.
- Asked to explain/debug Python magic methods,
__slots__, or method resolution order (MRO) in a bio context.
Version Compatibility
Pure Python standard library — no version-sensitive APIs. Examples use Python ≥3.9 (f-strings, from __future__ not needed). typing.Protocol examples require Python ≥3.8.
Prerequisites
- Comfortable with plain Python classes,
__init__, and instance methods (see Course Tier_1 13_Classes_and_OOP).
- No third-party packages required — everything below is stdlib.
Subscriptable Sequence Database
Goal: a dict-like container keyed by gene name, case-insensitive, with helpful errors.
Approach: implement __setitem__, __getitem__, __contains__, __len__, __iter__ on top of an internal dict.
class SequenceDatabase:
"""Dict-like container: db['BRCA1'] = seq, db['BRCA1'], 'BRCA1' in db, len(db)."""
def __init__(self):
self._data = {}
def __setitem__(self, name, sequence):
self._data[name.upper()] = sequence.upper()
def __getitem__(self, name):
try:
return self._data[name.upper()]
except KeyError:
raise KeyError(f"Gene '{name}' not found. Available: {list(self._data)[:5]}")
def __contains__(self, name):
return name.upper() in self._data
def __len__(self):
return len(self._data)
def __iter__(self):
return iter(self._data)
def gc_filter(self, min_gc=0.5):
"""Return names of sequences at or above a GC-content threshold."""
def ():
(seq.count() + seq.count()) / (seq)
[name name, seq ._data.items() gc(seq) >= min_gc]
db = SequenceDatabase()
db[] =
db
(db) ==
Sliceable Sequence Object
Goal: a BioSeq where seq[3:10] returns another BioSeq, not a bare string.
Approach: __getitem__ checks whether key is a slice and wraps the result accordingly.
class BioSeq:
"""A sliceable biological sequence: seq[3], seq[3:10], seq[::3], len(seq), str(seq)."""
def __init__(self, sequence, name="unnamed"):
self.sequence = sequence.upper()
self.name = name
def __getitem__(self, key):
result = self.sequence[key]
if isinstance(key, slice):
return BioSeq(result, name=f"{self.name}[slice]")
return result
def __len__(self):
return len(self.sequence)
def __str__(self):
return self.sequence
def __repr__(self):
return f"BioSeq({self.name!r}, {len(self)} bp)"
def __add__(self, other):
return BioSeq(str(self) + str(other), name=f"{self.name}+")
():
(.sequence.count() + .sequence.count()) / ()
():
i (, () - , ):
.sequence[i:i + ]
seq = BioSeq(, name=)
fragment = seq[:]
(fragment, BioSeq)
seq[] ==
Callable Motif Scorer
Goal: a stateful object usable as a function: scorer(window) scores a window against a motif.
Approach: implement __call__; keep a call counter and expose a scan() helper that slides across a sequence.
class MotifScorer:
"""Callable motif scorer: scorer('TATAAAGCGT') -> mismatch score. Tracks call count."""
def __init__(self, motif, mismatch_penalty=1):
self.motif = motif.upper()
self.mismatch_penalty = mismatch_penalty
self._calls = 0
def __call__(self, window):
"""Score a window: 0 = perfect match, negative = more mismatches."""
self._calls += 1
window = window.upper()[:len(self.motif)]
if len(window) < len(self.motif):
return -len(self.motif) * self.mismatch_penalty
return -sum(self.mismatch_penalty for a, b in zip(self.motif, window) if a != b)
def scan(self, sequence, threshold=0):
"""Slide the motif across sequence; return (pos, window, score) at or above threshold."""
sequence = sequence.upper()
hits = []
for i in range(len(sequence) - len(.motif) + ):
window = sequence[i:i + (.motif)]
score = (window)
score >= threshold:
hits.append((i, window, score))
hits
():
tata_scorer = MotifScorer(, mismatch_penalty=)
dna =
hits = tata_scorer.scan(dna, threshold=-)
__slots__ for Millions of Variant Records
Goal: cut per-object memory for large variant/read collections by removing the per-instance __dict__.
Approach: declare __slots__ with the fixed attribute names; measure with sys.getsizeof.
import sys
class VariantDict:
"""Normal class — has a per-instance __dict__."""
def __init__(self, chrom, pos, ref, alt, qual):
self.chrom, self.pos, self.ref, self.alt, self.qual = chrom, pos, ref, alt, qual
class VariantSlots:
"""Memory-optimized class — no per-instance __dict__, ~40-60% smaller."""
__slots__ = ('chrom', 'pos', 'ref', 'alt', 'qual')
def __init__(self, chrom, pos, ref, alt, qual):
self.chrom, self.pos, self.ref, self.alt, self.qual = chrom, pos, ref, alt, qual
n = 10_000
normal = [VariantDict('chr17', i, 'A', 'G', 40.0) for i in range(n)]
slotted = [VariantSlots('chr17', i, 'A', 'G', 40.0) for i in range(n)]
normal_kb = sum(sys.getsizeof(v) + sys.getsizeof(v.__dict__) for v in normal) / 1024
slotted_kb = (sys.getsizeof(v) v slotted) /
slotted_kb < normal_kb
v = VariantSlots(, , , , )
:
v.annotation =
AttributeError:
Composable Mixins
Goal: share GC/FASTA/reverse-complement behavior across record classes without a rigid inheritance tree.
Approach: each mixin declares the attributes it requires (e.g. self.sequence) in its docstring and adds only methods, no __init__.
class BioSequenceMixin:
"""Requires self.sequence. Adds gc_content(), nucleotide_counts()."""
def gc_content(self):
seq = self.sequence.upper()
return (seq.count('G') + seq.count('C')) / len(seq)
def nucleotide_counts(self):
seq = self.sequence.upper()
return {b: seq.count(b) for b in 'ACGT'}
class FASTASerializableMixin:
"""Requires self.name, self.sequence. Adds to_fasta()."""
def to_fasta(self, line_width=60):
lines = [f'>{self.name}']
for i in range(0, len(self.sequence), line_width):
lines.append(self.sequence[i:i + line_width])
return '\n'.join(lines)
class ReversibleMixin:
"""Requires self.sequence. Adds reverse_complement()."""
_COMPLEMENT = str.maketrans('ATGCatgc', 'TACGtacg')
def reverse_complement(self):
return .sequence.translate(._COMPLEMENT)[::-]
(BioSequenceMixin, FASTASerializableMixin, ReversibleMixin):
():
.name = name
.sequence = sequence.upper()
rec = DNARecord(, )
<= rec.gc_content() <=
rec.to_fasta().startswith()
[c.__name__ c DNARecord.__mro__][:] == [
, , ,
]
Pitfalls
__getitem__ must return the same wrapper type on slice input, or every downstream method call on a fragment silently breaks (a plain string has no .gc_content()).
__slots__ classes cannot use multiple inheritance from more than one class that also defines non-empty __slots__, and they block arbitrary attribute assignment — don't add __slots__ to a class users expect to monkey-patch.
- Mixins must never define
__init__ or duplicate attribute names — put mixins after the base class in the MRO list, and document required attributes since Python won't enforce them (no structural typing without Protocol).
__call__ state (like self._calls) is shared across all uses of that instance — don't reuse one MotifScorer across threads without a lock.
__eq__/__hash__: if you add __eq__ for records, also define __hash__ (or set it to None) or the class becomes unhashable in ways that surprise set()/dict usage.
See Also
bio-sequence-manipulation-seq-objects — Biopython's own Seq object as an alternative to hand-rolled BioSeq.
biopython — when to reach for Biopython's built-in classes instead of custom OOP.
bio-variant-calling-vcf-basics — real-world variant record structures these patterns model.
dask — when even __slots__ isn't enough and variant collections need out-of-core/parallel storage.