Conversion · GTF → GFF3
How to Convert GTF to GFF3 (and Why IDs Go Missing)
gffread reconstructs the Parent hierarchy GFF3 demands, but only if your GTF's gene_id and transcript_id were consistent to begin with.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 3 min read
- GTF
- .gtf, .gtf.gz · coordinates: 1-based-closed
- GFF3
- .gff3, .gff · coordinates: 1-based-closed
You need this conversion when a GTF-based pipeline (STAR, featureCounts, Cell Ranger) hands off to a tool or database that enforces the stricter GFF3 spec: NCBI submission, a non-model-organism annotation tool, a genome browser that wants explicit Parent chains instead of GTF's implicit gene_id/transcript_id linking. gffread is the standard tool for this and it does real work, not just a text substitution: it reads the gene_id and transcript_id on every GTF line and re-emits them as explicit ID and Parent attributes, in unquoted, URL-encoded key=value form.
What changes along the way: GTF restricts the feature-type column to fewer than ten values; GFF3 allows any Sequence Ontology term. GTF excludes the stop codon from the terminal CDS; GFF3 includes it, shifting CDS length by 3 bp for the last exon of coding transcripts. Optional attributes (gene_name, gene_biotype) and non-transcript features survive a plain conversion but get stripped if you run gffread in its minimalist -E mode.
The most common way this goes silently wrong: gffread reconstructs Parent relationships from whatever gene_id/transcript_id it finds, but it never invents structure that isn't there. A transcript-only GTF with no gene lines converts cleanly into a GFF3 with no gene lines, and the conversion won't error, it just quietly produces a file that fails the next tool that requires a gene feature. Versioned Ensembl IDs (ENSG00000141510.11) are carried through as-is, so if they get dropped it's because they were inconsistent in the source GTF, not because gffread stripped them.
The commands
Type your file names once; every command below updates.
01gffreadv0.12.7
bashgffread sample.gtf -o sample.gff3No format flag defaults gffread to GFF3 output; it reads gene_id and transcript_id off every GTF line and re-emits them as explicit ID/Parent pairs, nesting exons and CDS under their transcript and transcripts under their gene. Assumes every relevant line in {input} carries a gene_id and transcript_id that resolve consistently; if the GTF is transcript-only with no gene lines, gffread will not invent gene records, it just builds what the attributes support.
02gffreadv0.12.7
bashgffread -E sample.gtf -o sample.gff3-E prints parser warnings to stderr (duplicate IDs, malformed attributes, unrecognized feature types) as it converts, the fastest way to check whether {input} is actually spec-compliant GTF. Trade-off: this mode also produces a minimalist GFF3, keeping only transcript-level records and dropping optional attributes like gene_name or gene_biotype, so treat it as a diagnostic pass, not necessarily your final file.
03gffreadv0.12.7
bashgffread sample.gff3 -T -o roundtrip.gtfRound-trips the GFF3 you just made (the {output} of the conversion step) back to GTF2 with -T. Diff feature-type counts between roundtrip.gtf and your original {input} to confirm the conversion didn't silently drop gene, transcript, or exon records.
Coordinates, strand, names, builds
Both GTF and GFF3 use 1-based, closed coordinates, so gffread does no coordinate math when converting between them, and strand uses the same +/-/. convention in both. What actually changes: attribute syntax (quoted gene_id "x"; pairs become unquoted, URL-encoded ID=x;Parent=y), the feature-type vocabulary (GTF's fewer than ten allowed types vs GFF3's full Sequence Ontology), and stop-codon handling (GTF excludes the stop codon from the terminal CDS, GFF3 includes it, shifting CDS length by 3 bp on coding transcripts). Chromosome naming (chr1 vs 1, chrM vs MT) and genome build are not touched by the conversion at all; whatever naming and build your GTF used, the GFF3 inherits verbatim, so mismatches upstream stay mismatches downstream. Running gffread with -E also drops non-transcript features and optional attributes entirely, which is metadata loss worth knowing about before you pick that mode.
Check the output before you trust it
01Feature-type counts match between input and output
bashawk -F"\t" '{print $3}' input.gtf | sort | uniq -c awk -F"\t" '{print $3}' output.gff3 | sort | uniq -cExpected Gene, transcript, exon, and CDS counts are the same (or the difference is explainable, e.g. -E dropped non-transcript features); a big unexplained drop means records were lost.
02Gene ID version suffixes are intact
bashgrep -m5 'ID=gene' output.gff3Expected IDs look like ID=ENSG00000141510.11, with the version suffix present, not truncated to ENSG00000141510.
03Every mRNA/transcript has a Parent that resolves to an existing gene ID
bashcomm -23 <(grep -oP 'Parent=\K[^;]+' output.gff3 | sort -u) <(grep -oP 'ID=\K[^;]+' output.gff3 | sort -u)Expected Empty output; any line printed is a Parent reference with no matching ID, i.e. a broken hierarchy.
04Chromosome naming matches your reference/BAM
bashcut -f1 output.gff3 | sort -u | headExpected Naming convention (chr1 vs 1, chrM vs MT) matches what your FASTA and BAM already use, not a different build's convention.
05Gene features exist if your downstream tool requires them
bashawk -F"\t" '$3=="gene"' output.gff3 | wc -lExpected A nonzero count. Zero means the source GTF was transcript-only and gffread did not add gene records, which will break tools that require them.
Errors you will see, and what they mean
- Downstream tool that expects gene features finds none and fails or silently returns empty results
- Cause: The source GTF was transcript-only, with no gene feature lines; gffread reconstructs Parent hierarchy from what's present but never fabricates missing gene records. Fix: Check for gene lines before converting (`awk -F"\t" '$3=="gene"' input.gtf | wc -l`); if zero, get a GTF that actually includes gene records (e.g. a fresh GENCODE/Ensembl download) rather than expecting the conversion to add them.
- Gene IDs that matched fine in the GTF fail to join downstream after GFF3 conversion (ENSG00000141510 vs ENSG00000141510.11)
- Cause: Version suffixes on gene_id/transcript_id were inconsistent or missing in the source GTF, or the GFF3 was cross-referenced against a different annotation release than the original GTF. Fix: Extract and confirm versioned IDs before converting using the awk pattern from the tx2gene lesson, and keep the GTF and any GFF3 you compare it to from the same annotation release.
- Only a fraction of expected mitochondrial or ribosomal genes show up after filtering the converted file by ID
- Cause: MT- vs mt- and chrM vs MT naming differs by species and database; a filter written for human (MT-) matches nothing in a mouse-derived GFF3 (mt-), or the reverse. Fix: Check actual naming in the converted file (`grep -i '^chrM\|^MT' output.gff3 | head`) before writing ID filters, and match case and prefix to the species and database in use.
- GFF3 has far fewer records than the original GTF, or attributes like gene_name and gene_biotype are missing
- Cause: The conversion was run with `gffread -E`, which by design keeps only transcript-level records and drops optional attributes to produce a minimalist file. Fix: Drop -E for the full conversion if you need the complete structure and attributes, and use -E separately, just for its stderr warnings, as a diagnostic pass on the input.
- Same gene symbol maps to two different Ensembl IDs, or one ID maps to two symbols, after joining GFF3-derived IDs to a symbol table
- Cause: This is a property of the ID systems themselves: annotation releases and alternative naming mean Ensembl IDs and gene symbols are not a stable one-to-one pair, not a defect introduced by the conversion. Fix: Do symbol lookups with a proper mapping tool (biomaRt, NCBI Gene) keyed on the versioned ID from the GFF3, rather than a manual symbol match.
- Gene names like MARCH1 or SEPT1 show up as dates (1-Mar, 1-Sep) after opening the converted file
- Cause: Excel autoformats certain gene symbols as dates; this corrupts attribute values in a GFF3 just as it does any spreadsheet column of gene symbols. Fix: Never open a GTF, GFF3, or anything derived from their attribute columns in Excel; parse with awk, R, or Python instead.
Questions people ask
- Does gffread add missing gene features when converting GTF to GFF3?
No. If the source GTF only has transcript, exon, and CDS lines and no gene lines, gffread reconstructs Parent relationships from what exists but does not fabricate gene records. Any downstream tool that requires an explicit gene feature will still fail on the converted GFF3.
- Will my Ensembl gene ID version numbers (like ENSG00000141510.11) survive the conversion?
Yes, as long as gene_id and transcript_id carry the version suffix consistently on every relevant line in the source GTF. gffread passes attribute values through as-is; the failure mode is an inconsistent or malformed source GTF, not the conversion tool stripping versions.
- What's the difference between plain `gffread -o` and `gffread -E -o` for this conversion?
Plain conversion keeps the full gene/transcript/exon/CDS hierarchy and optional attributes.
-Eproduces a minimalist GFF3 with transcript-level records only and optional attributes dropped, but it prints parser warnings about malformed or non-compliant input, which makes it useful as a first diagnostic pass, not necessarily your final output.- Do coordinates shift when converting GTF to GFF3?
No. Both formats use 1-based closed coordinates, so gffread does no coordinate math during conversion. What changes is attribute syntax, the feature-type vocabulary, and how the stop codon is represented in the terminal CDS.
- Why does a downstream tool fail even though the GFF3 file looks fine?
Usually it's chromosome naming (chr1 vs 1, chrM vs MT) or species-specific gene prefixes (MT- vs mt-) mismatching between your GFF3 and your reference or BAM, not a defect in the conversion. Grep the actual naming in the output before assuming the file itself is broken.
Related reading on the blog
Sources
- GffRead Examples and Documentation — gffread conversion syntax and options, including -E and -T
- GTF/GFF Format Differences - AGAT Documentation — Feature-type vocabulary limits, attribute syntax, and stop-codon handling differences
- GFF3 Specification - The Sequence Ontology — Authoritative spec for ID and Parent attribute requirements, multi-parent features
- GFF3 Format - NCBI Documentation — NCBI's GFF3 implementation details, ID uniqueness scope