Python API Reference

The pyshifty package exposes the full Shifty engine through PyO3 bindings with native rdflib interop. Pre-built wheels are on PyPI — no Rust toolchain required.

Install

pip install pyshifty   # Python 3.9+
import shifty   # the package imports as `shifty`

Graph inputs throughout the API can be a str (Turtle text), bytes, pathlib.Path, or an rdflib.Graph.

Note

Where shapes are read from. If you pass a single graph (omit shapes or pass None), shifty reads both the shape definitions and the data from that one graph. If you pass a separate shapes graph, the schema is compiled only from the shapes graph — SHACL vocabulary that happens to sit in the data graph is ignored, never compiled into constraints. See Shapes and data graphs for the cross-frontend rule.

validate

The primary validation entry point is compatible with the pyshacl interface:

conforms, report_graph, results_text = shifty.validate(data, shapes)
  • data — the data graph to validate

  • shapes — the SHACL shapes graph (optional; if omitted, shapes are read from data)

  • Returns: (bool, rdflib.Graph, str) — conforms flag, W3C sh:ValidationReport graph, human-readable summary

shapes = """
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix ex:  <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:PersonShape a sh:NodeShape ;
    sh:targetClass ex:Person ;
    sh:property [
        sh:path ex:name ;
        sh:minCount 1 ;
        sh:datatype xsd:string ;
    ] ;
    sh:property [
        sh:path ex:age ;
        sh:maxCount 1 ;
        sh:datatype xsd:integer ;
    ] .
"""

data = """
@prefix ex: <http://example.org/> .

ex:Alice a ex:Person ; ex:name "Alice" ; ex:age 30 .
ex:Bob   a ex:Person .
"""

conforms, report_graph, results_text = shifty.validate(data, shapes)
# conforms → False
# report_graph → rdflib.Graph with sh:ValidationReport
# results_text → human-readable summary

Keyword arguments

Argument

Description

graph_mode

"union" (default), "data", or "union-all" — controls which triples are visible to path traversal and SPARQL evaluation

shape_names

Optional list of named shape IRIs to use as top-level validation entry points; referenced helper shapes are still evaluated normally

infer

True (default) — run SHACL-AF sh:rule entries to a fixed point before validating; set False to skip inference

# Skip inference, validate data only
conforms, report, text = shifty.validate(data, shapes, infer=False)

# Use data ∪ shapes for path/SPARQL evaluation
conforms, report, text = shifty.validate(data, shapes, graph_mode="union")

# Validate only selected named shapes as entry points
conforms, report, text = shifty.validate(
    data,
    shapes,
    shape_names=["http://example.org/PersonShape"],
)

Embedded shapes

When you pass two graphs, the schema is compiled only from shapes; shape definitions in data are not read. To validate against shapes that are embedded in the data graph, omit the second argument or pass None so the single graph is used for both roles:

conforms, report, text = shifty.validate("combined.ttl")
conforms, report, text = shifty.validate(combined_graph, None)

Do not pass an empty rdflib.Graph() for this case — an empty graph is treated as an explicit empty shapes graph and will produce no violations.

To validate data against shapes that live in both a dedicated shapes graph and the data graph, union them into the shapes argument yourself (shapes accepts a list of graphs, unioned before evaluation) — shifty will not read shapes from the data side automatically.

validate_algebra

Returns structured Violation objects instead of an RDF report graph:

result = shifty.validate_algebra(data, shapes)

print(result.conforms)           # False
for v in result.violations:
    print(v.focus_node)          # IRI of the failing focus node
    print(v.statement_id)        # stable statement id
    print(v.constraint_id)       # statement-level algebra id
    print(v.shape_name)          # shape that targeted this node (if any)
    for r in v.reasons:
        print(r.message)         # human-readable failure description
        print(r.path)            # property path checked, if applicable
        print(r.value)           # the offending value node
        print(r.constraint_kind)  # ConstraintKind.Cardinality, Sparql, ...
        print(r.constraint.render)

Accepts the same infer= and graph_mode= keyword arguments as validate().

Algebraic provenance

validate_algebra() exposes the algebraic operator that produced each validation cause:

Reason.constraint

A Constraint object for the originating algebra node. It has id, kind, render, definition, and json fields.

Reason.constraint_kind

A stable enum such as ConstraintKind.Cardinality, ConstraintKind.ClassMembership, ConstraintKind.ValueType, ConstraintKind.NodeKind, ConstraintKind.Conjunction, ConstraintKind.Disjunction, or ConstraintKind.Sparql. Use this for branching instead of parsing messages or Rust class names.

Violation.statement_id / Violation.constraint_id

The top-level statement identity. This is the stable key shared with RepairSession.witnesses().

Reason.constraint_id

The specific nested algebra node that produced the validation cause. This may differ from Violation.constraint_id when the violated shape is a conjunction, disjunction, or another composed constraint.

The usual validation-to-repair join is:

result = shifty.validate_algebra(data, shapes, infer=False)
session = shifty.RepairSession(shapes, data, infer=False)

witnesses = {
    (w.focus, w.statement_id, w.constraint_id): w
    for w in session.witnesses()
}

for v in result.violations:
    witness = witnesses.get((v.focus_node, v.statement_id, v.constraint_id))
    for r in v.reasons:
        if r.constraint_kind == shifty.ConstraintKind.Cardinality:
            print("count constraint:", r.constraint.definition)
        elif r.constraint_kind == shifty.ConstraintKind.Sparql:
            print("SPARQL diagnostic:", r.sparql_diagnostic)
    if witness is not None:
        print(witness.repair_tree().explain())

PreparedValidator

For repeated validation against the same shapes graph, compile once:

validator = shifty.PreparedValidator(shapes)

