Parses, queries, converts, and extracts from GTF and GFF3 gene-model annotation files - walking the gene/transcript/exon/CDS hierarchy with gffutils (queryable SQLite DB), converting formats and extracting transcript/CDS/protein FASTA with gffread, slurping to dataframes with gtfparse/pyranges, and sanitizing malformed files with AGAT. Covers the 1-based-inclusive vs 0-based BED coordinate conversion (start-1 only), deriving implicit features (introns/UTRs/TSS), phase-not-frame, the stop-codo...
Scanned 9/5/2026
Install to Claude Code
npx -y skills add FridrichMethod/awesome-skills --skill gtf-gff-handling --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Gtf Gff Handling?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/fridrichmethod-gtf-gff-handling)More formats (shields.io, HTML) on the badges page.
---
name: bio-genome-intervals-gtf-gff-handling
description: Parses, queries, converts, and extracts from GTF and GFF3 gene-model annotation files - walking the gene/transcript/exon/CDS hierarchy with gffutils (queryable SQLite DB), converting formats and extracting transcript/CDS/protein FASTA with gffread, slurping to dataframes with gtfparse/pyranges, and sanitizing malformed files with AGAT. Covers the 1-based-inclusive vs 0-based BED coordinate conversion (start-1 only), deriving implicit features (introns/UTRs/TSS), phase-not-frame, the stop-codon-in-or-out-of-CDS convention, and the chr1-vs-1 seqid and gene-ID-version mismatches that silently produce all-zero count matrices and dropped joins. Use when extracting features or sequences from an annotation, converting GTF<->GFF3 or GTF->BED, traversing the gene tree, or diagnosing a coordinate/provenance mismatch upstream of counting or DE.
tool_type: mixed
primary_tool: gffutils
---
## Version Compatibility
Reference examples tested with: gffutils 0.13+, gffread 0.12+, gtfparse 2.x, pyranges 0.1+ (or 1.0+ - see note), AGAT 1.4+.
Before using code patterns, verify installed versions match. If versions differ:
- CLI: `<tool> --version` then `<tool> --help` to confirm flags
- Python: `pip show <package>` then `help(module.function)` to check signatures
Two version landmines specific to this skill: (1) **gtfparse changed its return type** - older releases returned a pandas DataFrame, gtfparse >=2.x returns a **polars** DataFrame by default; pass `result_type='pandas'` before chaining pandas idioms (`.copy()`, boolean masks). (2) **pyranges has a major-version API split** - pyranges 0.x and the 1.0 rewrite differ in method names and attribute access; check `import pyranges; pyranges.__version__` before pasting code. gffutils stores **1-based** coordinates while pyranges stores **0-based** - their `start` fields differ by one by design. If code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt rather than retrying.
# GTF/GFF Handling
**"Pull these features (or their sequences) out of my annotation, convert it, or find out why my counts are wrong."** -> Treat the file as a serialized gene-model tree: walk gene->transcript->exon/CDS, derive implicit features, and reconcile coordinate and namespace conventions before trusting any number.
- CLI: `gffread in.gtf -T -o out.gtf` (convert), `gffread -w tx.fa -g genome.fa in.gtf` (FASTA), `agat_convert_sp_gxf2gxf.pl` (sanitize)
- Python: `gffutils.create_db(...)` then `db.children(gene, featuretype='exon')` (tree query); `gtfparse.read_gtf(..., result_type='pandas')` / `pyranges.read_gtf(...)` (dataframe)
## The Single Most Important Modern Insight -- A GTF/GFF3 Is a Serialized Gene-Model Tree, Not a Table of Intervals
Almost every painful bug here comes from the file looking like a CSV while behaving like a tree, or from a coordinate/provenance mismatch the tools never warn about - and **every one of these failures is silent**: nothing throws, the wrong answer just propagates. Three load-bearing facts the tutorials skip:
1. **The coordinate conversion is asymmetric.** GTF/GFF3 are 1-based fully inclusive `[start, end]`; BED (and pyranges-internal) are 0-based half-open `[start-1, end)`. Convert to BED by **subtracting 1 from the start only - the end is unchanged** (the inclusive 1-based end and the exclusive 0-based end are the same integer). Doing `start-1` AND `end-1` shifts the feature one base left and is the classic over-correction: invisible in coverage/overlap, catastrophic in CDS translation (a one-base frameshift garbles the protein). gffutils keeps 1-based, pyranges stores 0-based, so their `start` fields differ by one *correctly* - never "fix" that discrepancy.
2. **The all-zero count matrix.** featureCounts/htseq-count match a read to a feature by **string equality on the chromosome name**, so `chr1` != `1` != `NC_000001.11` produces a perfectly well-formed matrix of **zeros with no error or warning** - the only signal is `~0%` assigned in the summary. Same bug one altitude up: gene-ID version suffixes (`ENSG00000223972.5` vs `ENSG00000223972`) silently drop rows on an annotation join. Audit every cross-file key (chromosomes between BAM/GTF/FASTA, gene IDs between GTF/count-matrix/annotation) by **set intersection, never by eye**, before any count or join.
3. **phase is not frame, and the stop codon is a 3-bp ghost.** Phase (column 8) is the strand-aware count of bases to trim from the segment's transcriptional 5' end to reach the next codon (0/1/2) - **recompute it on any CDS edit** (AGAT/gffread do; hand-editing coordinates without fixing phase frameshifts the translation). GTF (Ensembl/GENCODE) **excludes** the stop codon from CDS; GenBank/GFF3 often **include** it - so a CDS length off by exactly 3 nt (or a protein +/-1 stop) between two sources is a convention mismatch, not a bug.
## Tool Taxonomy
| Tool | Role | Mechanism | When |
|------|------|-----------|------|
| gffutils | Queryable gene-tree DB (Python) | builds a SQLite DB; `children`/`parents`/`region` traverse the hierarchy; keeps 1-based coords | walk gene->transcript->exon/CDS, derive introns, query by ID/coordinate |
| gffread | Converter + sequence extractor (CLI) | fast C++, genome-aware; one binary | GTF<->GFF3, extract transcript/CDS/protein FASTA, region filter |
| pyranges | Vectorized interval engine (Python) | PyRanges/pandas-like; stores 0-based half-open | overlap joins, set ops, dataframe-native interval work |
| gtfparse | GTF -> dataframe (Python) | one call explodes column 9 into attribute columns | quick column/filter work; NOT hierarchy-aware (flat table) |
| AGAT | GFF/GTF sanitizer (Perl CLI) | reconstructs the full tree; adds missing features, fixes IDs/phase, deflates attributes | a malformed/non-standard file - run FIRST, before parsing |
## Decision Tree by Scenario
| Scenario | Recommended | Why |
|----------|-------------|-----|
| Walk the gene/transcript/exon hierarchy, derive introns | gffutils `create_db` + `children`/`parents` | the hierarchy is the point; flat parsers lose it |
| Convert GTF<->GFF3 or extract transcript/CDS/protein FASTA | gffread (`-T`, `-w`/`-x`/`-y -g`) | genome-aware, knows the stop-codon convention |
| Quick column/filter on a clean modern GTF | gtfparse (`result_type='pandas'`) | one-call dataframe; verify return type first |
| Overlap/set ops, large in-memory interval joins | pyranges | vectorized; route arithmetic -> interval-arithmetic |
| File malformed: no `##gff-version`, missing gene/exon lines, dup IDs, mixed conventions | AGAT `agat_convert_sp_gxf2gxf.pl` first | sanitize once vs writing a brittle parser around it |
| GTF -> BED for bedtools | `start-1`, end unchanged (-> bed-file-basics) | the off-by-one boundary is where it bites |
| Counts came out all-zero or DE join dropped rows | intersect seqid / gene-ID namespaces | string-equality match; no error is emitted |
| Counting reads per gene/feature | -> rna-quantification/featurecounts-counting | the seqid/strand landmines live there; set `-s` from chemistry |
| Judge whether the annotation itself is sound | -> genome-annotation/annotation-qc | this skill operates on the file, not its quality |
## Walk the Gene Tree and Derive Introns (gffutils)
**Goal:** Traverse gene -> transcript -> exon and reconstruct features (introns) that the file does not store explicitly.
**Approach:** Build a SQLite DB once (disabling gene/transcript inference when those lines already exist, for a ~100x speedup), then query children ordered by position and synthesize introns from the exon gaps.
```python
import gffutils
# disable_infer_* is GTF-only and applies when gene/transcript lines ALREADY exist (modern GENCODE/Ensembl) -> ~100x faster
db = gffutils.create_db('annotation.gtf', 'annotation.db', force=True,
disable_infer_genes=True, disable_infer_transcripts=True,
merge_strategy='create_unique')
gene = db['ENSG00000141510'] # gffutils returns 1-based coords (raw record)
for tx in db.children(gene, featuretype=['mRNA', 'transcript'], order_by='start'):
exons = list(db.children(tx, featuretype='exon', order_by='start'))
introns = list(db.interfeatures(exons, new_featuretype='intron')) # introns are not stored - derived from exon gaps
print(tx.id, len(exons), 'exons', len(introns), 'introns')
```
A modern GTF without `disable_infer_*` triggers the slow inference/merge machinery; an *older* minimal GTF lacking gene/transcript lines needs inference ON so gffutils reconstructs the envelopes. Match the flag to the file. introns, UTRs (`exon - CDS`), and TSS are derived, not stored - never infer biological absence from a missing feature line.
## Convert Formats and Extract Sequences (gffread)
gffread is genome-aware and respects the stop-codon convention, so it is the safe path for sequence extraction (naive coordinate math is not).
```bash
gffread annotation.gff3 -T -o annotation.gtf # GFF3 -> GTF2 (default output is GFF3)
gffread -w transcripts.fa -g genome.fa annotation.gtf # spliced exon (mature transcript) FASTA
gffread -x cds.fa -g genome.fa annotation.gtf # spliced CDS nucleotide FASTA
gffread -y proteins.fa -g genome.fa annotation.gtf # translated-CDS protein FASTA
gffread annotation.gtf -C -o coding.gtf # keep only coding transcripts
```
`-g` needs the genome FASTA (gffread auto-creates the `.fai`). `-w`/`-x`/`-y` splice the segments per transcript, so they handle multi-exon models correctly - do not concatenate exon FASTAs by hand.
## Convert GTF to BED with the Right Coordinate Shift
**Goal:** Emit a BED of a chosen feature type for bedtools, without the off-by-one frameshift.
**Approach:** Parse to a pandas frame, filter to the feature type, subtract 1 from the start *only*, leave the end untouched.
```python
import gtfparse
df = gtfparse.read_gtf('annotation.gtf', result_type='pandas') # gtfparse >=2.x defaults to POLARS - force pandas
genes = df[df['feature'] == 'gene'].copy()
genes['start'] = genes['start'] - 1 # 1-based inclusive -> 0-based half-open: START ONLY
bed = genes[['seqname', 'start', 'end', 'gene_id', 'score', 'strand']]
bed.to_csv('genes.bed', sep='\t', header=False, index=False)
```
For TSS/promoter derivation (strand-aware: `+` strand TSS = start, `-` strand TSS = end), route to proximity-operations - the promoter window is an imposed definition, not an annotated feature.
## Sanitize a Malformed File First (AGAT)
When a file lacks `##gff-version 3`, has non-Sequence-Ontology types, is missing `gene`/`exon`/UTR lines, has duplicate IDs, or mixes conventions, sanitize it once rather than coding around it:
```bash
agat_convert_sp_gxf2gxf.pl -g messy.gff3 -o clean.gff3 # adds missing ID/Parent + features, fixes dup IDs, recomputes phase, sorts
agat_convert_sp_gff2gtf.pl -g clean.gff3 -o clean.gtf # GFF3 -> GTF (collapses level1->gene, level2->transcript)
```
AGAT *makes decisions* (which convention to standardize to, how to derive missing features) - usually a feature, but when a source's exact encoding must be preserved (e.g. auditing a submission), inspect what it changed rather than trusting blindly.
## Per-Method Failure Modes
### Over-correcting the coordinate conversion
**Trigger:** subtracting 1 from both start and end when converting to BED. **Mechanism:** only the start representation differs; the inclusive 1-based end equals the exclusive 0-based end. **Symptom:** every feature shifted one base left; invisible in coverage, frameshifts CDS translation. **Fix:** `start-1`, end unchanged.
### Comparing coordinates across gffutils and pyranges
**Trigger:** asserting equality on `start` fields from both libraries in one script. **Mechanism:** gffutils keeps 1-based, pyranges stores 0-based. **Symptom:** an off-by-one that looks like a bug; "fixing" it introduces a real error. **Fix:** confirm each library's convention; expect the difference.
### All-zero count matrix (seqid mismatch)
**Trigger:** BAM aligned to `chr1`, GTF annotated with `1`. **Mechanism:** counters match reads to features by chromosome-name string equality. **Symptom:** well-formed matrix of zeros, no error; `~0%` assigned in the summary. **Fix:** intersect the BAM `@SQ`/idxstats chromosome set with the GTF column-1 set programmatically; remap one namespace, re-confirm.
### Dropped rows on a gene-ID join
**Trigger:** count matrix keyed `ENSG...` joined to annotation keyed `ENSG....5`. **Mechanism:** exact string match on a versioned vs unversioned ID. **Symptom:** join returns a dataframe but rows vanish / annotation is NA. **Fix:** strip `.\d+$` on both sides for matching; keep the version in the stored annotation for provenance.
### CDS length off by exactly 3
**Trigger:** comparing CDS/protein length across two sources, or translating after a convention-flipping conversion. **Mechanism:** GTF excludes the stop codon from CDS; GenBank/GFF3 often include it. **Symptom:** length differs by 3 nt / 1 aa; protein does/does not end in `*`. **Fix:** suspect the convention before debugging code; extract CDS with gffread/AGAT, which know it.
### Editing CDS coordinates without recomputing phase
**Trigger:** trimming/merging/lifting CDS coordinates, leaving column 8 as-is. **Mechanism:** phase is a static integer; the chain of per-segment phases depends on cumulative coding length. **Symptom:** downstream translation (gffread `-y`, table2asn, EMBL) frameshifts or rejects. **Fix:** treat a CDS edit + phase recompute as one atomic operation; let AGAT/gffread recompute.
### gffutils pathologically slow on a modern GTF
**Trigger:** `create_db` on a GENCODE/Ensembl GTF without the infer flags. **Mechanism:** gffutils infers gene/transcript envelopes and runs the merge machinery. **Symptom:** create_db hangs for many minutes. **Fix:** `disable_infer_genes=True, disable_infer_transcripts=True` when those lines already exist (~100x faster).
## Quantitative Thresholds
| Convention / threshold | Source | Rationale |
|------------------------|--------|-----------|
| GTF/GFF3 1-based inclusive; convert to BED with start-1, end unchanged | UCSC/SO format specs | the inclusive 1-based end == the exclusive 0-based end; over-correcting both ends frameshifts CDS |
| CDS length differs by exactly 3 nt between sources | GTF vs GenBank/GFF3 stop-codon convention | GTF (Ensembl/GENCODE) excludes the stop from CDS; GenBank/GFF3 often include it |
| `disable_infer_*` -> ~100x create_db speedup | gffutils docs | inference/merge machinery is skipped when gene/transcript lines already exist |
| seqid intersection required before counting | featureCounts/htseq string-equality match | non-overlapping chromosome names -> all-zero matrix with no error |
| Strip `.\d+$` from gene IDs on both sides before a join | Ensembl/GENCODE/RefSeq versioned accessions | version suffix tracks model revision; mismatch drops rows silently |
| featureCounts default `-s 0` (unstranded) vs htseq-count `-s yes` (stranded) | tool defaults (Liao 2014; Anders 2015) | switching tools changes the counting model; set `-s` from library chemistry, not the default |
## Common Errors
| Error / symptom | Cause | Solution |
|-----------------|-------|----------|
| All genes count zero | seqid mismatch (`chr1` vs `1` vs `NC_...`) | intersect BAM and GTF chromosome sets; remap one namespace |
| Counts low and flip when `-s` changes | wrong strandedness (featureCounts vs htseq defaults differ) | set `-s` from the library prep chemistry; verify assignment rate |
| Join drops rows / NA annotation | gene-ID version suffix (`ENSG....5` vs `ENSG...`) | strip `.\d+$` on both sides for matching |
| Biotype filter returns empty | attribute key differs by source: GENCODE uses `gene_type`, Ensembl/RefSeq use `gene_biotype` | check the actual key (it travels with the `chr1`-vs-`1` provenance split); query the present key |
| Translated protein is garbage | over-corrected coordinate (`start-1` AND `end-1`) | subtract 1 from start only |
| CDS/protein off by 3 nt / 1 aa | stop-codon-in-or-out-of-CDS convention | extract with gffread/AGAT; do not debug coordinate math |
| gtfparse pandas idioms raise AttributeError | gtfparse >=2.x returns polars | pass `result_type='pandas'` |
| pyranges AttributeError | 0.x vs 1.0 API mismatch | check `pyranges.__version__`; use matching method names |
| gffutils create_db hangs | infer machinery on a modern GTF | set `disable_infer_genes=True, disable_infer_transcripts=True` |
| `gffread -w/-x/-y` errors | missing or unindexed genome FASTA | pass `-g genome.fa` (gffread creates the `.fai`) |
## References
- Pertea G, Pertea M. 2020. GFF Utilities: GffRead and GffCompare. *F1000Research* 9:304.
- Stovner EB, Saetrom P. 2020. PyRanges: efficient comparison of genomic intervals in Python. *Bioinformatics* 36:918-919.
- Dale R. gffutils: GFF and GTF file manipulation and interconversion. Software, https://github.com/daler/gffutils (no journal publication).
- Dainat J. AGAT: Another Gff Analysis Toolkit to handle annotations in any GTF/GFF format. Zenodo. doi:10.5281/zenodo.3552717.
- Rubinsteyn A, et al. gtfparse: parsing tools for GTF (gene transfer format) files. Software, https://github.com/openvax/gtfparse (no journal publication).
- Liao Y, Smyth GK, Shi W. 2014. featureCounts: an efficient general purpose program for assigning sequence reads to genomic features. *Bioinformatics* 30:923-930.
- Anders S, Pyl PT, Huber W. 2015. HTSeq - a Python framework to work with high-throughput sequencing data. *Bioinformatics* 31:166-169.
## Related Skills
- bed-file-basics - BED format and the coordinate conversion this skill feeds into
- interval-arithmetic - Set operations on the features extracted here
- proximity-operations - Strand-aware TSS/promoter derivation from extracted features
- rna-quantification/featurecounts-counting - Consumes the GTF/GFF features; the seqid/strand landmines live there
- genome-annotation/functional-annotation - Downstream of feature/sequence extraction from the annotation
- genome-annotation/annotation-qc - Judges whether the annotation this skill parses is sound
- differential-expression/de-results - Map gene coordinates back to DE results
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!