# Python Bio Classes

> Build Python classes for Gene/DNA/RNA/Protein records with __eq__/__lt__/__hash__, @property validation, ABCs, and @classmethod parsers (from_fasta_string). Use when modeling genes/FASTA/GFF as objects or asked about Python OOP, inheritance, dataclasses.

- Skill: `pavel-kravchenko/python-bio-classes` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/python-bio-classes`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/python-bio-classes/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-classes

---


# Classes for Bioinformatics

## When to Use

- Modeling domain objects (Gene, Sequence, ProteinRecord, GeneAnnotation) instead of passing around loose strings/dicts
- Need objects that print nicely (`__str__`/`__repr__`), sort (`__lt__`), compare (`__eq__`), or support `len()`/`in`
- Need attribute validation (e.g. reject invalid nucleotide characters, invalid strand values) without scattering `if` checks everywhere
- Building a small class hierarchy where DNA/RNA/Protein share behavior (`BioSequence` base class) but differ in analysis methods
- Need alternative constructors (`from_fasta_string`, `from_vcf_row`) or a lightweight record type (`@dataclass`) for GFF/BED-style annotations

## Version Compatibility

Python ≥ 3.9 (3.10+ recommended for `X | Y` union hints). Uses only the standard library: `abc`, `dataclasses`, `functools`. No third-party dependencies.

## Prerequisites

- Comfortable with Python functions, dicts, and string methods (see `python-bio-functions`, `python-bio-strings`)
- Basic understanding of FASTA format for the parsing example below

## BioSequence Hierarchy

**Goal:** share common sequence behavior (length, printing, composition) across DNA/RNA/Protein while letting each subclass add its own analysis methods.
**Approach:** define a `BioSequence` base class with dunder methods and a `composition()` helper; subclass it for type-specific logic (GC content, complementing, transcription, molecular weight).

```python
class BioSequence:
    """Base class for all biological sequences."""

    def __init__(self, sequence: str, name: str = "unnamed"):
        self.sequence = sequence.upper()
        self.name = name

    def __len__(self) -> int:
        return len(self.sequence)

    def __str__(self) -> str:
        return f">{self.name}\n{self.sequence}"

    def __repr__(self) -> str:
        return f"{type(self).__name__}('{self.sequence}', name='{self.name}')"

    def __contains__(self, motif: str) -> bool:
        return motif.upper() in self.sequence

    def composition(self) -> dict:
        """Character frequency dict, e.g. {'A': 3, 'C': 2, ...}."""
        return {c: self.sequence.count(c) for c in sorted(set(self.sequence))}


class DNA(BioSequence):
    COMPLEMENT_MAP = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G', 'N': 'N'}

    def gc_content(self) -> float:
        gc = self.sequence.count('G') + self.sequence.count('C')
        return gc / len(self.sequence) * 100

    def reverse_complement(self) -> "DNA":
        comp = str.maketrans('ATGCN', 'TACGN')
        return DNA(self.sequence.translate(comp)[::-1], name=f"{self.name}_revcomp")

    def transcribe(self) -> "RNA":
        return RNA(self.sequence.replace('T', 'U'), name=f"{self.name}_rna")


class RNA(BioSequence):
    def to_dna(self) -> DNA:
        return DNA(self.sequence.replace('U', 'T'), name=f"{self.name}_dna")


class Protein(BioSequence):
    AA_WEIGHTS = {
        'A': 89, 'R': 174, 'N': 132, 'D': 133, 'C': 121, 'E': 147, 'Q': 146,
        'G': 75, 'H': 155, 'I': 131, 'L': 131, 'K': 146, 'M': 149, 'F': 165,
        'P': 115, 'S': 105, 'T': 119, 'W': 204, 'Y': 181, 'V': 117,
    }

    def molecular_weight(self) -> float:
        """Average molecular weight in Daltons (subtracts water lost per peptide bond)."""
        weight = sum(self.AA_WEIGHTS.get(aa, 110) for aa in self.sequence)
        return weight - (len(self.sequence) - 1) * 18


