gtf_pyparser
A lightweight, dependency-free Python library for parsing GTF (Gene Transfer Format) files into a structured object model.
Why gtf_pyparser?
Most GTF parsing libraries are either too heavy (requiring pandas, SQLite, or large C extensions) or return flat data structures that don't reflect the natural hierarchy of genomic annotation. gtf_pyparser gives you a clean Gene → Transcript → features object model with no dependencies beyond the Python standard library.
new V0.3+
added intervall tree support. you can now query genes over a position.
Roadmap
Currently working on direct access / indexing by name and position.
Installation
pip install gtf_pyparser
Position lookups (Gtf.build_intervals / Gtf.get_genes_at_position) require the optional intervaltree dependency:
pip install gtf_pyparser[intervals]
Quick start
import gtf_pyparser
genes = gtf_pyparser.parse_gtf("Homo_sapiens.GRCh38.gtf")
gene = genes["ENSG00000139618"]
print(gene.symbol, gene.start, gene.end)
for transcript_id, transcript in gene.transcripts.items():
exons = transcript.features.get("exon", [])
introns = gtf_pyparser.get_intron(exons)
print(f" {transcript_id}: {len(exons)} exons, {len(introns)} introns")
Coordinate convention
All coordinates are 0-based half-open (start inclusive, end exclusive), consistent with BED and BAM format. GTF files are 1-based inclusive; the conversion is applied automatically during parsing.
Data model
Gtf
├── dict: dict[primary_key, Gene]
│ └── Gene
│ ├── interval: Interval // start, end, chr, strand, phase, attributes.
│ ├── attribute: dict
│ └── transcripts: dict[transcript_id, Transcript]
│ └── Transcript
│ ├── interval: Interval
│ ├── attribute: dict
│ └── features: dict[feature_type, list[Interval]] // exon / CDS / UTR / ...
└── it: dict[chr, IntervalTree] // lazily built, keyed by gene_id
API
Parsing
gtf_pyparser.parse_gtf(gtf_file, primary_key="gene_id")
Parse a GTF file into a dict-like container of Gene objects, keyed by primary_key.
genes = gtf_pyparser.parse_gtf("annotation.gtf")
genes = gtf_pyparser.parse_gtf("annotation.gtf", primary_key="gene_name")
gtf_file— path to the GTF fileprimary_key— attribute used to key the returned container (default:"gene_id")- Returns — a
Gtfobject supporting indexing,in,len, iteration, and.keys()/.values()/.items(), exactly likedict[str, Gene]
gtf_pyparser.get_attr(string)
Parse a raw GTF attribute string into a key-value dictionary. Useful when processing GTF lines outside of the full parser.
attrs = gtf_pyparser.get_attr('gene_id "ENSG00000139618"; gene_name "BRCA2";')
# {"gene_id": "ENSG00000139618", "gene_name": "BRCA2"}
Repeated attribute keys (e.g. multiple tag "..." entries) are collected into a list instead of overwriting each other.
Position lookups
Requires the intervaltree package — see Installation.
Gtf.get_genes_at_position(position, flanking=0) returns the gene_id of every gene overlapping a genomic position. The underlying IntervalTree index is built lazily on first use (or explicitly via Gtf.build_intervals()).
genes = gtf_pyparser.parse_gtf("annotation.gtf")
genes.get_genes_at_position({"chr": "chr1", "start": 100_000, "end": 100_050})
# ["ENSG00000139618"]
genes.get_genes_at_position({"chr": "chr1", "start": 100_000, "end": 100_050}, flanking=500)
position— dict-like withchr,start,endkeys,start < endflanking— bases to extend the query on both sides (default0)- Returns —
list[str]of overlappinggene_ids, orNoneif the chromosome isn't in the GTF or nothing overlaps
Derived features
gtf_pyparser.get_intron(exons)
Derive intron intervals from a list of exon Interval objects belonging to a single transcript. Introns are numbered biologically: from 1 upward on the + strand, and from n down to 1 on the - strand.
exons = transcript.features.get("exon", [])
introns = gtf_pyparser.get_intron(exons)
exons— list ofInterval, need not be pre-sorted- Returns —
list[Interval], empty if fewer than two exons are provided
Position classification
Transcript.classify_position(position, strand, strand_aware=True) and Gene.classify_position(position, strand, strand_aware=True) classify a 0-based genomic position relative to a transcript's (or every transcript of a gene's) exon/intron structure.
gene.classify_position(150, "+")
# {"ENST00000001": "exon", "ENST00000002": "intron"}
Returns one of "geneStart", "geneEnd", "exon", "intron", "junctionDonnor", "junctionAcceptor", or None if the position falls outside the feature's span. Gene.classify_position returns a dict[transcript_id, result] covering every transcript on the gene.
Data classes
Interval
Immutable (frozen) genomic coordinate record — replaced rather than mutated when coordinates need updating.
| Attribute | Type | Description |
|---|---|---|
chr |
str |
Chromosome or sequence name |
start |
int |
0-based start (inclusive) |
end |
int |
0-based end (exclusive) |
strand |
str |
"+", "-", or "." for unstranded |
phase |
int or str |
Reading frame (0, 1, 2), or "." if not applicable |
attribute |
dict |
Key-value pairs from the GTF attribute column |
Other properties: length, position (dict with just chr/start/end/strand).
Methods:
overlaps(other, strand_aware=True, closed=False)/contains(position, strand, strand_aware=True, closed=False)— half-open by default (touching intervals don't overlap;position == endisn't contained); passclosed=Trueto treat both endpoints as inclusive.eq_pos(other)— compare genomic position and strand only, ignoring phase/attribute.clone()— return a copy with its own independentattributedict.to_dict()/Interval.from_dict(dict_)— round-trip through a plain dict.Interval.from_position_str(string, is_one_based=False)— parse a"chr:start-end(strand)"string; passis_one_based=Trueto convert from 1-based inclusive (GTF/samtools region notation) to this class's 0-based half-open convention.
Transcript
Groups all GTF records sharing a transcript_id. The genomic span always reflects the union of all features seen so far.
| Attribute | Type | Description |
|---|---|---|
transcript_id |
str |
Ensembl transcript ID or equivalent |
transcript_symbol |
str or None |
Human-readable transcript symbol |
interval |
Interval |
Current genomic span of the transcript |
features |
dict[str, list[Interval]] |
Feature type → list of intervals |
Convenience properties: start, end, chr, phase, length, attribute (of the transcript's own interval), exons, exons_positions, intron (derived via get_intron), cds, mrna, utr_5p, utr_3p.
Gene
A gene locus containing one or more transcript isoforms.
| Attribute | Type | Description |
|---|---|---|
gene_id |
str |
Primary identifier (e.g. Ensembl gene ID) |
symbol |
str |
Gene symbol, resolved from gene_symbol, gene_name, or gene_id |
interval |
Interval |
Genomic span from the GTF gene record |
transcripts |
dict[str, Transcript] |
Transcript ID → Transcript |
Convenience properties: start, end, chr, phase, length, attribute, biotype (checks biotype, gene_biotype, then gene_type), transcript_names, transcript_length, has_transcript, has_exon, exon / intron (list of (gene_id, transcript_id, ...) tuples across all transcripts).
Interoperating with easyfasta
gtf_pyparser has no dependency on easyfasta (or any other sequence library) — the two interoperate purely by shape. Interval supports dict-style access (interval["start"], interval.get("strand")), which is exactly the protocol easyfasta.fai_common.query_position/query_iter/query_splice expect from a "position": anything with ["chr"], ["start"], ["end"], and an optional ["strand"]. That means Interval objects — including a transcript's .exons — can be passed straight through, no glue code or extra dependency required:
import gtf_pyparser
from easyfasta import fai_common
genes = gtf_pyparser.parse_gtf("annotation.gtf")
transcript = genes["ENSG00000139618"].transcripts["ENST00000001"]
# a single feature
cds_seq = fai_common.query_position("genome.fa", transcript.cds[0])
# concatenate all exons into the spliced transcript sequence
mrna_seq = fai_common.query_splice("genome.fa", transcript.exons)
Interval.to_dict() / Interval.from_dict() round-trip through the same chr/start/end/strand shape, so plain dicts work interchangeably with Interval objects anywhere this protocol is expected.
Logging
Progress and error messages are emitted under the "gtf_pyparser" logger. To suppress informational output:
import logging
logging.getLogger("gtf_pyparser").setLevel(logging.WARNING)
Versioning
gtf_pyparser.__version__ reflects the installed package's version, derived from git tags via setuptools_scm. In an unbuilt source checkout (no install step run yet) it falls back to "unknown".
Development
pip install -e ".[dev]"
pytest
License
MIT
Citation / acknowledgement: Romain Lannes 2026
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gtf_pyparser-0.3.3.tar.gz.
File metadata
- Download URL: gtf_pyparser-0.3.3.tar.gz
- Upload date:
- Size: 26.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b6ff6e2b914c59aec5909542f95e86da32ea24a144bda5123db3f8ec0ce1420
|
|
| MD5 |
b5fa0e61e8f36152ddf1328a9161e201
|
|
| BLAKE2b-256 |
d3e43677a1c89bc29371c652067f091f32f3f78112d2ed645b49ac089d58b65c
|
File details
Details for the file gtf_pyparser-0.3.3-py3-none-any.whl.
File metadata
- Download URL: gtf_pyparser-0.3.3-py3-none-any.whl
- Upload date:
- Size: 22.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a3fa2e2c43830072bab70a72c7f40cbb3c36f5f81fdf22e5ea47607d9cc1f3e1
|
|
| MD5 |
bb3cf150b20f1f3f294881685d80c59a
|
|
| BLAKE2b-256 |
a67aaefea54dca703b5b2214394435ddc9faacd55a3b305004f3e9968e04953e
|