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 supportlen()/in - Need attribute validation (e.g. reject invalid nucleotide characters, invalid strand values) without scattering
ifchecks everywhere - Building a small class hierarchy where DNA/RNA/Protein share behavior (
BioSequencebase 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).
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.
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.
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.
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)).
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
selfis the instance, not the class:self.sequencereads the instance attribute;DNA.sequencewould be a class-level variable. Don't confuse the two.- Mutable class attributes:
class Gene: tags = []— appending totagson 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: assigningself.sequence = valueinside thesequencesetter re-invokes the setter → infinite recursion. Store toself._sequence. __eq__disables__hash__: Python sets__hash__ = Noneautomatically once you define__eq__. Add__hash__explicitly if instances need to live in asetor be a dict key, and hash only immutable fields.- Abstract class instantiation:
SequenceAnalyzer("ATGC")raisesTypeError: 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 behindfield(compare=False), or ordering will break in surprising ways once you add anamefield beforestart.
See Also
python-bio-oop— advanced patterns:__getitem__/__setitem__subscriptable databases,__call__scorers,__slots__, mixinspython-bio-functions— functions and default arguments used inside methodspython-bio-error-handling—try/exceptpatterns for theValueErrors raised by validating setters herepython-bio-decorators— decorator mechanics behind@property/@classmethod/@staticmethod