dna = DNA("ATGCGATCGATCGTAGCGATCG", name="test_gene")
print(dna.gc_content(), dna.reverse_complement().sequence, isinstance(dna, BioSequence))
```

## Comparable and Hashable Sequences

**Goal:** make sequence objects sortable (`sorted(seqs)`) and comparable by value (`seq1 == seq2`), which the `__str__`/`__repr__` pair above does not give you for free.
**Approach:** implement `__eq__` and `__lt__` explicitly, returning `NotImplemented` for foreign types; note that defining `__eq__` sets `__hash__` to `None`, so restore it explicitly if instances need to go in a `set`/dict key.

```python
class DNASequence:
    """DNA sequence with rich comparison support."""

    VALID_BASES = set('ATGCN')

    def __init__(self, sequence: str, name: str = "unnamed"):
        sequence = sequence.upper()
        invalid = set(sequence) - self.VALID_BASES
        if invalid:
            raise ValueError(f"Invalid nucleotides: {invalid}")
        self.sequence = sequence
        self.name = name

    def __len__(self) -> int:
        return len(self.sequence)

    def __eq__(self, other) -> bool:
        if not isinstance(other, DNASequence):
            return NotImplemented
        return self.sequence == other.sequence

    def __lt__(self, other) -> bool:
        if not isinstance(other, DNASequence):
            return NotImplemented
        return len(self.sequence) < len(other.sequence)

    def __hash__(self) -> int:
        # Required because __eq__ is defined; hash on the immutable value used for equality.
        return hash(self.sequence)

    def __repr__(self) -> str:
        return f"DNASequence('{self.sequence[:20]}...', name='{self.name}')"


s1, s2, s3 = DNASequence("ATGCATGC", "a"), DNASequence("ATGCATGC", "b"), DNASequence("ATGCATGCATGC", "c")
assert s1 == s2 and s1 != s3 and s1 < s3
for s in sorted([s3, s1, DNASequence("AT", "tiny")]):
    print(s.name, len(s))
```

## Properties for Validation

**Goal:** reject invalid data (bad nucleotides, bad strand values) the moment an attribute is set, instead of failing later deep in an analysis function.
**Approach:** back the public attribute with a private `_sequence`, expose it through `@property`/`@x.setter`, and add a read-only computed property (no setter) for derived values like GC content.

```python
class Gene:
    VALID_STRANDS = {'+', '-'}

    def __init__(self, name: str, sequence: str, strand: str = '+'):
        self.name = name
        self.sequence = sequence   # goes through the setter below
        self.strand = strand       # goes through the setter below

    @property
    def sequence(self) -> str:
        return self._sequence

    @sequence.setter
    def sequence(self, value: str):
        value = value.upper()
        invalid = set(value) - set('ATGCN')
        if invalid:
            raise ValueError(f"Invalid nucleotides: {invalid}")
        self._sequence = value

    @property
    def strand(self) -> str:
        return self._strand

    @strand.setter
    def strand(self, value: str):
        if value not in self.VALID_STRANDS:
            raise ValueError(f"Strand must be '+' or '-', got '{value}'")
        self._strand = value

    @property
    def gc_content(self) -> float:
        """Read-only computed property -- no setter, so `gene.gc_content = 5` raises AttributeError."""
        return (self._sequence.count('G') + self._sequence.count('C')) / len(self._sequence) * 100


gene = Gene("TP53", "ATGGAGGAGCCGCAGTCAGATC", strand='+')
print(f"{gene.name}: GC={gene.gc_content:.1f}%")
```

## Abstract Base Classes

**Goal:** force every concrete analyzer subclass to implement `validate()`/`summary()`, catching missing methods at instantiation time instead of at first call.
**Approach:** subclass `abc.ABC` and mark required methods with `@abstractmethod`; instantiating the ABC itself raises `TypeError`.

```python
from abc import ABC, abstractmethod


class SequenceAnalyzer(ABC):
    def __init__(self, sequence: str):
        self.sequence = sequence.upper()

    @abstractmethod
    def validate(self) -> bool: ...

    @abstractmethod
    def summary(self) -> dict: ...


class DNAAnalyzer(SequenceAnalyzer):
    def validate(self) -> bool:
        invalid = set(self.sequence) - set('ATGCN')
        if invalid:
            raise ValueError(f"Invalid DNA bases: {invalid}")
        return True

    def summary(self) -> dict:
        gc = (self.sequence.count('G') + self.sequence.count('C')) / len(self.sequence) * 100
        return {'length': len(self.sequence), 'gc_content': round(gc, 2)}


analyzer = DNAAnalyzer("ATGCGATCGATCG")
analyzer.validate()
print(analyzer.summary())
```

## Alternative Constructors and Dataclasses

**Goal:** parse a FASTA string into an object (`@classmethod`), validate input with no instance available (`@staticmethod`), and get a lightweight sortable annotation record for free (`@dataclass`).
**Approach:** `@classmethod` receives `cls` and returns `cls(...)`, which subclasses inherit correctly; `@dataclass(order=True)` auto-generates `__init__`/`__repr__`/`__eq__`/`__lt__` from field order (exclude non-key fields with `field(compare=False)`).

```python
from dataclasses import dataclass, field


