Skip to content

ADR-0002: Python-first shapes, emit SHACL

  • Status: Accepted
  • Date: 2026-04-12

Context

Shapes are the core modeling primitive — they define what data looks like and constrain what enters the graph. Two authoring paths were considered:

  1. SHACL-first: developers write Turtle / JSON-LD SHACL files; framework generates Python classes.
  2. Python-first: developers write Python classes with typed fields; framework emits SHACL.

SHACL-first is the semantically pure answer. Ontologists have tools (Protégé, TopBraid, WebVOWL) that work natively. Reuse of existing vocabularies is frictionless.

Python-first is the developer-friendly answer. Types live where the code lives. IDE autocomplete works. Diffs are reviewable in PRs. Evolution happens in the same file as business logic.

Decision

Python-first. Shapes are declared as Python classes decorated with @shape, with fields bound to predicates via predicate() descriptors. trails onto export emits canonical SHACL (.ttl) to ontology/generated.ttl.

External vocabularies (schema.org, FOAF, PROV, SOSA) are imported as .ttl — SHACL-first for consumption, Python-first for authoring.

@shape(iri="myapp:Patient", extends=["schema:Person"])
class Patient:
    name: str               = predicate("schema:name")
    dob:  date              = predicate("schema:birthDate")
    allergies: list["Allergy"] = predicate("myapp:hasAllergy", min=0)

Consequences

Positive

  • Single source of truth lives where application code lives.
  • IDE support (autocomplete, type checking, go-to-definition) works immediately.
  • PR reviews see shape changes as code diffs.
  • Shape evolution happens in the same commit as business logic.
  • New developers don't need to learn SHACL syntax to start.
  • Python's type system (list[X], X | None, generics) maps naturally to SHACL cardinalities and datatypes.

Negative

  • Not all SHACL is expressible in natural Python. SPARQL-based SHACL constraint components, qualified value shapes, and complex path expressions require escape hatches (e.g., @shape(extra_shacl="...")).
  • Ontologists cannot author shapes directly — they must go through developers. Mitigated by supporting import of hand-written SHACL for external vocabularies.
  • Tooling that expects SHACL-first (e.g., Protégé round-trip) won't work seamlessly. Mitigated by the emitted .ttl being standards-compliant.

Non-consequences

  • Inference and validation at runtime are unchanged — they operate on the emitted SHACL.
  • Published ontology bundles (Ring 3 registry) carry emitted SHACL, not Python source.

Revisit conditions

  • If a significant user segment cannot work this way (e.g., regulated industries requiring human-authored SHACL for audit), add a @shape_from_ttl("…") path as an alternative.