ontolint
ontolint is an offline ontology-quality linter for RDF graphs. It provides
graph-local checks for RDF/OWL, SKOS, lexical quality, class hierarchies, and
optional SHACL validation. It is a practical review aid—not a substitute for
OWL 2 DL validation, a reasoner, or expert ontology review.
What it checks
239 of the 294 catalogue rules are implemented; the rest are planned. The rule reference documents each one with an example.
- 227 Python graph checks for metadata, identifiers, IRIs, labels and lexical quality, class hierarchies, OWL axioms and class expressions, SKOS, SHACL shape quality, governance, ontology metrics, mapping quality, input-safety hygiene and, opt-in, OBO Foundry and FAIR publishing conventions.
INPUT001for RDF parsing failures.SHACL001for optional pySHACL validation results.TOOL001for configuration and execution failures.SEM001for opt-in ROBOT OWL-profile validation failures.SEC001,SEC002,SEC003,SEC006,SEC009,SEC010,SEC011andSEC013for unsafe inputs, rejected by the loader before linting (see below).
Each finding includes a rule identifier, severity, focus, message, suggested remediation, and confidence level. Rules are policy-oriented: a finding is not automatically a logical inconsistency.
Requirements and installation
Python 3.13 or later is required. ontolint is published on
PyPI and installs a command named ontolint.
Install it as a standalone tool, isolated from your other Python packages:
# uv (recommended)
uv tool install ontolint
# or pipx
pipx install ontolint
# or into the current environment
python -m pip install ontolint
Run it once without installing it:
uvx ontolint ontology.ttl
Upgrade with uv tool upgrade ontolint, pipx upgrade ontolint or
python -m pip install --upgrade ontolint. Check the installation with:
ontolint --version
ontolint --help
python -m ontolint works as well. Docker is needed only for --owl-profile.
To try the unreleased development version, install from GitHub instead, for
example uv tool install git+https://github.com/arnaudgelas/ontolint. From a
clone of the repository, uv tool install . installs the checkout and
uv tool install --editable . keeps it linked to your working copy.
Development setup
uv sync --group dev
In a development checkout run the command through uv, for example
uv run ontolint ontology.ttl. For a standard pip environment, install the
package and its test tools:
python -m pip install -e .
python -m pip install pytest hypothesis pytest-gremlins pytest-xdist
Run the linter
Use the library directly when integrating ontolint into Python applications:
from rdflib import Graph
from ontolint import Linter
graph = Graph().parse("ontology.ttl")
findings = Linter(graph, base="https://example.org/ont#").run()
The command-line interface is a thin wrapper around the same Python library
(in a development checkout, prefix the commands with uv run):
# Text report
ontolint ontology.ttl \
--base 'https://example.org/ont#'
# JSON report written to a file
ontolint ontology.ttl \
--base 'https://example.org/ont#' \
--format json --output report.json
# Fail a CI job for warning- or error-level findings
ontolint ontology.ttl --fail-on warning
Supported report formats are text, json, and sarif.
| Exit code | Meaning |
|---|---|
0 |
No finding meets the configured --fail-on threshold. |
1 |
At least one finding meets the threshold. |
2 |
Tool failure: unreadable input or configuration, an unknown rule selector, a usage error, or a requested rule that failed or is unsupported (for example --owl-profile without Docker). |
Exit 2 outranks 1: when a requested backend did not deliver a verdict the run cannot
pass, even with --fail-on never. The report is still written so you can see why. Rules that
are skipped (no shapes supplied, no OWL profile requested, ignored) never change the exit code.
Discovering rules
ontolint --version
ontolint --list-rules # table
ontolint --list-rules --format json # machine-readable
ontolint --explain SHQ001 # title, severity, fix, backend, scope, requirements
--list-rules shows the catalogue status of every rule: implemented (can be selected and
run) or proposed (planned only; it has no severity or scope, cannot be selected and never
runs). This is different from the per-run execution status below. --explain on an unknown
identifier exits 2.
Selecting rules and profiles
--select and --ignore take rule identifiers (ONT001) or family prefixes (ONT),
case-insensitively, comma-separated and/or repeated. Unknown selectors, empty entries
(ONT,) and proposed-only rules are errors (exit 2), so a typo never becomes an empty run.
--profile picks a bundle of defaults:
| Profile | Rules | Severity overrides | Default --fail-on |
|---|---|---|---|
minimal |
ONT, IRI, DATA, SKOS, SHACL, SEM (structural integrity) |
none | error |
recommended |
all (same as no profile) | none | error |
ci |
all | none | warning |
strict |
all except SEC007 |
ONT003 becomes error |
info (any finding fails) |
obo |
all, plus the opt-in OBO family |
none | warning |
fair |
all, plus the opt-in FAIR family |
none | warning |
Opt-in rule families. The OBO family (OBO Foundry conventions) and the FAIR family (offline publishing
checks) never run by default: it is excluded from running without a profile and from
recommended, ci, strict and minimal. Enable them with --profile obo,
--profile fair, --select OBO, --select FAIR or a single id such as --select OBO003. --list-rules and --explain mark these
rules as opt-in. An explicit --select replaces a profile's selection, so
--profile obo --select LEX runs only LEX.
Precedence when options combine:
- The rule set is
--selectif given, else the profile's rules, else all rules.--selectreplaces the profile's selection. - Ignores always win over selection:
--ignore, the profile's ignores and the policy file'signored_rulesare unioned and removed. Policyignored_rulesremain exact identifiers. - Severity overrides: profile first, then the policy file's
severity_overrides(policy wins). --fail-on: the command line, else the profile's value, elseerror.
Other policy values (base, languages, thresholds) behave as before: explicit CLI options beat the policy file.
Rule execution status
JSON and SARIF always say what happened to every selected rule, so "no findings" can never be
confused with "not checked": executed, skipped (not applicable or ignored, with the
reason), unsupported (backend unavailable, e.g. no pyshacl or Docker) or failed
(the rule or backend raised). Rules that were deselected by --select/profile are simply
absent. Text output stays unchanged unless a rule was ignored, unsupported or failed, in which
case a Rule execution: block is appended.
- JSON keeps
summaryandfindingsand addsschema_version(currently1), apathon each finding,rules(id, family, title, effective and default severity, category, backend, scope, requirements, suggestion),execution_summaryand per-filefiles[].executions. - SARIF 2.1.0 adds
tool.driver.rules(name, short/full description, help,defaultConfiguration.level, category/backend/scope properties),ruleIndexand anartifactLocationon every result, and a runinvocation. Skipped, unsupported and failed rules aretoolExecutionNotifications(levelsnote,warning,error);executionSuccessfulisfalsewhen any rule failed or is unsupported. - SARIF findings for Turtle, N3, N-Triples and N-Quads inputs include the source line and column of their focus term when it can be resolved. RDF/XML and JSON-LD findings retain their artifact URI but omit a region rather than reporting an unreliable location.
Several files
ontolint ontologies/*.ttl --format sarif --output findings.sarif
- Globbing is the shell's job (on Windows
cmd/PowerShell pass the pattern through; use a shell that expands it or list the files). Directories are rejected with an error rather than walked. - Each file is linted independently with the same options and its own report; results are
aggregated. Files are ordered by report path regardless of argument order, and duplicates
(
./a.ttl, a symlink, another spelling on a case-insensitive file system) collapse into one. If any file cannot be parsed, every parse failure is reported and the run exits2before linting.--shapesand--owl-profileapply to every file; ROBOT runs once per file. - Report paths (JSON
path, SARIFartifactLocation.uri, fingerprints) are POSIX-style: relative to the working directory when the file is beneath it (onto/a.ttl), otherwise absolute (file:///C:/x/a.ttlin SARIF), with backslashes converted. Run from a stable directory such as the repository root so baselines and SARIF paths are portable. - Fingerprints: a single-file run hashes
(rule, focus, message)exactly as before, so existing baselines keep working. A multi-file run also hashes the report path so identical findings in two files stay distinct; a baseline written from one file therefore does not carry over to a multi-file run. Write multi-file baselines from multi-file runs.
GitHub annotations
--format github is not available yet. The escaping helpers (%, CR, LF and, for properties,
: and ,) already exist in ontolint.github; a formatter has not yet been added.
Optional SHACL validation
Pass a shapes graph with --shapes to enable pySHACL validation:
ontolint ontology.ttl --shapes shapes.ttl
The integration uses meta_shacl=True and advanced=True. Newer SHACL draft
features are not guaranteed to be supported.
OWL profile validation with ROBOT in Docker
Build the minimal project image once. Its final stage contains only the ROBOT JAR and a non-root Java runtime:
docker build --tag ontolint-robot:1.9.11 docker/robot
ontolint ontology.ttl --owl-profile DL
The profile run mounts the ontology directory read-only, disables container
networking, drops Linux capabilities, and applies CPU, memory, process, and
timeout limits. The image build verifies the ROBOT JAR against a pinned
SHA-256. Supply a digest-pinned image with --robot-image in CI.
Policy and baselines
A JSON policy can supply a base IRI, label-language requirements, thresholds, ignored rules, and severity overrides.
{
"base": "https://example.org/ont#",
"languages": ["en"],
"depth_limit": 10,
"inheritance_limit": 5,
"soup_limit": 15,
"ignored_rules": ["CLS002"],
"severity_overrides": {"ONT003": "error", "OWL005": "info"},
"required_annotations": {"class": ["http://www.w3.org/2004/02/skos/core#definition"]},
"alignment_namespaces": ["http://purl.obolibrary.org/obo/"],
"alignment_min_coverage": 0.5,
"max_parse_seconds": 60
}
The optional keys switch rules on only when set: required_annotations maps a term kind
(class, property, individual, concept) to the predicates every owned term of that
kind must carry (GOV016); alignment_namespaces lists the namespaces mapping targets
must belong to (ALIGN012); alignment_min_coverage is the share of classes and
properties that must have a mapping (ALIGN016). Unknown keys are reported by GOV020,
and an unsupported policy_version rejects the file. max_parse_seconds (also
--max-parse-seconds) limits the time spent parsing one input and reports SEC003
when it is exceeded; it parses in a worker process, so leave it unset for the fastest
in-process load.
# Emit SARIF using a policy
ontolint ontology.ttl \
--config policy.json --format sarif --output findings.sarif
# Record accepted findings, then report only newly introduced ones
ontolint ontology.ttl --write-baseline baseline.json
ontolint ontology.ttl \
--baseline baseline.json --fail-on warning
Baselines compare stable hashes of (rule, focus, message) (plus the report path for multi-file runs). They are useful for
incremental adoption, but are not semantic version comparisons.
Development and quality checks
# Unit and property-based tests (coverage must stay at or above 95%)
uv run pytest -q
# Run tests in parallel
uv run pytest -q -n auto
# Skip the slow Hypothesis and fuzz tests
uv run pytest -q -n auto -m 'not slow'
# Type checks
uv run pyright && uv run mypy && uv run mypy --platform win32
# Mutation testing (opt-in)
uv run pytest -m 'not slow' --gremlins --no-cov --gremlin-parallel --gremlin-cache
# Lint and format the project
uv run ruff check . && uv run ruff format --check .
Hypothesis tests generate RDF graphs and check properties of the linter, such as
deterministic output and agreement of cycle diagnostics with an independent
reachability oracle. A mutation-based fuzzer (tests/fuzz/) feeds mutated
files to the loader, the rules and the CLI. Both are marked slow; run the
longer profile with HYPOTHESIS_PROFILE=ci-deep. Mutation testing targets the
engine, linter, CLI, loader, reporting, adapters and rules (see
docs/mutation-policy.md).
To add rules, see docs/adding-rules.md.
Releasing
uv run python scripts/bump_version.py {major|minor|patch|X.Y.Z} updates the version files and
CHANGELOG.md; it never commits, tags or pushes. See
docs/releasing.md.
CI workflows
.github/workflows/ci.yml runs the fast PR checks (Ruff, mypy, pyright,
pip-audit, tests with coverage, package build). All actions are pinned to
commit SHAs and the uv version is pinned; Renovate keeps both current.
.github/workflows/fuzz.yml runs the mutation fuzzer weekly with a longer
budget per target and uploads any crashing input.
.github/workflows/release.yml runs on a vX.Y.Z tag: it calls the CI
workflow, checks that the tag matches the version in pyproject.toml, builds
the distributions and publishes them to PyPI with Trusted Publishing from the
pypi environment.
.github/workflows/validation.yml runs weekly and on manual dispatch:
- ROBOT Docker image build and smoke test (also on pull requests that touch
docker/and carry thevalidationlabel). Logs are uploaded as artifacts. - Mutation testing with the command above plus JSON/HTML reports, followed by
.github/scripts/mutation_gate.py, which enforces the per-operator and overall thresholds. Reports are uploaded as artifacts.
The operators, thresholds, scope, waiver process and blocking/advisory rules are defined in docs/mutation-policy.md.
Rule reference
docs/rules.md documents every rule with its severity, fix, a flagged
example, a clean example and the reported message. Rules that are not
implemented yet are listed as TODO. The file is generated; regenerate it with
uv run python tests/rule_docs_generator.py.
Interpretation principles
- RDF syntax validity and OWL logical consistency are different properties.
- SHACL validation does not establish ontology-design quality.
- Multiple RDFS domains or ranges are conjunctive, not alternative choices.
- Under OWL's open-world assumption, missing evidence is not falsehood.
- A subclass cycle is a review warning, not automatically an inconsistency.
owl:allValuesFromdoes not imply the existence of a value.- A SKOS
broadercycle is informational; it is not universally prohibited. - The linter does not fetch URLs or resolve
owl:importsautomatically. - Semantic OWL axioms are never auto-fixed because intent cannot be inferred reliably.
Scope and limitations
- Term-level rules only judge terms the file owns. With
--basethe base decides (a base such ashttp://purl.obolibrary.org/obo/RO_is a plain prefix; a warning is printed when it selects no declared term). Without it the owned namespaces are derived from theowl:OntologyIRI(s) (the IRI, the IRI without#///.owl, and for OBO ontologies the.../obo/<NAME>_ID space); RDF/RDFS/OWL/XSD terms are never owned, and if no declared term matches, every declared term is treated as owned. - Definitions are read from
rdfs:comment,skos:definition,dcterms:description,dc:description,obo:IAO_0000115,obo:IAO_0000600andprov:definition; labels fromrdfs:labelandskos:prefLabel. Add more with the policy listsextra_definition_predicatesandextra_label_predicates(absolute IRIs). - With
--shapes, the SHACL step first checks the shapes graph and reports SHACL001 as failed withShapesRejectedwhen pySHACL would crash or stall on it: an IRI that rdflib cannot serialise (such as{,},|,^, a space or a quote), a cyclicrdf:list, a list cell with severalrdf:restvalues, a list shared by several lists, a list longer than 400 items, or lists whose squared lengths sum above 160000 (pySHACL's meta-validation re-walks each list for every internal result). The data graph is not refused up front: odd IRIs, large enumerations and malformed lists are accepted, and if pySHACL fails on one anyway the failure is reported asShapesRejectedwith the underlying error. - Class-hierarchy analysis uses asserted
rdfs:subClassOfstatements; it does not calculate entailments or inspect an import closure. CLS002can be noisy for TBox-only ontologies and is often disabled in that profile.- Shape coverage (
SHQ006) checks direct instances and direct class targets only. - SARIF source regions are currently available for Turtle-family and N-Triples-family inputs; RDF/XML and JSON-LD source mapping remain future work.
Related tools and sources
- RDFLib
- pySHACL
- W3C SHACL Recommendation
- W3C SKOS Reference
- OOPS! pitfall catalogue
- OntoSniff
- ROBOT
- OBO Foundry principles
The data/ directory contains crosswalks to the OOPS! and OntoSniff catalogues.
Those mappings are traceability aids, not claims of identical behavior.
Input safety
Ontologies are untrusted input, so the loader is deliberately strict:
- Parsing runs with the network disabled. Any socket or DNS attempt is blocked
and reported (
SEC013); a JSON-LD file with an external@contextis refused (SEC006).owl:importsis never fetched. - Inputs larger than
--max-input-bytes(default 64 MiB) or holding more than--max-triplestriples (default 5,000,000) are rejected (SEC001,SEC002). Both can also be set in the--configpolicy file. With--max-parse-secondsa parse that runs too long is stopped (SEC003). - RDF/XML with external entities or nested entity expansion is refused
(
SEC010), compressed files are refused (SEC011), and relative paths that escape the working directory or symlinks that leave it are refused (SEC009). - Rejected inputs are reported as
SECxxx: ...on stderr with exit code2, like a parse failure. Post-parse hygiene checks (SEC004,SEC005,SEC007,SEC008,SEC012,SEC014) are ordinary findings;SEC007is an info note naming theowl:importsthat were not followed.
Roadmap
data/rule_catalogue.csv is the machine-readable
catalogue. It records 294 entries: 239 executable diagnostics and 55 proposed
candidates. The additional entry beyond the original roadmap is TOOL001, an
operational diagnostic for the linter itself. The 18 OPS entries of the
original roadmap describe command-line and report features, not checks on an
ontology; they moved to docs/cli-features.md.
Thirteen further entries that cannot be specified soundly, or duplicate another rule, were
removed; see docs/dropped-rules.md.
The next major areas are competency-question and ontology-diff rules (both need a second input), reasoner-based semantic checks through ROBOT, safe import-closure support, richer SHACL authoring checks, and network-based FAIR checks.
License
Licensed under the Apache License, Version 2.0. See LICENSE.
Metadata
Release files for ontolint 0.3.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 | Provenance | Actions |
|---|---|---|---|---|
| ontolint-0.3.0.tar.gz | 151.5 kB | With provenance | Details for ontolint-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ontolint-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for ontolint-0.3.0-py3-none-any.whl |
Total release size: 324.1 kB
Release files / ontolint-0.3.0.tar.gz
| Download URL | ontolint-0.3.0.tar.gz |
|---|---|
| Size | 151.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
373d2697eb50fe10e0bc9e9584732f5ba9898bb17a260f345a6bbf4895cae17d
|
|
BLAKE2b-256 checksum How to use checksums |
14b87c5241281d5e56a77b9d45d67402523e2a91f1501aceb7fce5acc447daaa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 8, 2026.
Transparency logRelease files / ontolint-0.3.0-py3-none-any.whl
| Download URL | ontolint-0.3.0-py3-none-any.whl |
|---|---|
| Size | 172.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b5f86e8f41507b5bba635c9566e2880c1de2c79334cbdc7d2b2a34277e0481b6
|
|
BLAKE2b-256 checksum How to use checksums |
4012f1a06592932e394c5638ed542e9093aa6b00440845e99a845ae08eb37158
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 8, 2026.
Transparency logSecurity for ontolint 0.3.0
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for ontolint-0.3.0.tar.gz
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 Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.