Coordinate Systems
In one line: genomics uses two incompatible position conventions, and mixing them is the most common off-by-one bug in the field.
The two systems
1-based, fully closed ("interbase off"): the first base is position 1, and an interval [start, end] includes both endpoints. A single base at position 5 is 5-5. Length is end - start + 1.
0-based, half-open: the first base is position 0, and an interval [start, end) includes the start but excludes the end. A single base at index 5 is [5, 6). Length is end - start.
sequence: A C G T
1-based: 1 2 3 4
0-based: 0 1 2 3 4 ← coordinates sit *between* bases
The 0-based system is sometimes called "interbase" because the numbers label the gaps between bases, not the bases themselves.
Which format uses which
| Format / tool | System |
|---|---|
| BED | 0-based, half-open |
| BAM (binary fields) | 0-based |
| GFF / GTF | 1-based, closed |
| VCF | 1-based, closed |
| SAM (text POS field) | 1-based |
| HGVS | 1-based, closed |
| Python slicing / NumPy | 0-based, half-open |
| Ensembl API | 1-based |
| UCSC browser display | 1-based (but its BED files are 0-based) |
That last row is the classic trap: UCSC shows you 1-based coordinates but exports 0-based BED.
Converting
# 1-based closed → 0-based half-open (i.e. to a Python slice)
start0, end0 = start1 - 1, end1
# 0-based half-open → 1-based closed
start1, end1 = start0 + 1, end0
seq[start1 - 1 : end1] # extracting a GFF/VCF feature from a Python string
Only the start shifts. The end is numerically identical. That asymmetry is why the bug is so easy to make and so easy to miss — half of your test cases will pass.
Biopython does this for you
SeqFeature.location always stores 0-based half-open internally, regardless of the source format. A GFF feature at 1-based 100-200 becomes [99:200] in Biopython. When you print a location you see [99:200], and feature.extract(record.seq) gives the right subsequence.
Do not add or subtract 1 yourself when working with a SeqFeature — Biopython already did. See SeqFeature and Location.
hgvs and the extra subtleties
HGVS is 1-based, but c. coordinates carry two extra complications:
- There is no position 0. In
c.numbering, position 1 is the A of the initiator ATG, and the base immediately before it isc.-1, notc.0. Likewise*1is the first base after the stop codon. - Intronic offsets.
c.87+3means "3 bases into the intron after coding position 87";c.88-3means "3 bases before coding position 88". These areoffsetvalues on the position object, not part of the base number.
hgvs models this with BaseOffsetPosition (fields base, offset, datum). Never do naive arithmetic on .base. See SequenceVariant and hgvs Pitfalls.
Insertions are where everyone dies
An insertion has no width, so where does it go?
- HGVS:
c.100_101insA— names the two flanking bases. Unambiguous. - VCF:
POS=100 REF=A ALT=AT— anchors on the preceding base and includes it in both alleles. - BED:
start == end— a zero-width interval.
Converting between these is not a coordinate shift, it is a representation change. Use hgvs Normalizer and a real VCF library rather than string surgery.
Practical defences
- Name your variables
start0/start1, never barestart. - Write one assertion per interval-producing function:
assert end >= startfor half-open,assert end >= startandlength == end - start + 1for closed. - Test with a single-base feature and a two-base feature. Off-by-ones survive tests on long intervals.
- Test at a chromosome start (position 1 / index 0). Underflow shows up there.
See also
SeqFeature and Location · Applied - GFF and Genomic Intervals · Applied - Genomic File Formats · hgvs Pitfalls · HGVS Nomenclature · Indexing and Slicing