Python Data Types for Bioinformatics
When to Use
- Explaining or debugging why
0.1 + 0.2 == 0.3isFalsein a GC-content or p-value comparison. - Converting raw text fields (from
line.split("\t"), a FASTA header, or a VCF row) intoint/floatfor coordinates, depth, or scores. - Choosing between
Noneand""to represent missing vs. empty biological data. - Writing sequence-validation functions that return
bool(valid nucleotide check, stop-codon check). - Teaching Python fundamentals (variables, naming, type conversion) using DNA/RNA/protein examples instead of generic ones.
Version Compatibility
Python ≥ 3.9 (f-strings with =, str.maketrans/translate all stable since 3.6+). No third-party packages required — this skill is pure standard library.
Prerequisites
- None — this is a foundational skill. Assumes only that Python is installed (
python3 --version). - Useful follow-on skills:
bio-sequence-manipulation-seq-objects,bio-sequence-io-read-sequences.
Core Data Types
# type example bioinformatics use
# int 42 sequence length, read count, coordinate
# float 0.487 GC content, E-value, p-value
# str "ATGCGA" DNA/RNA/protein sequence, gene name
# bool True / False is the sequence valid? has a stop codon?
# NoneType None missing data, "not yet annotated"
gene_name = "BRCA1"
chromosome = 17
gc_content = 0.423
is_tumor_suppressor = True
gene_function = None # not yet annotated
for value in (gene_name, chromosome, gc_content, is_tumor_suppressor, gene_function):
print(f"{str(value):25s} -> {type(value).__name__}")
Naming rules (break these = SyntaxError): letters/digits/underscores only, cannot start with a digit,
case-sensitive, cannot use a keyword (if, for, class, ...).
Naming conventions: snake_case for variables/functions (gene_name), UPPER_CASE for constants
(AVOGADRO = 6.022e23), descriptive names (gc_content, not gc or x).
Goal: parse a tab-delimited genomic record into the right types and derive a value from it.
Approach: split the line on tabs, then explicitly convert each field with int()/float() —
fields from split() are always str, even if they look numeric.
def parse_gene_record(line):
"""Parse a tab-delimited record: gene, chrom, start, end, gc_fraction.
Returns a dict with correctly-typed fields, or None if the line is malformed.
"""
fields = line.rstrip("\n").split("\t")
if len(fields) != 5:
return None
gene, chrom, start, end, gc = fields
return {
"gene": gene, # already a str
"chrom": int(chrom), # str -> int
"start": int(start), # str -> int (0-based, inclusive)
"end": int(end), # str -> int (exclusive, half-open)
"gc_fraction": float(gc), # str -> float
"length": int(end) - int(start),
}
record = parse_gene_record("BRCA1\t17\t43044295\t43170245\t0.423")
print(f"{record['gene']} chr{record['chrom']}:{record['start']}-{record['end']} "
f"({record['length']:,} bp, GC={record['gc_fraction']*100:.1f}%)")
Goal: compare floating-point scores (p-values, GC fractions) without falling into the
0.1 + 0.2 != 0.3 trap, and validate a sequence's nucleotide alphabet.
Approach: use a tolerance instead of == for floats; use all()/set membership for
sequence validation, which returns a bool you can branch on directly.
def floats_close(a, b, tol=1e-9):
"""Compare two floats with a tolerance instead of exact equality."""
return abs(a - b) < tol
def is_valid_dna(sequence, alphabet="ATGC"):
"""Return True if every character (case-insensitive) is in the given alphabet."""
valid = set(alphabet)
return all(nuc in valid for nuc in sequence.upper())
print(floats_close(0.1 + 0.2, 0.3)) # True (0.1 + 0.2 == 0.3 is False!)
print(is_valid_dna("ATGCGATCGA")) # True
print(is_valid_dna("ATGCXYZ")) # False -- X, Y, Z are not nucleotides
Goal: reverse-complement a DNA sequence and report its GC content, tying together
str slicing, str.translate, and int/float conversion.
Approach: build a translation table once with str.maketrans, apply it, then reverse
with slice step -1; compute GC% with str.count and integer/float division.
def reverse_complement(sequence):
"""Return the reverse complement of a DNA sequence (upper- or lower-case input)."""
table = str.maketrans("ATGCatgc", "TACGtacg")
return sequence.translate(table)[::-1]
def gc_content(sequence):
"""Calculate GC content as a percentage (float), 0.0 for an empty sequence."""
seq = sequence.upper()
if not seq:
return 0.0
return (seq.count("G") + seq.count("C")) / len(seq) * 100
dna = "ATGCGATCGATCGTAGC"
print(f"Original: 5'-{dna}-3'")
print(f"Rev comp: 5'-{reverse_complement(dna)}-3'")
print(f"GC content: {gc_content(dna):.1f}%")
Pitfalls
- Floating-point arithmetic is not exact:
0.1 + 0.2is0.30000000000000004, not0.3. This affects every language using IEEE 754 doubles. Always compare floats with a tolerance (abs(a - b) < 1e-9), never==. int()truncates, it does not round:int(3.9)is3. Useround(3.9)to get4. This matters when converting a read-depth fraction or coverage estimate to an integer.- String immutability: you cannot write
dna[0] = "G"(TypeError). Every "modification" creates a new string:mutated = "G" + dna[1:]. Nonevs. empty string:Nonemeans "this value does not exist";""means "this value exists but has zero length". Do not conflate them — a missing FASTA/VCF field is semantically different from an empty one.is None, not== None: always check withif result is None;==can give surprising results if a class overrides equality.split()never converts types:line.split("\t")always returns a list ofstr, even for numeric-looking fields — you must callint()/float()explicitly, or a later arithmetic op will raiseTypeError.- Off-by-one errors: Python slices are half-open
[start, stop), but genomic coordinates are often 1-based/inclusive (GFF, VCF) vs. 0-based/half-open (BED) — check which convention a file format uses before slicing.
See Also
bio-sequence-manipulation-seq-objectsbio-sequence-manipulation-sequence-propertiesbio-sequence-io-read-sequencesbiopython