Applied - Genomic File Formats
In one line: what each format holds, its coordinate convention, and which Python library actually reads it.
The table
| Format | Holds | Coords | Read with |
|---|---|---|---|
| FASTA | sequence | — | SeqIO, pyfastx, pysam.FastaFile |
| FASTQ | sequence + quality | — | SeqIO, pysam.FastxFile, pyfastx |
| SAM/BAM/CRAM | read alignments | 1-based text / 0-based binary | pysam |
| VCF/BCF | variants | 1-based | pysam, cyvcf2, Reading and Writing Data |
| BED | intervals | 0-based half-open | pandas, pyranges, bioframe |
| GFF3/GTF | annotation | 1-based closed | pandas, gffutils, pyranges |
| bigWig/bigBed | dense signal | 0-based | pyBigWig, deeptools |
| GenBank/EMBL | sequence + features | 1-based → 0-based in memory | SeqIO |
| PDB/mmCIF | structure | residue numbering | Bio.PDB |
| HDF5/.h5ad | matrices + metadata | — | h5py, anndata |
| Parquet | tables | — | Reading and Writing Data |
The coordinate column is the one that will cost you. See Coordinate Systems.
Biopython does not read alignment files
Bio.SeqIO handles sequence formats. It does not read BAM, CRAM, VCF, BCF, tabix indexes or bigWig. For those, pysam is the answer — it wraps htslib, the same C library samtools and bcftools use.
import pysam
bam = pysam.AlignmentFile("sample.bam", "rb")
for read in bam.fetch("chr17", 7_668_000, 7_688_000): # 0-based!
read.query_name, read.reference_start, read.mapping_quality, read.cigarstring
vcf = pysam.VariantFile("cohort.vcf.gz")
for rec in vcf.fetch("chr17", 7_668_000, 7_688_000):
rec.chrom, rec.pos, rec.ref, rec.alts # rec.pos is 1-based
rec.info["AF"], rec.samples["S1"]["GT"]
fa = pysam.FastaFile("hg38.fa")
fa.fetch("chr17", 7_676_153, 7_676_154) # 0-based half-open
Note the mixed conventions inside pysam itself: fetch() takes 0-based coordinates, but rec.pos on a VCF record returns the 1-based POS. This is deliberate — the region query follows the API convention, the record field follows the file format. It is also a reliable source of off-by-one bugs.
FASTA
>NM_000546.6 Homo sapiens tumor protein p53 (TP53), mRNA
GATGGGATTGGGGTTTTCCCCTCCCATGTGCTCAAGACTGGCGCTAAAAGTTTTGAGCTT
- Header line starts with
>; the ID is up to the first whitespace. - Lowercase = soft-masked (repeats).
.upper()before composition analysis. N= unknown base..faiindex (fromsamtools faidx) enables random access.
FASTQ
@read_001 1:N:0:ATCACG
GATTACAGATTACAGATTACA
+
IIIIIIIIIIIIIIIIIIIII
Four lines per record. Quality is ASCII with offset 33 (Phred+33) for anything modern.
Q = ord(char) - 33
P_error = 10 ** (-Q / 10)
Q20 = 1% error, Q30 = 0.1%, Q40 = 0.01%. Legacy Illumina (pre-1.8) used offset 64 — a file read with the wrong offset gives quality scores off by 31, which looks like uniformly terrible data.
VCF
##fileformat=VCFv4.3
##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency">
#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT SAMPLE1
chr17 7676154 . G C 99 PASS AF=0.001;DP=44 GT:AD:DP 0/1:22,22:44
- 1-based.
INFOpacks many variables into one;-delimited string.FORMATdefines the:-delimited per-sample subfields.- Indels anchor on the preceding base: an insertion is
REF=A ALT=AT. - Left-aligned by convention — the opposite of HGVS. See hgvs Pitfalls.
See Applied - Reading VCF with pandas.
GFF3 / GTF
chr17 RefSeq gene 7668402 7687550 . - . ID=gene-TP53;Name=TP53
chr17 RefSeq CDS 7673535 7673837 . - 0 Parent=rna-NM_000546.6
Nine tab-separated columns, 1-based closed, # comments. Column 9 packs arbitrary key–value attributes — GFF3 uses key=value;, GTF uses key "value";.
See Applied - GFF and Genomic Intervals.
BED
chr17 7668401 7687550 TP53 0 -
0-based half-open — note it is one less than the GFF start for the same feature. Minimum three columns; up to twelve. The chrom, start, end triple is the lingua franca of interval tools.
Compression and indexing
bgzip -c file.vcf > file.vcf.gz # bgzip, NOT gzip — block-compressed
tabix -p vcf file.vcf.gz # creates .tbi for random access
samtools faidx genome.fa # .fai
samtools index sample.bam # .bai
bgzip is not gzip. A bgzip file is a valid gzip file, but it is written in independently-decompressible blocks, which is what makes tabix random access possible. A plain gzip'd VCF cannot be tabix-indexed, and this catches people out constantly.
In Python, pd.read_csv and gzip.open read bgzip files fine (they are gzip-compatible); you just cannot query a region without going through pysam.
Chromosome naming
The single most common cause of a join returning nothing:
| Source | Style |
|---|---|
| UCSC | chr1, chrM |
| Ensembl | 1, MT |
| NCBI RefSeq | NC_000001.11 |
df["chrom"] = df["chrom"].str.removeprefix("chr") # → Ensembl style
df["chrom"] = "chr" + df["chrom"].astype(str) # → UCSC style
df["chrom"] = df["chrom"].replace({"MT": "chrM", "M": "chrM"})
Normalise at load time, in one place, and assert afterwards. See Merging and Joining.
A loader module worth writing
from pathlib import Path
import pandas as pd
GFF_COLS = ["seqid","source","type","start","end","score","strand","phase","attributes"]
BED_COLS = ["chrom","start","end","name","score","strand"]
def read_gff(path):
df = pd.read_csv(path, sep="\t", comment="#", names=GFF_COLS,
dtype={"seqid":"category","type":"category",
"start":"int32","end":"int32"})
df["start0"] = df["start"] - 1 # ← convert ONCE, name it explicitly
df["end0"] = df["end"]
return df
def read_bed(path, n_cols=6):
return pd.read_csv(path, sep="\t", header=None, names=BED_COLS[:n_cols],
dtype={"chrom":"category","start":"int32","end":"int32"})
The start0/end0 naming convention is the discipline that prevents coordinate bugs. A bare start column in a frame that mixes BED and GFF data is an accident waiting to happen.
Reproducibility
Record for every reference file: the assembly (GRCh38, and which patch), the annotation release (RefSeq 110, GENCODE v45), the source URL, and a checksum. "We used hg38" is not enough — patch releases add and remove contigs.
See Applied - Reproducible Environment.
Common mistakes
- Coordinate convention mixups. BED vs GFF vs VCF.
chr1vs1producing silent empty joins.- gzip instead of bgzip, so tabix indexing fails.
- Wrong FASTQ quality offset.
- Ignoring soft-masking.
- Expecting Biopython to read BAM/VCF.
- Assuming one row per variant in a VCF. Multi-allelic sites pack several ALTs into one row.
- Not recording the annotation release.
See also
Coordinate Systems · Applied - Reading VCF with pandas · Applied - GFF and Genomic Intervals · Applied - FASTA and FASTQ Workflows · SeqIO · Reading and Writing Data · Applied - Reproducible Environment