TripleModel
Typed Pydantic models for RDF. Declare fields once, get correct triples in and out of pyoxigraph Store objects — no manual quad writes for every property.
| Install | pip install triplemodel |
| Import | from triplemodel import TripleModel, rdf_field |
| Docs | triplemodel.readthedocs.io |
Person(slug="alice", name="Alice") → (ex:alice, foaf:name, "Alice") → Person(...)
TripleModel is the mapping layer between Pydantic-shaped domain models and RDF triples: subject IRIs, XSD literals, nested resources, rdf:List, language tags, graph sync, and file parse/serialize. It is stateless (no ORM session); SparqlModel (sessions, SPARQL, ORM) builds on top — see the ecosystem guide.
0.12.0 is beta. Public API is frozen from 0.9 until 1.0 — see API stability, changelog, migration 0.11 (and 0.10 pyoxigraph). 0.12.0 is additive (no migration guide). See roadmap.
Install
pip install triplemodel
Requirements: Python 3.10+, Pydantic 2, pyoxigraph 0.5+.
Quick start
from triplemodel import TripleModel, rdf_field
FOAF = "http://xmlns.com/foaf/0.1/"
class Person(TripleModel):
class Rdf:
namespace = "http://example.org/people/"
type_uri = f"{FOAF}Person"
id_field = "slug"
slug: str
name: str = rdf_field(f"{FOAF}name")
age: int | None = rdf_field(f"{FOAF}age", default=None)
alice = Person(slug="alice", name="Alice", age=30)
graph = alice.to_graph()
assert Person.from_graph(graph, alice.subject_uri()) == alice
print(alice.subject_uri())
http://example.org/people/alice
Unmapped fields are ignored on export/import — useful for computed or application-only data.
Features
| Area | Capability |
|---|---|
| Mapping | Nested class Rdf + rdf_field() or Annotated[..., Predicate(...)]; predicate validation at class definition |
| Identity | Subject IRIs from namespace + id_field (percent-encoded ids); IriId for full-IRI ids |
| Scalars | str, int, float, bool, date, datetime; IRI-like str → URIRef; literal_datatype (e.g. xsd:gYear) |
| Collections | set[T] → multiple objects per predicate; list[T] → ordered rdf:List |
| Literals | LangString, Lang(), OpaqueLiteral, ResourceRef |
| Nesting | Child TripleModel with Rdf.embed "iri" or "bnode"; ref_field for URI foreign keys |
| Typing | Rdf.instance_of for Wikidata-style property classification (wdt:P31, etc.) |
| Graph writes | to_graph / sync_to_graph with add, replace, or patch |
| Namespaces | Rdf.prefixes, CURIE predicates ("foaf:name"), bind_namespaces |
| Named graphs | Rdf.graph_iri, to_dataset / from_dataset, TriG / N-Quads via Dataset |
| File I/O | parse / parse_file / parse_url, serialize, load_graph, load_dataset, load_models, load_models_from_graph, load_models_from_dataset, dump_model (Turtle, TriG, N-Triples, JSON-LD, …) |
| Dispatch | parse(..., dispatch=True), graph_to_model_dispatch, all_from_graph_dispatch by rdf:type |
| Inverse predicates | rdf_field(..., inverse=...) for import; forward predicate on export |
| Package typing | PEP 561 py.typed |
| SPARQL | ask, construct_models, select_models, load_sparql, apply_update, prepare_model_query |
| Graph algorithms | graphs_equal, graph_diff, model_diff, cbd_graph / cbd_model, hydrate_refs, model_join |
| RDFS | Subclass-aware dispatch, transitive_objects / transitive_subjects, VocabularyRegistry, Transitive import |
| Scale & stores | iter_graph_to_models, load_models_streaming, parse_into_store_graph, open_graph (memory / disk) |
| Strict import | Rdf.strict_import, Rdf.warn_unmapped_fields on graph_to_model |
| Plugins | triplemodel.plugins — register_predicate_resolver, literal/resource registration |
| Codegen | Experimental triplemodel-codegen CLI (OWL/RDFS → stub models) |
list vs set
| Annotation | RDF shape |
|---|---|
set[str] |
Multiple objects on one predicate (unordered) |
list[str] |
One ordered rdf:List (rdf:first / rdf:rest) |
Use set for tags or duplicate predicates; use list when the graph should contain a real RDF list.
Full example
Language tags, RDF lists, and nested blank-node embeds:
from typing import Annotated
from triplemodel import TripleModel, rdf_field, sync_to_graph
from triplemodel.terms.lang import Lang, LangString
from triplemodel.vocab import DC, FOAF
class Address(TripleModel):
class Rdf:
namespace = "http://example.org/address/"
type_uri = "http://example.org/Address"
id_field = "slug"
slug: str = "home"
street: str = rdf_field("http://example.org/street")
class Person(TripleModel):
class Rdf:
namespace = "http://example.org/people/"
type_uri = f"{FOAF}Person"
id_field = "slug"
embed = "bnode"
prefixes = {"foaf": str(FOAF), "dc": str(DC)}
slug: str
title: LangString = rdf_field(f"{DC}title")
nick: list[str] = rdf_field("foaf:nick", default_factory=list)
address: Address | None = rdf_field("http://example.org/home", default=None)
person = Person(
slug="alice",
title=LangString("Alice's profile", "en"),
nick=["Al", "Alice"],
address=Address(street="1 Main St"),
)
graph = person.to_graph()
person.nick = ["Alice"]
sync_to_graph(person, graph, mode="replace")
again = Person.from_graph(graph, person.subject_uri())
print(again.nick)
['Alice']
Runnable version: examples/exit_criteria_03.py (same models; see also examples/doc/snippets/).
How mapping works
class Rdf — resource metadata
| Attribute | Role |
|---|---|
namespace |
Base IRI; subject = namespace + encoded id_field value |
type_uri |
rdf:type on export; filter for all_from_graph() |
id_field |
Python field whose value becomes the subject id segment |
embed |
"iri" (default) or "bnode" for nested models |
prefixes |
CURIE map → Graph.bind on new graphs |
graph_mode |
Default to_graph mode when mode= is omitted |
blank_node_policy |
"fresh" or "stable" nested bnodes |
skolemize_export / skolemize_import |
Blank-node skolemization defaults |
base_uri |
Default base IRI for resolving relative IRIs on parse |
jsonld_context |
Reserved for API stability; not applied on pyoxigraph (issues UserWarning) |
Override the subject per call with uri= when the IRI still lives under namespace:
graph = alice.to_graph(uri="http://example.org/people/alice")
Helpers: subject_base(), id_from_subject_uri().
Fields → predicates
name: str = rdf_field("foaf:name") # with Rdf.prefixes
from typing import Annotated
from triplemodel import Predicate
title: Annotated[str, Predicate("http://purl.org/dc/terms/title")]
title_fr: Annotated[str, Predicate(f"{DC}title"), Lang("fr")]
Subclasses inherit a parent Rdf when the child does not define one. A child class Rdf: replaces the parent config entirely — never use an empty nested Rdf on a subclass.
Scalar → RDF (export)
| Python | RDF |
|---|---|
str (plain) |
xsd:string |
str with URI scheme (http:, urn:, …) |
URIRef |
int, float, bool, date, datetime |
XSD literal |
LangString, ResourceRef, OpaqueLiteral |
matching literal / IRI |
Register custom types with register_literal_type and LiteralRegistry.
Updating an existing graph
Default to_graph() uses mode="add" — it only appends. To remove triples when fields are cleared, use sync modes:
from triplemodel import sync_to_graph
sync_to_graph(person, graph, mode="replace") # replace all owned triples for this subject
sync_to_graph(person, graph, mode="patch") # per-predicate replace; lighter touch
replace clears nested IRI children and list heads before re-export; patch updates predicates present in the model and clears empty fields (including on nested resources). See the updating graphs guide.
API overview
Instance & class methods
| Method | Description |
|---|---|
subject_uri(uri=None) |
Subject IRI for this instance |
to_triples(uri=None) |
(subject, predicate, object) rows |
to_graph(graph=None, *, uri=None, mode="add", ...) |
Serialize into a Graph |
sync_to_graph(graph, *, uri=None, mode="replace", ...) |
Sync owned triples in-place on this instance |
from_graph(graph, uri, *, resolver=, registry=, ...) |
Load one resource |
all_from_graph(graph, *, type_uri=None, resolver=, registry=, de_skolemize=, ...) |
Load all resources of this type |
parse / parse_file / parse_url |
Parse RDF and return list[TripleModel] (class methods; optional dispatch=True) |
serialize |
Write instance triples to a file or string |
rdf_config() |
Resolved RdfConfig |
Common imports
from triplemodel import (
TripleModel,
rdf_field,
ref_field,
Predicate,
IriId,
GraphMode,
sync_to_graph,
models_to_graph,
load_graph,
load_models,
load_models_from_graph,
load_models_from_dataset,
load_dataset,
parse_into_dataset,
model_to_dataset,
models_to_dataset,
get_graph_context,
graph_to_model_dispatch,
all_from_graph_dispatch,
ask,
apply_update,
construct_models,
select_models,
load_sparql,
open_sparql_graph,
prepare_model_query,
run_sparql,
init_bindings_from_model,
init_ns_from_model,
graph_from_construct_result,
merge_graphs,
expand_curie,
bind_namespaces,
LangString,
Lang,
ResourceRef,
OpaqueLiteral,
RDF,
XSD,
)
Full API: Read the Docs API reference.
More examples
Batch export
from triplemodel import Store
from triplemodel import models_to_graph
graph = models_to_graph([alice, bob])
models_to_graph([alice, bob], Graph()) # merge into existing graph
Encoded subject ids
bob = Person(slug="bob jones", name="Bob")
print(bob.subject_uri())
http://example.org/people/bob%20jones
Multiple tags on one predicate — use set[str] = rdf_field("foaf:topic", default_factory=set).
Runnable scripts: examples/exit_criteria_03.py, examples/readme_examples.py, examples/realworld/ (Nobel, DCAT, Wikidata, Schema.org), and examples/doc/snippets/.
TripleModel vs SparqlModel
| You need | Package |
|---|---|
Pydantic ↔ triples on an in-memory Store |
triplemodel |
File I/O, datasets, SPARQL sessions, cascade put |
SparqlModel (planned TripleModel dependency) |
Known limitations
- Union queries —
from_datasetreads one named graph only; useDataset.queryfor union semantics (see the datasets guide). - BNode embed is experimental; prefer
embed="iri"for stable linking. - Collections —
list[T]/set[T]require scalarT;list[TripleModel]andset[TripleModel]are not supported. - Inverse predicates — import uses forward or inverse triples (forward wins on conflict);
sync_to_graphinadd,replace, orpatchclears incoming inverse triples before writing forward predicates (including reassignment and dropped nested IRI/bnode children). Export writes forward predicates only. - Discovery —
all_from_graph()and dispatch discovery match subjects via forward owned predicates (andrdf:type/instance_of); resources reachable only through inverse triples are not discovered. - Dispatch parse —
parse(..., dispatch=True)loads every registeredrdf:type(not only the class you call.parseon);type_uri=is ignored whendispatch=True. - Skolemize —
skolemize/de_skolemizeon import or export mutate the entire sharedStore, not only the resource being loaded or synced.sync_to_graph(..., mode="patch")runs skolemize after cleanup and export;replace/addskolemize viawrite_model_addbefore appending new triples. - Remote SPARQL graph —
open_sparql_graphandload_sparqlraiseNotImplementedErrorin 0.10.0; load remote data into a localStore(see SPARQL guide). - BNode embed + fresh policy — with default
blank_node_policy="fresh",replace/patchremove and re-export nested blank nodes on every sync even when the nested value is unchanged; useembed="iri"orblank_node_policy="stable"for stable identities. - BNode subjects are skipped by
all_from_graph()and byparse(..., dispatch=True)/all_from_graph_dispatch(). - Subclass dispatch —
parse(..., dispatch=True)andall_from_graph_dispatch()resolve subjects viaRdf.resolve_subclass(RDFS closure when enabled); unregistered types are omitted without error. - Default add mode does not remove stale triples — use
sync_to_graphormode="replace". sync_to_graph()defaults toreplacewhenRdf.graph_modeis"add"(unliketo_graph(), which defaults to add).
Details: user guides · RDF lists & lang.
Development
git clone https://github.com/eddiethedean/triplemodel.git && cd triplemodel
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,docs]"
make ci # pytest, lint, docs (matches GitHub Actions)
make release-check # before tagging: examples + twine check
CI: Python 3.10–3.13. Release process: RELEASING.md.
Documentation
| Resource | Link |
|---|---|
| User guides | guides index |
| API reference | api |
| Changelog | CHANGELOG.md |
| Roadmap | docs/ROADMAP.md |
| Ecosystem | docs/ECOSYSTEM.md |
License
MIT — see LICENSE.
Metadata
Release files for triplemodel 0.12.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| triplemodel-0.12.0.tar.gz | 222.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| triplemodel-0.12.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 335.5 kB
Release files / triplemodel-0.12.0.tar.gz
| Download URL | triplemodel-0.12.0.tar.gz |
|---|---|
| Size | 222.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4443fe48a45f09a7deae17f5a159ed325756516e686888413b6c456ef601610d
|
|
BLAKE2b-256 checksum How to use checksums |
a3e8acbc46f6087ea622eff748cce4aef69ef2e47a2f5f392569dac98d69e996
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Release files / triplemodel-0.12.0-py3-none-any.whl
| Download URL | triplemodel-0.12.0-py3-none-any.whl |
|---|---|
| Size | 113.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2a4a201e3bd361070c035fe33ba3257dd9bf99d8b398a1fc6865eb5c2273ae17
|
|
BLAKE2b-256 checksum How to use checksums |
dd502b878cf98dfb2bac9a24d2e9d13482d8166bbbad4023bb788bc96df8d905
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|