| name | python-bio-classes |
| description | 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. |
| tool_type | python |
| primary_tool | Python |
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).
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) c ((.sequence))}
():
COMPLEMENT_MAP = {: , : , : , : , : }
() -> :
gc = .sequence.count() + .sequence.count()
gc / (.sequence) *
() -> :
comp = .maketrans(, )
DNA(.sequence.translate(comp)[::-], name=)
() -> :
RNA(.sequence.replace(, ), name=)
():
() -> DNA:
DNA(.sequence.replace(, ), name=)
():
AA_WEIGHTS = {
: , : , : , : , : , : , : ,
: , : , : , : , : , : , : ,
: , : , : , : , : , : ,
}
() -> :
weight = (.AA_WEIGHTS.get(aa, ) aa .sequence)
weight - ((.sequence) - ) *
dna = DNA(, name=)
(dna.gc_content(), dna.reverse_complement().sequence, (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(.sequence) < (other.sequence)
() -> :
(.sequence)
() -> :
s1, s2, s3 = DNASequence(, ), DNASequence(, ), DNASequence(, )
s1 == s2 s1 != s3 s1 < s3
s ([s3, s1, DNASequence(, )]):
(s.name, (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
self.strand = strand
@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 .VALID_STRANDS:
ValueError()
._strand = value
() -> :
(._sequence.count() + ._sequence.count()) / (._sequence) *
gene = Gene(, , strand=)
()
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': (.sequence), : (gc, )}
analyzer = DNAAnalyzer()
analyzer.validate()
(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."""
(sequence.upper()) <= ()
() -> :
desc = .description
rec = FastaRecord.from_fasta_string()
(rec, FastaRecord.is_valid_dna())
:
chromosome:
start:
end:
name: = field(compare=, default=)
strand: = field(compare=, default=)
() -> :
.end - .start
() -> :
.chromosome == other.chromosome .start < other.end other.start < .end
genes = [GeneAnnotation(, , , name=),
GeneAnnotation(, , , name=)]
g (genes):
(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 ValueErrors raised by validating setters here
python-bio-decorators — decorator mechanics behind @property/@classmethod/@staticmethod