pylingEYE N3 Reasoner
Notebooks PyPI GitHub

Pyling notebook 01

When an RDF graph needs to say more¶

RDF is good at recording what we know. Applications often need the consequences of those facts as well.

We will start with a two-triple graph: Socrates is a man, and every man belongs to the broader class of mortals. RDFLib can store and query that graph, but it will not invent the missing type assertion on its own. This first notebook adds one N3 rule and brings the inferred graph back into RDFLib, where an ordinary Python application can use it.

The point is not the famous syllogism. It is the integration pattern: RDFLib in, N3 reasoning, RDFLib out.

1. Begin with the facts the application owns¶

Keeping data construction in RDFLib means existing parsers, stores, namespace helpers, and SPARQL code remain useful. At this point the graph contains only explicit statements.

In [1]:
from rdflib import Graph, Namespace, RDF
from pyling import reason_stream

EX = Namespace("http://example.org/")

graph = Graph()
graph.bind("ex", EX)
graph.add((EX.Socrates, RDF.type, EX.Man))
graph.add((EX.Man, EX.subClassOf, EX.Mortal))

print(f"The application supplied {len(graph)} triples")
print(graph.serialize(format="turtle"))
The application supplied 2 triples
@prefix ex: <http://example.org/> .

ex:Socrates a ex:Man .

ex:Man ex:subClassOf ex:Mortal .


2. State the missing implication¶

The rule below is deliberately independent of Socrates. It says that any resource typed as a class also has the type reached through ex:subClassOf. The variables turn one local example into reusable domain logic.

In [2]:
rules = """
@prefix : <http://example.org/> .

{
  ?resource a ?class .
  ?class :subClassOf ?superClass .
} => {
  ?resource a ?superClass .
} .
"""

3. Materialize the consequence¶

reason_stream accepts several sources in one run. We serialize the RDFLib graph, add the rules, and ask pyling to retain the input facts in the closure. The result can then be converted straight back to an RDFLib graph.

In [3]:
result = reason_stream(
    {"sources": [graph.serialize(format="turtle"), rules]},
    include_input_facts_in_closure=True,
)
closure = result.as_rdflib_graph(include_input_facts=True)

assert (EX.Socrates, RDF.type, EX.Mortal) in closure
print(f"{len(result.derived)} new triple was derived")
print("Socrates is mortal:", (EX.Socrates, RDF.type, EX.Mortal) in closure)
1 new triple was derived
Socrates is mortal: True

4. Inspect the boundary between input and inference¶

Applications usually care about that distinction. The original graph is unchanged; result.derived and result.closure_n3 expose what the reasoner added, while closure offers the combined graph for downstream RDFLib code.

In [4]:
print("Original graph:", len(graph), "triples")
print("Combined closure:", len(closure), "triples")
print("\nDerived N3:")
print(result.closure_n3)
Original graph: 2 triples
Combined closure: 3 triples

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

:Man :subClassOf :Mortal .
:Socrates a :Man .
:Socrates a :Mortal .

Where this leads¶

One hand-written rule is easy to understand, but real vocabularies contain many interacting implications. In notebook 02, we keep the same materialization pattern and replace the toy rule with a maintained OWL 2 RL profile.

Python N3 reasoning in the EYE ecosystem. All notebooks