Extensible linter and quality pipeline for RDF, SKOS, OWL and RDFS vocabularies
Project description
semanticlint
Extensible linter and quality pipeline for RDF, SKOS, OWL and RDFS vocabularies.
Features
- Stage 1 — Lint: syntax validation for Turtle, RDF/XML, N-Triples, JSON-LD
- Stage 2 — Integrity: SKOS integrity conditions (W3C), OWL consistency, RDFS checks
- Stage 3 — Quality: label, definition and language coverage
- Auto-detection: identifies SKOS / OWL / RDFS / RDF vocabulary types automatically
- Extensible: add custom checks with a single class and
@CheckRegistry.register - Configurable:
onto-ci.ymlalongside your vocabulary files controls which stages run
Why semanticlint, and how it relates to SHACL / pySHACL
Decision: semanticlint is the aggregation layer; pySHACL is
the SHACL engine underneath it. Shape-based, per-node constraints (a class must have an
rdfs:label, a property must declare rdfs:domain/rdfs:range, SKOS label disjointness, …) are
expressed as SHACL shapes and validated through pySHACL — the W3C-standard, reusable way to do it.
semanticlint does not reimplement what SHACL already does well.
Objective: aggregate, under one tool and one report, the validations and metrics that SHACL cannot express on its own. SHACL validates one node against a shape and answers pass / fail per node. It is not designed for statistical, graph-wide quality — questions like:
- What fraction of classes carry a label? (coverage metrics, not per-node constraints)
- Rolling many heterogeneous signals — SHACL shape results, aggregate metrics, and imperative
Python checks — into a single severity-graded report with
fail-ongating for CI.
This limitation of SHACL for aggregate data-quality assessment is documented in the literature — see “Is SHACL Suitable for Data Quality Assessment?” (arXiv, 2025): https://arxiv.org/html/2507.22305v2. semanticlint exists to cover precisely that gap: it runs SHACL where SHACL fits, and adds coverage metrics, auto vocabulary detection, curated rule sets, and unified reporting on top.
Scope of the two tools
| Capability | pySHACL (SHACL engine) | semanticlint (aggregation layer) |
|---|---|---|
Per-node shape constraints (sh:minCount, domain/range, datatypes, disjointness) |
✅ | ✅ (via pySHACL) |
Severity per result (sh:Violation/Warning/Info) |
✅ | ✅ |
| Aggregate coverage metrics (e.g. % of classes labelled) | ❌ | ✅ |
| Imperative / custom Python checks alongside shapes | ❌ | ✅ |
| Auto-detect SKOS / OWL / RDFS / RDF and select applicable checks | ❌ | ✅ |
| Curated, ready-made OWL / SKOS / RDFS / URI rule sets | ❌ (you author every shape) | ✅ |
Unified report + fail-on gating for CI across all of the above |
❌ | ✅ |
Rule of thumb: if a rule is a per-node constraint, write it as a SHACL shape (pySHACL runs it). If it is an aggregate, or needs to sit in one graded report with everything else, that is what semanticlint adds.
Installation
pip install semanticlint
Quick start
semanticlint check my-taxonomy.ttl
semanticlint check vocabularies/ --select SKO --ignore SKO003
Project-specific rules (*.shapes.ttl)
Rules that are specific to your ontology — business constraints the built-in checks can't know
about — live in a plain SHACL file committed next to the vocabulary it constrains. semanticlint
auto-discovers any *.shapes.ttl file (a sibling when you check a single file, the whole tree
when you check a directory), unions it with the built-in shapes, and enforces it. No configuration,
no registration.
Say zoo.ttl defines a zoo ontology. Drop a zoo.shapes.ttl beside it:
# zoo.shapes.ttl — versioned in git next to zoo.ttl
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/zoo#> .
ex:PersonEmploymentShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [ sh:path ex:work_for ;
sh:class ex:Department ;
sh:maxCount 1 ;
sh:message "A Person may work_for at most one Department" ] .
semanticlint check zoo/
# ERROR [PersonEmploymentShape] alice: A Person may work_for at most one Department
# 1 violation: 1 error (fail-on: error) → exit code 1, CI fails
Notes:
- No annotations needed. The violation's id defaults to the shape's name
(
PersonEmploymentShape); set a custom one withslint:checkId "ZOO001"if you want it stable for--select/--ignore. - Severity & CI gating. A plain shape reports at SHACL's default
sh:Violation→ error, so it fails CI under the default--fail-on error. Usesh:severity sh:Warning/sh:Infoto soften it. - Shapes files are never linted as data — a
*.shapes.ttlis treated as rules, not as a vocabulary to check. - Targets do the gating, so a local shape only fires on the nodes it targets — it's safe to keep shared shapes for several vocabularies in one directory.
Writing a custom check
from semanticlint import Check, CheckRegistry, VocabType, Severity, Violation
from rdflib import RDF
from rdflib.namespace import SKOS
@CheckRegistry.register
class CamelCaseConceptCheck(Check):
id = "CUS001"
description = "Concept local names should be CamelCase"
severity = Severity.WARNING
applies_to = VocabType.SKOS
def run(self, graph, config):
violations = []
for concept in graph.subjects(RDF.type, SKOS.Concept):
local = str(concept).split("#")[-1].split("/")[-1]
if not local[0].isupper():
violations.append(Violation(self.id, f"{local} is not CamelCase", self.severity, concept))
return violations
Configuration (onto-ci.yml)
version: 1
sources: ["*.ttl", "vocabularies/**/*.ttl"]
select: ["RDF", "SKO", "QUA"]
ignore: []
quality:
min_label_coverage: 1.0
min_definition_coverage: 0.5
languages: ["en"]
plugins:
- my_project.custom_checks
License
MIT — logo is public domain (W3C RDF logo via Wikimedia Commons).
Project details
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 semanticlint-0.5.0.tar.gz.
File metadata
- Download URL: semanticlint-0.5.0.tar.gz
- Upload date:
- Size: 95.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c31902526ee921bd1c4d88ad55f75830a43fb86b4f990856be7405cd9ebabbd
|
|
| MD5 |
ae60bf9a719ff63dd8ca581c71ae837a
|
|
| BLAKE2b-256 |
87861384e64bcbcc1d485e13cf67f64b5ae6469d7e1bc128079caabd8cf8d91a
|
Provenance
The following attestation bundles were made for semanticlint-0.5.0.tar.gz:
Publisher:
publish.yml on gbelbe/semanticlint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
semanticlint-0.5.0.tar.gz -
Subject digest:
3c31902526ee921bd1c4d88ad55f75830a43fb86b4f990856be7405cd9ebabbd - Sigstore transparency entry: 2081946775
- Sigstore integration time:
-
Permalink:
gbelbe/semanticlint@dc727fa0c4a9512ece000e4f64d3e47356c74452 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/gbelbe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@dc727fa0c4a9512ece000e4f64d3e47356c74452 -
Trigger Event:
push
-
Statement type:
File details
Details for the file semanticlint-0.5.0-py3-none-any.whl.
File metadata
- Download URL: semanticlint-0.5.0-py3-none-any.whl
- Upload date:
- Size: 29.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88afd5bacb64b8a1c35d8a1c9a225744bebcf9a0c598514ddca9421331894125
|
|
| MD5 |
a03fe6b97a6897d4e84342d607689f4a
|
|
| BLAKE2b-256 |
e0c4a478387ced436ca9ca7d1cef24586b523e00425d3f0e14f7cf60744291ab
|
Provenance
The following attestation bundles were made for semanticlint-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on gbelbe/semanticlint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
semanticlint-0.5.0-py3-none-any.whl -
Subject digest:
88afd5bacb64b8a1c35d8a1c9a225744bebcf9a0c598514ddca9421331894125 - Sigstore transparency entry: 2081946871
- Sigstore integration time:
-
Permalink:
gbelbe/semanticlint@dc727fa0c4a9512ece000e4f64d3e47356c74452 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/gbelbe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@dc727fa0c4a9512ece000e4f64d3e47356c74452 -
Trigger Event:
push
-
Statement type: