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:
- SHACL-first: developers write Turtle / JSON-LD SHACL files; framework generates Python classes.
- 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
.ttlbeing 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.