gff3-validator
A one-stop validator for GFF3 annotation files, built by FAIR-bioHeaders with the Sequence Ontology (SO) group.
Status: early release (0.1.0). The rule catalogue is a draft under review with the Sequence Ontology group (FHR-Specification #62). The core syntax, attribute, directive and structure rules that the GFF3 specification settles are implemented, and the optional biology rules run with
--genome; Sequence Ontology and header rules are not yet. Install withpip install gff3-validator, or use it in the browser at https://fair-bioheaders.github.io/gff3-validator/. Rule ids, levels and output formats may still change before 1.0.
Goal
One place, one name, three interfaces (web page, CLI, library) that tell you whether a GFF3 file is valid against the GFF3 specification (version 1.26) and the Sequence Ontology, and that explain every problem in plain language with the rule id, the line and a suggested fix. When a file carries a FAIR-bioHeaders (FHGFF3) provenance header, the validator also checks it and its link to the genome. See spec 009 (copied in specs/001-gff3-validator).
Plain GFF3 without a header is a first-class input: core validation does not depend on FAIR-bioHeaders.
Rule catalogue
Every check is a numbered rule in rules/catalogue.yaml, rendered as docs/rules.md. Each rule has an id, level (error, warning, info), layer (core, FHGFF3, biology), a reference into GFF3 1.26 or SO, a bad example and a fix, its implementation status and its SO review state. Open questions for SO are collected in docs/questions-for-SO.md. A suggested fix is guidance only; the validator never rewrites a file.
docs/EVALUATION.md reviews the 2024 FHGFF3 prototype, the LinkML GFF3 model and existing validators (GenomeTools, AGAT, GFF3toolkit, the SO-era Perl validator, NCBI table2asn), with a side-by-side run on small broken files.
Implemented rules
68 of 88 catalogue rules are implemented:
GFF-SYN-001(error): First line is the ##gff-version 3 directiveGFF-SYN-002(error): Only one ##gff-version directiveGFF-SYN-003(error): Feature lines have nine tab-separated columnsGFF-SYN-004(error): Undefined columns are ".", not emptyGFF-SYN-005(error): No unescaped control charactersGFF-SYN-006(warning): File is UTF-8GFF-SYN-007(info): Possible end-of-line commentGFF-SYN-008(error): Percent-encoding is well formedGFF-SYN-009(warning): Characters encoded that need not beGFF-SYN-010(error): seqid uses the allowed charactersGFF-SYN-012(error): type is definedGFF-SYN-013(error): start and end are integersGFF-SYN-014(error): start is at least 1GFF-SYN-015(error): start is not greater than endGFF-SYN-016(error): score is a number or "."GFF-SYN-017(error): strand is +, -, . or ?GFF-SYN-018(error): phase is 0, 1, 2 or "."GFF-SYN-019(error): CDS features have a phaseGFF-ATT-001(error): Attributes are tag=value pairs separated by semicolonsGFF-ATT-002(error): Tags and values are not emptyGFF-ATT-003(info): Empty attribute pair or trailing semicolonGFF-ATT-004(error): Each tag appears once per lineGFF-ATT-005(error): Reserved characters are escaped in tags and valuesGFF-ATT-006(error): Single-valued reserved tags have one valueGFF-ATT-007(warning): Unknown reserved (upper-case) tagGFF-ATT-008(warning): Tag differs from a reserved tag only by caseGFF-ATT-009(info): Quoted attribute valueGFF-ATT-010(error): Target has the form "target_id start end [strand]"GFF-ATT-011(error): Gap is a list of operationsGFF-ATT-012(warning): Gap is given with TargetGFF-ATT-013(warning): Gap lengths agree with the feature and Target lengthsGFF-ATT-014(error): Dbxref and Ontology_term values are DBTAG:IDGFF-ATT-016(warning): Is_circular is "true"GFF-DIR-001(error): ##sequence-region has the form: seqid start endGFF-DIR-002(error): One ##sequence-region per seqidGFF-DIR-003(error): ### closes all forward referencesGFF-DIR-004(error): Only FASTA after ##FASTAGFF-DIR-005(warning): FASTA section starts with ##FASTAGFF-DIR-006(warning): FASTA records are well formedGFF-DIR-007(warning): ##species is an NCBI Taxonomy URLGFF-DIR-008(warning): ##genome-build has a source and a build nameGFF-DIR-009(info): Ontology directives are recorded but not fetchedGFF-DIR-010(info): Unknown directiveGFF-STR-001(error): IDs are unique, except for the lines of one discontinuous featureGFF-STR-002(warning): Lines of one feature share seqid and strandGFF-STR-003(warning): Lines of one feature have the same ParentGFF-STR-004(error): Parent refers to an ID in the fileGFF-STR-005(error): Derives_from refers to an ID in the fileGFF-STR-006(error): No Parent cyclesGFF-STR-007(warning): No Derives_from cyclesGFF-STR-008(error): Features are within their ##sequence-regionGFF-STR-009(info): seqid without ##sequence-regionGFF-STR-010(warning): seqids appear in the ##FASTA sectionGFF-STR-011(error): Features fit within the embedded sequenceBIO-001(error): seqid is in the genomeBIO-002(error): Features fit within the genome sequenceBIO-003(warning): ##sequence-region agrees with the genomeBIO-004(warning): CDS phases are consistentBIO-005(warning): CDS segments lie within exonsBIO-006(warning): CDS starts with a start codonBIO-007(warning): CDS ends with a stop codonBIO-008(warning): No internal stop codonsBIO-009(info): CDS length is a multiple of threeBIO-010(warning): Strand is consistent within a geneBIO-011(info): Biology checks skippedHDR-001(error): FHGFF3 header present when requiredHDR-002(info): FHGFF3 header present but not validatedHDR-003(info): Header checks skipped (--no-header)
Planned interfaces
- CLI
gff3-validate FILE [--genome genome.fa [--translation-table N]] [--format text|json|html|sarif], reading plain, gzip/BGZF or stdin (-) input. Exit codes: 0 no errors, 1 errors found, 2 usage error or unreadable input (the run is then incomplete). - Library
gff3_validator.validate(path)returning findings with rule id, level, line, column, message and fix. - Web page running the same engine in the browser through Pyodide, so files never leave the computer (see Use it in your browser).
- Reports in text, JSON, HTML (one self-contained file) and SARIF 2.1.0 (for code-scanning annotations in CI).
- Repository profiles (NCBI, Ensembl, Alliance) layered on the core rules, later and only with each repository's review.
- Distribution on PyPI and Bioconda, a Galaxy wrapper, an FHR-Nextflow module and a container, after the first reviewed release.
Headers: a FAIR-bioHeaders header is validated when present; --require-header
makes it mandatory and --no-header skips header checks. Header checks need the
optional extra, pip install "gff3-validator[headers]" (FAIR-bioHeaders
toolkit 0.4). FHGFF3 support in the toolkit is not released yet, so the
skeleton only detects #~ header lines and reports that header validation is
not implemented.
Current skeleton
poetry install
poetry run gff3-validate tests/fixtures/valid/canonical_gene.gff3
poetry run gff3-validate --format json annotation.gff3.gz
The reader streams line by line and stops reading features at ##FASTA;
FASTA records are read for their ids and lengths only, never kept. Per-line
rules keep no state. Findings for problems that can occur on most lines
(GFF-SYN-006, GFF-SYN-009, GFF-ATT-003, GFF-ATT-009) are reported once, at
the first occurrence, with a count, so they cannot crowd errors out of the
--max-findings cap.
Memory
The structure rules (IDs, Parent and Derives_from references, cycles, bounds) need state. Memory is O(number of distinct IDs) plus O(number of seqids):
- each ID gets an integer index and four packed array entries (first line, seqid/type/strand combination, first Parent, Parent-list hash); features with several Parents or with Derives_from keep the extra edges in sparse maps;
- features without an ID are not remembered after their line, except for a reference to an ID not yet defined;
- forward references are kept per referenced ID (first line and count), not
per line, and are resolved when the ID appears or at the end of the file;
###only advances a counter, so a reference that crosses it is detected when the ID is finally defined (GFF-DIR-003); - per seqid: its first line, furthest start and end, and at most 100 lines
beyond its
##sequence-regionend (then a count); - Parent and Derives_from cycles are found at the end of the file by an iterative depth-first search (one byte of state per ID, no recursion).
On a synthetic file of 3.1 million lines (143,000 genes with two mRNAs, five exons and four CDS segments each, 2.1 million distinct IDs) a run takes about 22 s and 380 MiB peak RSS on Python 3.13, and 40 s and 430 MiB on Python 3.9 (roughly 170 to 200 bytes per ID). The opt-in test regenerates such a file and reports the numbers:
GFF3_VALIDATOR_PERFORMANCE_TEST=1 poetry run pytest -s tests/test_performance.py
# GFF3_VALIDATOR_PERFORMANCE_LINES=3000000 sets the size (default 2000000)
Biology checks (--genome)
gff3-validate --genome genome.fa.gz annotation.gff3
gff3-validate --genome genome.fa --translation-table 11 bacterium.gff3
The biology rules (BIO-001 to BIO-011) run only with --genome; without it
the report lists the biology layer as not checked. They compare seqids and
lengths with the genome, check CDS phases, CDS within exons, start, stop and
internal stop codons, coding length and strand consistency. Codon checks use
an NCBI translation table: 1 by default (the table is never inferred from the
organism; bacteria, archaea and plastids need --translation-table 11).
Partial CDS have no standard marker in GFF3, so the codon rules are warnings.
Translation exceptions (selenocysteine, pyrrolysine, readthrough, stops
completed by polyadenylation) are exempt when the codon is covered by a
recoded_codon child of the CDS (or a subtype), following the SO/NCBI
discussion in SO-Ontologies#658 (pending SO confirmation), or by a legacy
transl_except attribute.
The genome is a FASTA file, plain or gzip/BGZF (detected from the magic bytes;
not stdin). Leading ; lines, such as a FAIR-bioHeaders ;~ header, are
skipped. The validator reads the file once to build an in-memory index like
samtools faidx (name, length, offset, bases and bytes per line; memory
O(number of sequences)) and then reads CDS bases on demand, in 64 kb blocks
with a 2 MiB cache, so no chromosome is loaded whole. A gzip genome is first
decompressed to an anonymous temporary file in TMPDIR, which needs as much
free disk space as the uncompressed genome and is removed at the end. A genome
that cannot be indexed reliably (sequence before the first >, duplicate
names, lines of different lengths inside a record, non-sequence characters)
stops the run with exit code 2.
CDS segments (four integers per line), the exons of each transcript and the
recoded codons of each CDS are kept until the next ### or the end of the
file, then judged and dropped. With ### after each gene this is a few
genes' worth; without ### it is about 40 bytes per CDS line and 20 bytes per
exon, plus about 300 bytes per transcript. On a synthetic file of 524,000
lines (23,809 genes on both strands, 47,618 CDS of 1,066 codons) with a
231 MiB genome, a run takes about 9 s and 97 MiB peak RSS on Python 3.13
(14 s and 104 MiB on 3.9); without ### lines, 140 MiB. The opt-in
performance test includes this case
(GFF3_VALIDATOR_PERFORMANCE_GENOME_LINES sets its size).
Use it in your browser
https://fair-bioheaders.github.io/gff3-validator/ (once GitHub Pages is
enabled for this repository) runs the validator inside the page: the
gff3-validator wheel built from this repository, in Python compiled to
WebAssembly (Pyodide). Drop or pick a .gff3 or
.gff3.gz file, optionally add a genome FASTA for the biology checks and a
translation table, choose how to treat a FAIR-bioHeaders header, and the page
shows the HTML report and offers it, the JSON and the SARIF report for
download. The reports are the ones gff3-validate --format html|json|sarif
writes for the same file and options; CI checks this on every fixture
(web/test/parity.mjs).
Privacy. Your files are read by the page and never uploaded. After the
page has loaded, the only requests are for the Python runtime and PyYAML from
one pinned Pyodide release on cdn.jsdelivr.net and for the validator wheel
from the page's own site. The page's Content-Security-Policy (a meta tag; the
validator runs in a Web Worker started from a blob: URL, which inherits it)
allows connections only to the site itself and to that Pyodide release, so the
page could not send a file elsewhere even by mistake. pyodide.js and
pyodide-lock.json are loaded with Subresource Integrity (sha384, in
web/pyodide.json); the lock file pins the sha256 of
PyYAML, and the page checks the wheel against the sha256 in its
manifest.json. The rest of the Pyodide runtime (pyodide.asm.mjs, the
WebAssembly binary and the standard library) is loaded by pyodide.js from
the same pinned release without SRI, because Pyodide offers no way to pass
hashes for them.
Size limits. The annotation is read from disk in 1 MiB slices and is never held in memory; memory grows with the number of feature IDs, as on the command line. Measured in headless Chrome (Pyodide 314.0.7, Python 3.14) on the synthetic files of the performance test:
| Input | Time | WebAssembly memory |
|---|---|---|
| 1.1 million lines, 90 MB plain (1.05 million IDs) | 18 s | 155 MiB |
| 3.3 million lines, 28 MB gzip (270 MB uncompressed, 3.15 million IDs) | 56 s | 387 MiB |
| 524,000 lines (42 MB) with a 231 MiB genome, plain or gzip | 18 s | 90 to 108 MiB, plus the genome |
That is two to three times slower than the CLI. A full vertebrate annotation
(about 3 million lines) works. Memory grows by about 120 bytes per ID and
WebAssembly memory cannot exceed 4 GiB (browsers may stop a tab sooner), so
files several times larger than those measured here (not tested) need the
command line. The genome is copied into the page's memory (a gzip genome is also
decompressed there), so genomes of a few hundred MB are fine and multi-GB
genomes are not; use gff3-validate --genome for those.
Build and try the page locally:
poetry run python scripts/build_web.py # builds the wheel and web/dist
python -m http.server -d web/dist 8000 # then open http://localhost:8000/
.github/workflows/pages.yml builds web/dist, runs the parity check and
deploys to GitHub Pages on every push to main (and on demand). To bump
Pyodide, run poetry run python scripts/build_web.py --pin VERSION (it
downloads pyodide.js and pyodide-lock.json of that release, computes the
SRI hashes, checks that PyYAML is included and rewrites web/pyodide.json),
then cd web/test && npm install --save-exact pyodide@VERSION, rebuild and run
node web/test/parity.mjs --python "poetry run python".
Development
See CONTRIBUTING.md. The rule catalogue is the source of
truth: edit rules/catalogue.yaml, then run python scripts/render_rules.py to
regenerate docs/rules.md, the packaged copy and the list above (CI checks for
drift with --check).
License
MIT. Copyright (c) 2026 Adam Wright and David Molik (FAIR-bioHeaders contributors).
Metadata
Release files for gff3-validator 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gff3_validator-0.1.0.tar.gz | 66.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gff3_validator-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 137.2 kB
Release files / gff3_validator-0.1.0.tar.gz
| Download URL | gff3_validator-0.1.0.tar.gz |
|---|---|
| Size | 66.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1ac25dda9af64b41ce25cb6042496493c96838879fac90371ba47cfd657aa24f
|
|
BLAKE2b-256 checksum How to use checksums |
31be073a330b8eb157dbb5659412d1268d6abcfaf9fa39831ec85cc165e823dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency logRelease files / gff3_validator-0.1.0-py3-none-any.whl
| Download URL | gff3_validator-0.1.0-py3-none-any.whl |
|---|---|
| Size | 70.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ea5f457543c11ed5a3ca759eae96daba1574e299cf92b1ddde4db8ffa890dcd6
|
|
BLAKE2b-256 checksum How to use checksums |
4c6152a49d7985d78aac0e1f55408a655b689db11950ef2b3eb9c540f0f2a980
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency log