Your first validation

In this tutorial you write a SHACL shapes file and a data file, validate one against the other, read the report, and fix the data. It takes about ten minutes and assumes no prior SHACL knowledge — only that you can read Turtle, RDF’s text format, well enough to recognise a triple when you see one.

Install

Wheels for pyshifty are published to PyPI and contain a pre-compiled engine, so installing it needs no Rust toolchain:

pip install "pyshifty[rdflib]"

The distribution is named pyshifty but the module is shifty:

import shifty

For the command-line tool, download an archive from GitHub Releases and put the extracted shifty executable on your PATH. Linux x86-64, Windows x86-64, and macOS arm64 archives are built by the release workflow. To build from source instead, use Rust:

git clone https://github.com/gtfierro/shifty
cd shifty
cargo install --path crates/shifty-cli

This tutorial uses the CLI for the first half and Python for the second. If you want to use only Python, skip to Validate from Python — nothing in the CLI half is a prerequisite.

Create the shapes and data graphs

Create a working directory with two files in it. The first describes what a valid person looks like. Call it shapes.ttl:

@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:email ;
        sh:minCount 1 ;
    ] .

Read that as three separate claims. sh:targetClass ex:Person says which nodes this shape applies to: every node typed ex:Person. The first sh:property block says each of those nodes must have at least one ex:name, and that every value found there must be a string. The second says each must have at least one ex:email, with no constraint on the value.

The second file is the data to check. Call it data.ttl:

@prefix ex: <http://example.org/> .

ex:alice a ex:Person ; ex:name "Alice" ; ex:email "alice@example.org" .
ex:bob   a ex:Person ; ex:name 123 .

Alice satisfies both obligations. Bob’s ex:name is an integer rather than a string, and he has no ex:email. The validator reports these as distinct reasons.

Run the validator

shifty validate --shapes shapes.ttl --data data.ttl
conforms: false — 1 violation in 2 findings

Finding 1 of 2
  target       class(ex:Person)
  severity     Violation
  shape        ex:PersonShape
  failure      at least 1 value(s) required along ex:email, found 0
  path         ex:email
  found        0 value(s) along the path; at least 1 required
  requirement  ∃[1..] ex:email

  affects      ex:bob
  value node   (the focus node itself)
  also fails   Finding 2

Finding 2 of 2
  target       class(ex:Person)
  severity     Violation
  shape        ex:PersonShape
  failure      test(datatype(xsd:string)) not satisfied
  path         ex:name
  requirement  test(datatype(xsd:string))

  affects      ex:bob
  value node   "123"^^xsd:integer
  also fails   Finding 1

notation
  ∃[m..n] p . X   between m and n values along p satisfy X

Alice is absent from the output. A validation report contains failures only. It does not distinguish between a node that passed validation and a node that was never selected by the shape. To inspect selection and passing evaluations, use the evidence interface; to extract selected nodes and values as bindings, use shape maps. The failure-explanation tutorial demonstrates the evidence workflow.

The CLI groups by finding: one for the missing email and one for the wrong name datatype. Both affect Bob, so the summary says one violation in two findings. value node identifies "123"^^xsd:integer for the datatype failure; the missing email has no offending value. target class(ex:Person) shows why Bob was checked. See How shapes are compiled for the compiled constraint notation.

Fix the data

Edit data.ttl so Bob’s name is a string and he has an email:

@prefix ex: <http://example.org/> .

ex:alice a ex:Person ; ex:name "Alice" ; ex:email "alice@example.org" .
ex:bob   a ex:Person ; ex:name "Bob"   ; ex:email "bob@example.org" .

Run the same command again:

conforms: true

The quotes around "Bob" are doing real work here. In Turtle, 123 is an xsd:integer and "123" is an xsd:string; they are different RDF terms, and sh:datatype distinguishes them. Many SHACL datatype violations come from this difference.

Validate from Python

shifty.validate takes the data graph first and the shapes graph second and returns the same three kinds of values as pyshacl.validate. Check keyword arguments when migrating an existing call; Python API reference lists the accepted Shifty signature.

import pathlib
import shifty

conforms, report_graph, results_text = shifty.validate(
    pathlib.Path("data.ttl"),
    pathlib.Path("shapes.ttl"),
)

print(conforms)
print(results_text)

You get three things back. conforms is the boolean. report_graph is an rdflib.Graph holding a W3C sh:ValidationReport, which is the interoperable form to hand to another tool. results_text is that report rendered for a human:

Validation Report
Conforms: False
Results (2):
Constraint Violation in DatatypeConstraintComponent
  Severity: sh:Violation
  Source Shape: _:...
  Focus Node: <http://example.org/bob>
  Result Path: <http://example.org/name>
  Value: "123"^^<http://www.w3.org/2001/XMLSchema#integer>
  Message: Value 123 does not have datatype <http://www.w3.org/2001/XMLSchema#string>

Constraint Violation in MinCountConstraintComponent
  Severity: sh:Violation
  Source Shape: _:...
  Focus Node: <http://example.org/bob>
  Result Path: <http://example.org/email>
  Message: Fewer than 1 values on path <http://example.org/email>

This is the standard SHACL report vocabulary rather than the CLI’s compact summary, so the two commands print different text for the same input. The Source Shape is a blank-node identifier because the property shapes in shapes.ttl were written inline with [ ... ] and so have no IRI of their own. The blank-node identifiers and result order can vary between runs.

Note

This tutorial uses validate() because it returns the standard W3C report vocabulary. Shifty also provides validate_algebra(), its native structured result interface. See validation interfaces before choosing an interface for an application.

Any of str (Turtle text), bytes, pathlib.Path, or an rdflib.Graph works as an argument, and a list of them is merged first. Passing paths is the fastest option, because the file is parsed in Rust without a round-trip through rdflib.

Shapes and data graph inputs

You passed two files, and Shifty treated them asymmetrically: shapes were read only from shapes.ttl. If data.ttl had contained a stray sh:NodeShape — copied in from somewhere, or generated by an upstream tool — it would have been ignored rather than quietly becoming a constraint.

If you pass just one graph, it plays both roles:

shifty validate --shapes combined.ttl
conforms, report, text = shifty.validate("combined.ttl")

This is the common case where shape definitions and instance data live in the same file. Shapes graphs and data graphs specifies the graph-input and visibility rules, including how to validate against shapes embedded in data.

Next steps

Reading validation results in code continues with this same graph, and moves from reading a report to writing code that consumes one.

For a specific job — running SHACL-AF rules, extracting bindings from a conforming node, or looking at how the shapes were compiled — the how-to guides are the shorter path.