DEV Community

Cover image for The off-by-one error that ate genomics: how 0 based and 1 based coordinates started a 20-year civil war
Nadia Baig
Nadia Baig

Posted on

The off-by-one error that ate genomics: how 0 based and 1 based coordinates started a 20-year civil war

I once removed the first base of every gene in a dataset.
Nobody noticed for two days.
The bug was seven characters long: I had converted a VCF coordinate into a BED interval by subtracting 1 from start and forgotten that BED intervals are already 0-based and half-open. My "correction" was a corruption.

I wish this story were unusual. It isn't. Bioinformatics has, over about twenty years of organic growth, built the most impressive collection of off-by-one hazards in modern software. Here's the story of why and the survival rules I now live by.

Why coordinates matter in biology

A genome is a very long string of the letters A, T, C and G. Every genomic feature: a gene, a variant, a regulatory element, a read alignment has to say where on that string it lives. That means a start position and an end position.
Sounds easy. It has ruined careers.

The problem: bioinformatics has two different philosophies about how to number positions, and there is no truce.

  1. 0-based, half-open: start counting at 0. The end position is not included. Beloved by Unix, Python, and the UCSC ecosystem.

  2. 1-based, closed: start counting at 1. The end position is included. Beloved by biologists, most databases, and the VCF / GFF ecosystem.

Both are self-consistent. Both are perfectly valid. And they meet, silently, in the middle of your pipeline.

Same feature. Four different notations

Take a tiny 10 base DNA sequence and try to describe the three highlighted bases in the middle. You will get four different, all-correct answers:

That's not a madeup example. That is the daily reality of anyone working with genomic data. BED and VCF are the two most commonly mixed formats in the world, and they disagree on both the start convention and whether the end is included.
The moment you copy a coordinate from one tool's output into another tool's input, you are one careless minute away from a silent, systematic bug that ships to production.

The bug, visualised

Say you meant to describe the three C's at positions 4-6 (1-based). You type 4 6 into a BED-based tool without translating. Here is what actually happens:

Not only did the feature shift by one base but it also shrank to two bases, because the "end" in a half open interval is exclusive. So a single confused number typing gives you the wrong sequence and the wrong length. The tool doesn't complain. The pipeline finishes. The plot looks fine.
Repeat this across 20,000 genes in a genome and you have destroyed your dataset in a way that will only surface later, when a biologist looks at a single well-known gene and says "wait, this isn't right."

That was my two day bug. Multiply it across the field and you have a discipline wide problem.

Why we have both - a short historical excuse

The 0-based half open convention came out of UCSC and the Unix / C world. It's mathematically elegant: the length of an interval is trivially end - start, adjacent intervals never overlap, and slicing a Python string looks identical to slicing a genomic region.

The 1-based closed convention came out of biology and databases. If you ask a biologist "which is the first base of your gene?" they will say "the first one." That's 1, not 0. Biological literature has always numbered positions from 1, and formats designed for biologists (VCF, GFF, GTF, and the raw SAM/BAM record) followed suit.
Neither community backed down. So instead of standardising, we agreed to disagree and let the pain fall on whoever writes the pipeline.

The survival cheat sheet

Every bioinformatician I know has some version of this printed above their monitor:

The rough rule: formats born in the Unix / UCSC pipeline world are 0-based; formats born in the biology / database world are 1-based. When you're not sure, look it up. When you're really not sure, write a test.

Practical rules I now follow

  1. Never manually convert coordinates ever. Use a library. Python's pyranges, pybedtools, and the bioframe libraries all handle 0-based half-open internally. R has GRanges. Let the library do the translation.

  2. When you write coordinates to logs, always annotate. Not chr1:1000-2000. Instead: chr1:1000-2000 (BED, 0-based half-open). Your future self will thank you.

  3. Sanity check with a known feature. Pick one gene you know cold — its start, its end, its length. Print those out at every stage of your pipeline. If those numbers ever move by ±1, stop and investigate before you trust anything else.

  4. Trust the file format's spec, not the tool's documentation. Tool documentation lies. File format specs mostly don't.

  5. Add integration tests using tiny reference intervals. A three base "feature" like the one in these figures, run through your entire pipeline. If you get any answer other than exactly those three bases coming out, something is broken.

The lesson isn't really about genomics

Every field that grew organically before it standardised has one of these fights.

Web development: are your CSS box sizing calculations content box or border box? Same bytes, different meaning.

Time and dates: is Sunday the start of the week or the end? Is a "quarter" fiscal or calendar? January is 0 in JavaScript's Date but 1 in almost every other language.

Databases: is a table's rowid 0-indexed or 1-indexed? Are string positions in your query language 0-indexed (Python) or 1-indexed (SQL)?

Video/audio: is the timecode 30fps drop-frame or non-drop?

The reason genomics stands out is scale. We deal with three billion positions per human genome and hundreds of samples per project. An off-by-one bug that would produce one wrong pixel in a web app produces three billion silently wrong positions in a variant call file. The blast radius is enormous.

The take away

Off-by-one is the oldest bug in software. Genomics has industrialised it. Every format we invented in the last thirty years is a small opinion about where counting should start and whether the end is included, and every pipeline that touches more than one format is one careless conversion away from a silent, systematic mistake.

The cure isn't more discipline. The cure is:

  • use libraries,

  • annotate every coordinate you write down,
    test with a known feature, and stop trusting your intuition about half-open intervals because your intuition is wrong roughly half the time.

Over to you

Where have off-by-one and coordinate-system bugs bitten you? Genomics? Web dates? Video timecodes? Database offsets? I would love to hear the war stories in the comments and any conventions your team has adopted to make the confusion less silent.

Top comments (0)