# Validate many data graphs against the compiled shapes
for data_file in data_files:
    conforms, report, text = validator.validate(data_file)

# Or use the structured result form
result = validator.validate_algebra(data, infer=False)

File inputs

import pathlib

conforms, report, text = shifty.validate(
    pathlib.Path("data.ttl"),
    pathlib.Path("shapes.ttl"),
)

pathlib.Path inputs are parsed directly by Rust. rdflib.Graph inputs use N-Triples for the Python-to-Rust transfer to avoid rdflib’s slower Turtle serializer.

Property witnesses

validate / validate_algebra report violations. PreparedValidator.witnesses() is their inverse: for every focus node that conforms to a target/profile node shape, it returns the values each sh:property shape’s sh:path resolved to. Useful when a SHACL profile doubles as an extraction schema — e.g. disambiguating several same-typed sensors on a piece of equipment via sh:qualifiedValueShape.

shapes = """
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix zea: <http://example.org/zea#> .
@prefix ex:  <http://example.org/> .

ex:VavProfile a sh:NodeShape ;
    sh:targetClass ex:Vav ;
    sh:property [
        zea:role ex:OutsideAirTempRole ;
        sh:path ex:hasPoint ;
        sh:qualifiedValueShape [ sh:hasValue ex:oat ] ;
        sh:qualifiedMinCount 1 ;
        sh:qualifiedMaxCount 1 ;
    ] ;
    sh:property [
        zea:role ex:ReturnAirTempRole ;
        sh:path ex:hasPoint ;
        sh:qualifiedValueShape [ sh:hasValue ex:rat ] ;
        sh:qualifiedMinCount 1 ;
        sh:qualifiedMaxCount 1 ;
    ] .
ex:OutsideAirTempRole zea:roleName "outsideAirTemp" .
ex:ReturnAirTempRole zea:roleName "returnAirTemp" .
"""
data = """
@prefix ex: <http://example.org/> .
ex:vav1 a ex:Vav ; ex:hasPoint ex:oat, ex:rat, ex:sat, ex:mat .
"""

validator = shifty.PreparedValidator(shapes)
for w in validator.witnesses(data, key_path="zea:role/zea:roleName"):
    print(w.focus, w.key, w.values)
# <http://example.org/vav1> outsideAirTemp ['<http://example.org/oat>']
# <http://example.org/vav1> returnAirTemp  ['<http://example.org/rat>']

key_path is a SPARQL 1.1 property path expression — sequence /, alternation |, inverse ^, and the Kleene forms */+/? are all supported — evaluated from each sh:property shape’s own node, over the shapes graph, to produce a stable key. The key above isn’t a direct annotation on the property shape — it lives one hop further away, through an intermediate role-descriptor node — which a bare predicate lookup couldn’t reach but a path can. A direct annotation (zea:roleName "outsideAirTemp" right on the sh:property shape) would just be key_path="zea:roleName"; a descriptor that points at the property shape instead of the other way around would use an inverse hop, key_path="^zea:describes/zea:roleName". Prefixes resolve against the shapes document’s declared @prefixes.

Each PropertyWitness has:

Attribute

Description

focus

The focus node that conformed

shape

The node shape (application profile) it conformed to

key

The lexical value reached by key_path, or the property shape’s own IRI/blank-node id when the path resolves to no value (or is omitted)

values

The deduped sh:path bindings, rendered in full (<iri>, "lit", "lit"@lang, "lit"^^<datatype>) so IRI and literal bindings stay distinguishable — narrowed to the sh:qualifiedValueShape matches when the property shape declares one

infer

Run SHACL-AF sh:rule entries to a fixed point:

result = shifty.infer(data, rules)

print(result.inferred_count)    # number of newly derived triples
g = result.graph()              # rdflib.Graph with original + inferred data
rules = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .

ex:RectangleShape a sh:NodeShape ;
    sh:targetClass ex:Rectangle ;
    sh:rule [
        a sh:TripleRule ;
        sh:subject sh:this ;
        sh:predicate ex:area ;
        sh:object [ sh:path ex:width ] ;
    ] .
"""

data = """
@prefix ex: <http://example.org/> .
ex:r1 a ex:Rectangle ; ex:width 3 ; ex:height 2 .
"""

result = shifty.infer(data, rules)
print(result.inferred_count)    # 1
g = result.graph()              # graph contains ex:r1 ex:area 3 .

If rules are embedded in the data graph, omit the second argument or pass None:

result = shifty.infer(combined_data_and_rules)
result = shifty.infer(combined_data_and_rules, None)

Passing rdflib.Graph() as the second argument means “run with an explicit empty rules graph” — no embedded rules will be parsed.

graph_mode

Both validate() and validate_algebra() accept a graph_mode keyword argument that controls which triples are visible to path traversal and SPARQL evaluation:

Mode

Behaviour

"data"

Focus nodes come from the data graph; path traversal and SPARQL use data only

"union" (default)

Focus nodes from data; paths and SPARQL use data ∪ shapes

"union-all"

Focus nodes and evaluation both use data ∪ shapes

infer() does not accept graph_mode.

shape_names

Both validate() and validate_algebra() accept shape_names=[...] to validate only selected named shapes as top-level entry points:

result = shifty.validate_algebra(
    data,
    shapes,
    shape_names=["http://example.org/PersonShape"],
)

Only target-bearing statements owned by the selected named shapes are used as entry points. Dependencies referenced from those entries are still evaluated normally, including helper shapes reached through sh:node, sh:property, qualified value shapes, and boolean shape expressions. Shape names may be passed as bare IRIs or wrapped in angle brackets.

For full API documentation including the RepairSession interface, see docs.rs/shifty-engine.