class FastaRecord:
    def __init__(self, seq_id: str, sequence: str, description: str = ""):
        self.seq_id = seq_id
        self.sequence = sequence.upper()
        self.description = description

    @classmethod
    def from_fasta_string(cls, fasta_text: str) -> "FastaRecord":
        """Alternative constructor: parse a '>id desc\\nSEQ' formatted string."""
        lines = fasta_text.strip().split('\n')
        header = lines[0]
        if not header.startswith('>'):
            raise ValueError("FASTA header must start with '>'")
        parts = header[1:].split(None, 1)
        seq_id, description = parts[0], (parts[1] if len(parts) > 1 else "")
        return cls(seq_id, ''.join(lines[1:]), description)

    @staticmethod
    def is_valid_dna(sequence: str) -> bool:
        """No instance needed -- pure validation helper."""
        return set(sequence.upper()) <= set('ATGCN')

    def __str__(self) -> str:
        desc = f" {self.description}" if self.description else ""
        return f">{self.seq_id}{desc}\n{self.sequence}"


rec = FastaRecord.from_fasta_string(">BRCA1 breast cancer gene\nATGGATTTCGATCGATCGTAGC")
print(rec, FastaRecord.is_valid_dna("ATGXZ"))


@dataclass(order=True)
class GeneAnnotation:
    """A GFF/BED-style annotation, sortable by (chromosome, start, end)."""
    chromosome: str
    start: int
    end: int
    name: str = field(compare=False, default="")
    strand: str = field(compare=False, default='+')

    @property
    def length(self) -> int:
        return self.end - self.start

    def overlaps(self, other: "GeneAnnotation") -> bool:
        return self.chromosome == other.chromosome and self.start < other.end and other.start < self.end


genes = [GeneAnnotation("chr17", 43044295, 43170245, name="BRCA1"),
         GeneAnnotation("chr7", 55019017, 55211628, name="EGFR")]
for g in sorted(genes):
    print(g.name, g.chromosome, g.length)
```

## Dunder Methods Reference

| Method | Enables |
|--------|---------|
| `__init__` | `Gene("BRCA1", "ATG...")` |
| `__str__` | `print(gene)` — human readable |
| `__repr__` | `repr(gene)` — developer view, should allow recreation |
| `__len__` | `len(seq)` |
| `__eq__` | `seq1 == seq2` |
| `__lt__` | `seq1 < seq2`, `sorted(seqs)` |
| `__contains__` | `"ATG" in seq` |
| `__hash__` | use as `set`/dict-key member (lost when `__eq__` is defined) |

## Pitfalls

- **`self` is the instance, not the class**: `self.sequence` reads the instance attribute; `DNA.sequence` would be a class-level variable. Don't confuse the two.
- **Mutable class attributes**: `class Gene: tags = []` — appending to `tags` on one instance mutates it for every instance. Set mutable defaults inside `__init__` (`self.tags = []`) instead.
- **Forgetting `super().__init__()`**: in a subclass `__init__`, skipping this means the parent's setup (and any parent attributes) never runs.
- **Properties without a `_` backing store**: assigning `self.sequence = value` inside the `sequence` setter re-invokes the setter → infinite recursion. Store to `self._sequence`.
- **`__eq__` disables `__hash__`**: Python sets `__hash__ = None` automatically once you define `__eq__`. Add `__hash__` explicitly if instances need to live in a `set` or be a dict key, and hash only immutable fields.
- **Abstract class instantiation**: `SequenceAnalyzer("ATGC")` raises `TypeError: Can't instantiate abstract class` — that's the intended enforcement, not a bug.
- **`@dataclass(order=True)` compares fields in declaration order**: put fields you don't want in the comparison/sort key behind `field(compare=False)`, or ordering will break in surprising ways once you add a `name` field before `start`.

## See Also

- `python-bio-oop` — advanced patterns: `__getitem__`/`__setitem__` subscriptable databases, `__call__` scorers, `__slots__`, mixins
- `python-bio-functions` — functions and default arguments used inside methods
- `python-bio-error-handling` — `try`/`except` patterns for the `ValueError`s raised by validating setters here
- `python-bio-decorators` — decorator mechanics behind `@property`/`@classmethod`/`@staticmethod`

