This defines the formal model precisely, establishes vocabulary, states hard rules, and lists explicit non-goals. When in doubt, check against this file before generating code, prose, or schema definitions.
The notation itself isn't the point — the benefits come from what the process of formalizing forces, and those benefits survive translation into plain prose.
It settles ambiguity permanently. Natural language descriptions of data structures
always leave wiggle room. "Edges have types" could mean a dozen things. Writing
It separates schema from instance. The
It gives you a checklist. The 6-tuple (plus
It anchors the vocabulary. Once you've defined
Neither of these are essential to the typed graph as a mathematical object. Both are
implemented using the (also non-essential)
In high-stakes domains like medicine, law, aircraft design, etc., it should be possible to track any assertion in the graph back to the source document from which it was extracted. This makes chains of reasoning traceable and transparent. It allows domain experts to confirm or refute any conclusion reached while reasoning on the graph.
Any high-stakes domain will have meticulously curated ontologies that are widely accepted as authoritative descriptions of the objects and concepts relevant to the field. A canonical ID is an unambiguous reference to a term from such an ontology, frequently framed as a URI. Using these as entity IDs in a graph ensures that reasoning can reliably cross boundaries between diverse papers, journals, and publications, without loss of reasoning validity.
A typed graph G is a 6-tuple
-
$T_V$ — finite set of entity types -
$T_E$ — finite set of predicate types - For each
$p \in T_E$ :-
$\text{dom}(p) \subseteq T_V$ — permitted subject entity types -
$\text{ran}(p) \subseteq T_V$ — permitted object entity types -
$\text{Tr}(p) \subseteq \text{Trait}$ — finite set of semantic traits
-
A field-bearing typed graph additionally equips each entity type and predicate
type with a field schema
-
$V$ — set of entity instances -
$E \subseteq V \times T_E \times V$ — set of directed, typed edge instances (triples) -
$\tau_V: V \to T_V$ — type assignment for entities -
$\tau_E: E \to T_E$ — induced by the middle element of each edge triple
-
Domain/range:
$\forall (v_1,\ p,\ v_2) \in E:\ \tau_V(v_1) \in \text{dom}(p)\ \land\ \tau_V(v_2) \in \text{ran}(p)$ -
Schema conformance: each
$v \in V$ carries fields satisfying$\Phi(\tau_V(v))$ ; each$e \in E$ carries fields satisfying$\Phi(p_e)$
Traits are realized as Python mixin classes inherited alongside BaseEdge.
The unparameterized traits (Symmetric, Transitive, Functional,
InverseFunctional) are plain marker classes. Inverse is a generic parameterized
by the partner predicate type. Rule has no clean type-level expression and is
documented in prose on the edge class.
Examples:
-
LocatedIn(BaseEdge, Transitive)— carries$\text{Tr}(p) = {\text{Transitive}}$ -
MarriedTo(BaseEdge, Symmetric)— carries$\text{Tr}(p) = {\text{Symmetric}}$ -
HasTrueIdentity(BaseEdge, Functional)— carries$\text{Tr}(p) = {\text{Functional}}$ -
Contains(BaseEdge, Inverse[ContainedBy])— carries$\text{Tr}(p) = {\text{Inverse}(\texttt{ContainedBy})}$
Use these terms consistently throughout the book. Do not treat them as synonyms.
| Term | Definition |
|---|---|
| Entity type | A member of EntityInstance. Defines a class of entities: the fields they carry and their permitted roles in edges. Example: Person, Location, Moment. |
| Predicate type | A member of BaseEdge. Defines a class of edges: their field schema, domain, range, and traits. Example: LocatedIn, KnowsAt, Possesses. |
| Entity instance | A member of EntityInstance subclass with an id and data fields. Example: a specific Person instance for Sherlock Holmes. |
| Edge instance | A member of BaseEdge subclass. The thing that carries metadata such as provenance and epistemic status. |
| Field schema | The Pydantic model declaration of named fields and their types for a given entity type or predicate type. Defined by |
| Domain |
subject field on the edge class. |
| Range |
object_ field on the edge class. |
| Trait | A declarative semantic property of a predicate type. Member of class LocatedIn(BaseEdge, Transitive). |
| Schema | The tuple |
| Instance graph | The tuple |
| Reification | Promoting an edge instance to a named entity so that other predicates can take it as an argument. This model eliminates the need for reification to annotate edges — provenance and epistemic fields travel as typed fields on the edge instance itself. Reification remains necessary and appropriate when a proposition must appear in the subject or object role of a higher-order predicate (e.g. an epistemic predicate whose object is a specific asserted fact). That use is intentional, not an anti-pattern. |
- Relationship — use this to mean edge instance, never a predicate type.
- Node — informal synonym for entity instance. Acceptable in casual prose, not in definitions.
- Property — overloaded; could mean a field on an instance or a trait on a predicate type. Be explicit.
- Axiom — not used in this model. Use trait instead.
These rules follow from the formal definition and must not be violated in code examples, schema designs, or explanatory prose.
R1. Traits belong to predicate types, never to edge instances.
A predicate either has Transitive or it does not. That is part of what the
predicate means. An individual edge instance cannot be transitive or
non-transitive — that distinction belongs to its type. In Python, traits are
declared by inheriting the trait mixin class alongside BaseEdge; they are
visible on the class, not on any instance.
R2. Metadata fields belong to edge instances, never to predicate types.
Provenance, confidence, timestamps, epistemic status — all of these are facts
about a particular assertion. They live on the edge instance. The predicate type
defines which fields are required (via
R3. Edges are directed triples, not annotated pairs.
An edge instance is
R4. Domain and range are sets of entity types, not entity instances.
Formally,
R5. Schema is fixed; instances are populated.
Nothing discovered during ingestion changes
R6. Domain and range constraints are enforced by the Python type system.
Entity types are Python classes, not ID prefixes. Each edge class declares its
subject and object_ fields with concrete EntityInstance subclass annotations.
Mypy enforces these constraints statically; Pydantic enforces them at construction
time. No bespoke ID parsing, prefix conventions, or model_validator machinery
is needed or permitted for domain/range enforcement. When a predicate permits
multiple subject or object types, declare a Union (e.g. subject: Person | Organization). The field name object_ is used throughout to avoid shadowing
the Python builtin object.
R7. Pydantic models for edge instances are frozen.
Use model_config = ConfigDict(frozen=True). Edge instances are facts; they
must not be mutated after construction.
Entity types are EntityInstance subclasses. Predicate types are BaseEdge
subclasses with trait mixins inherited alongside. Domain and range constraints
require no custom validation logic — they are expressed as type annotations and
enforced by mypy and Pydantic.
from __future__ import annotations
from typing import ClassVar, Generic, Literal, TypeVar, get_args, get_origin
from pydantic import BaseModel, Field, ConfigDict
# ------------------------------------------------------------------
# Entity base class and concrete types
# ------------------------------------------------------------------
class EntityInstance(BaseModel):
model_config = ConfigDict(frozen=True)
id: str
def __repr__(self) -> str:
return f"{type(self).__name__}({self.id!r})"
class Person(EntityInstance): ...
class Location(EntityInstance): ...
class Moment(EntityInstance): ...
class Object(EntityInstance): ...
class Document(EntityInstance): ...
# ------------------------------------------------------------------
# Trait mixin classes
#
# Unparameterized traits are plain marker classes. Inverse is a generic
# parameterized by the partner predicate type. Rule has no clean
# type-level expression and is documented in prose on the edge class.
#
# Trait must not inherit from BaseModel; it is a plain mixin so that
# Pydantic's ModelMetaclass remains in control of edge class construction.
# ------------------------------------------------------------------
class Trait:
"""Marker base for all semantic traits."""
...
class Transitive(Trait): ...
class Symmetric(Trait): ...
class Functional(Trait): ...
class InverseFunctional(Trait): ...
P = TypeVar('P', bound='BaseEdge')
class Inverse(Trait, Generic[P]):
"""This predicate is the inverse of P."""
...
def get_inverse(cls: type[BaseEdge]) -> type[BaseEdge] | None:
"""Return the declared inverse predicate type, if any."""
for base in getattr(cls, '__orig_bases__', []):
if get_origin(base) is Inverse:
return get_args(base)[0]
return None
# ------------------------------------------------------------------
# Edge base class and concrete predicates
# ------------------------------------------------------------------
class BaseEdge(BaseModel):
model_config = ConfigDict(frozen=True)
class LocatedIn(BaseEdge, Transitive):
"""A Location is situated within another Location."""
subject: Location
object_: Location
class ContainedBy(BaseEdge, Inverse['Contains']):
"""A Location is contained by another Location."""
subject: Location
object_: Location
class Contains(BaseEdge, Inverse[ContainedBy]):
"""A Location contains another Location."""
subject: Location
object_: Location
class PossessesAt(BaseEdge):
"""A Person possesses an Object as of a given Moment."""
subject: Person
object_: Object
at_moment: Moment
confidence: float = Field(ge=0.0, le=1.0, default=1.0)
known_to: frozenset[str] = frozenset() # person IDs
epistemic_status: Literal[
"ground_truth", "believed", "false_belief", "inferred"
] = "ground_truth"
class MentionedIn(BaseEdge):
"""A Person or Location is mentioned in a Document."""
subject: Person | Location
object_: DocumentPassing a Location where a Person is required is a mypy error and a Pydantic
ValidationError at construction time. No runtime prefix parsing is involved.
Fields that reference other entities — such as at_moment — carry typed
EntityInstance subclass values, not strings.
Traits are introspectable at runtime: issubclass(LocatedIn, Transitive) is
True; get_inverse(Contains) returns ContainedBy. The ContainedBy forward
reference in Inverse['Contains'] resolves once both classes are defined.
Not RDF / OWL. In RDF, predicates are URIs and are themselves nodes; the graph is a flat set of triples with no first-class edge objects. OWL adds description logic semantics and open-world assumption. This model is a closed-world property graph with typed, field-bearing edge instances. The distinction matters especially for annotating edges: RDF requires reification or named graphs; this model puts metadata directly on the edge instance.
Not Neo4j's informal property graph.
Neo4j allows arbitrary key-value properties on edges without schema enforcement.
This model requires a declared field schema (
Not an entity-relationship diagram. ER diagrams are a database design tool. This is a runtime knowledge representation with provenance, epistemic scope, and trait-based inference semantics.
Not a general ontology language. This model does not support open-world reasoning, class hierarchies, disjointness axioms, or the full OWL trait vocabulary. Traits are a small, fixed set of declarative properties. If a use case seems to require full description logic, that is scope creep.
Not a stringly-typed system. Entity types are Python classes, not ID prefixes. Domain and range enforcement is the job of the Python type system and Pydantic, not of string parsing. Any pattern that encodes type information inside an identifier string and then parses that string to enforce constraints is a violation of R6.
The worked example uses the Sherlock Holmes canon as domain.
- Ontology authority: Baker Street Wiki (https://bakerstreet.fandom.com)
- Schema construction method: inductive — built by annotating stories, not pre-designed
- Primary stories: "A Scandal in Bohemia", "The Speckled Band"
- Provisional entity types:
Person,Location,Object,Document,Moment,Event,Persona,Plan - Epistemic fields on edge instances:
known_to,at_moment,epistemic_status
The Holmes schema must satisfy all hard rules above. If a design decision for the Holmes domain seems to require violating a rule, that is a signal to revisit the decision, not to bend the rule.