# Python Bio Oop

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

- Skill: `pavel-kravchenko/python-bio-oop` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/python-bio-oop`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/python-bio-oop/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: pavel-kravchenko (https://skillmd.com/u/pavel-kravchenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pavel-kravchenko/python-bio-oop

---


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

```python
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 gc(seq):
            return (seq.count('G') + seq.count('C')) / len(seq)
        return [name for name, seq in self._data.items() if gc(seq) >= min_gc]


db = SequenceDatabase()
db['BRCA1'] = "ATGGATTTATCTGCTCTTCG"
assert 'brca1' in db          # case-insensitive lookup
assert len(db) == 1
```

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

```python
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  # single character for int index

    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}+{other.name}")

    def gc_content(self):
        return (self.sequence.count('G') + self.sequence.count('C')) / len(self)

    def codons(self):
        """Yield codons from reading frame 0."""
        for i in range(0, len(self) - 2, 3):
            yield self.sequence[i:i + 3]


seq = BioSeq("ATGGCCGATCGATCGTAGCGA", name="test_gene")
fragment = seq[3:12]           # returns BioSeq, not str
assert isinstance(fragment, BioSeq)
assert seq[0] == 'A'           # int index returns a plain character
```

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

```python
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(self.motif) + 1):
            window = sequence[i:i + len(self.motif)]
            score = self(window)
            if score >= threshold:
                hits.append((i, window, score))
        return hits

    def __repr__(self):
        return f"MotifScorer(motif={self.motif!r}, calls={self._calls})"


tata_scorer = MotifScorer('TATAAA', mismatch_penalty=2)
dna = "GCGATCGTATAATGCGGTATAAAGCGATCGATATAAGCG"
hits = tata_scorer.scan(dna, threshold=-2)  # allow 1 mismatch
```

## `__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`.

```python
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 = sum(sys.getsizeof(v) for v in slotted) / 1024
assert slotted_kb < normal_kb  # __slots__ wins

v = VariantSlots('chr17', 43045629, 'G', 'A', 99.5)
try:
    v.annotation = "pathogenic"   # not in __slots__
except AttributeError:
    pass  # expected: cannot add arbitrary attributes
```

## 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__`.

```python
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 self.sequence.translate(self._COMPLEMENT)[::-1]


class DNARecord(BioSequenceMixin, FASTASerializableMixin, ReversibleMixin):
    def __init__(self, name, sequence):
        self.name = name
        self.sequence = sequence.upper()


rec = DNARecord("test_gene", "ATGGCCGATCGATCG")
assert 0 <= rec.gc_content() <= 1
assert rec.to_fasta().startswith(">test_gene")

# MRO (Method Resolution Order) — Python uses C3 linearization for diamond inheritance
assert [c.__name__ for c in DNARecord.__mro__][:4] == [
    'DNARecord', 'BioSequenceMixin', 'FASTASerializableMixin', 'ReversibleMixin'
]
```

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

