Skip to content

type_bridge.models.role

role

Role descriptor for TypeDB relation role players.

Role

Role(role_name, player_type=None, *additional_player_types, cardinality=None, plays_cardinality=None, overrides=None, abstract=False, ordered=False, distinct=False, doc=None, meta=None)

Descriptor for relation role players with type safety.

Generic type T represents the type (Entity or Relation) that can play this role. TypeDB supports both entities and relations as role players.

Example

Entity as role player

class Employment(Relation): employee: Role[Person] = Role("employee", Person) employer: Role[Company] = Role("employer", Company)

Relation as role player

class Permission(Relation): permitted_subject: Role[Subject] = Role("permitted_subject", Subject) permitted_access: Role[Access] = Role("permitted_access", Access) # Access is a Relation

Initialize a role.

Parameters:

Name Type Description Default
role_name str

The name of the role in TypeDB

required
player_type type[T] | None

The optional type (Entity or Relation) that can play this role

None
additional_player_types type[T]

Optional additional types allowed to play this role

()
cardinality Card | None

Optional relates-side cardinality — players allowed per relation (e.g., Card(2, 2) for exactly 2 players)

None
plays_cardinality Card | None

Optional plays-side cardinality — relations a single player may play this role in (e.g., Card(0, 1) to enforce "at most one"). Distinct from cardinality; attaches to the player's plays edge, so it requires a player type.

None
overrides str | None

Parent role name that this role specializes via TypeDB's relates child as parent syntax. Used only for descriptor computation (effective-set role exclusion); specialization semantics are resolved at schema-define time.

None
abstract bool

When True, marks this role as abstract at the TypeDB schema level (emitted as @abstract on the relates clause). The engine rejects direct players at the declaring relation's own scope; subtypes that plain-inherit or override the role are unaffected.

False
ordered bool

When True, declares this role as a list role (relates name[] in TypeQL). Schema-only; instance-level list writes are not yet supported by the engine.

False
distinct bool

When True, emits @distinct on the relates clause. Requires ordered=True; raises ValueError otherwise.

False
doc str | None

TypeDB 3.12+ @doc("...") documentation emitted on the relates clause.

None
meta dict[str, str] | None

TypeDB 3.12+ @meta("key", "value") annotations emitted on the relates clause, one value per key.

None

Raises:

Type Description
ReservedWordError

If role_name is a TypeQL reserved word

TypeError

If player type is a library base class (Entity, Relation, TypeDBType), or if plays_cardinality is set on a relates-only role (no player type)

ValueError

If distinct=True without ordered=True

Source code in type_bridge/models/role.py
def __init__(
    self,
    role_name: str,
    player_type: type[T] | None = None,
    *additional_player_types: type[T],
    cardinality: Card | None = None,
    plays_cardinality: Card | None = None,
    overrides: str | None = None,
    abstract: bool = False,
    ordered: bool = False,
    distinct: bool = False,
    doc: str | None = None,
    meta: dict[str, str] | None = None,
):
    """Initialize a role.

    Args:
        role_name: The name of the role in TypeDB
        player_type: The optional type (Entity or Relation) that can play this role
        additional_player_types: Optional additional types allowed to play this role
        cardinality: Optional relates-side cardinality — players allowed per relation
            (e.g., Card(2, 2) for exactly 2 players)
        plays_cardinality: Optional plays-side cardinality — relations a single player
            may play this role in (e.g., Card(0, 1) to enforce "at most one"). Distinct
            from ``cardinality``; attaches to the player's plays edge, so it requires a
            player type.
        overrides: Parent role name that this role specializes via TypeDB's
            ``relates child as parent`` syntax. Used only for descriptor computation
            (effective-set role exclusion); specialization semantics are resolved at
            schema-define time.
        abstract: When ``True``, marks this role as abstract at the TypeDB schema level
            (emitted as ``@abstract`` on the ``relates`` clause). The engine rejects
            direct players at the declaring relation's own scope; subtypes that
            plain-inherit or override the role are unaffected.
        ordered: When ``True``, declares this role as a list role (``relates name[]``
            in TypeQL). Schema-only; instance-level list writes are not yet supported
            by the engine.
        distinct: When ``True``, emits ``@distinct`` on the relates clause. Requires
            ``ordered=True``; raises ``ValueError`` otherwise.
        doc: TypeDB 3.12+ ``@doc("...")`` documentation emitted on the relates clause.
        meta: TypeDB 3.12+ ``@meta("key", "value")`` annotations emitted on the
            relates clause, one value per key.

    Raises:
        ReservedWordError: If role_name is a TypeQL reserved word
        TypeError: If player type is a library base class (Entity, Relation, TypeDBType),
            or if plays_cardinality is set on a relates-only role (no player type)
        ValueError: If distinct=True without ordered=True
    """
    # Validate role name doesn't conflict with TypeQL reserved words
    validate_reserved_word(role_name, "role")

    if distinct and not ordered:
        raise ValueError(
            f"Role '{role_name}': distinct=True requires ordered=True. "
            "@distinct is only valid on a list role (`relates name[]`)."
        )

    self.role_name = role_name
    self.cardinality = cardinality
    self.plays_cardinality = plays_cardinality
    self.overrides = overrides
    self.is_abstract = abstract
    self.ordered = ordered
    self.distinct = distinct
    self.doc = doc
    self.meta: dict[str, str] = dict(meta) if meta else {}
    unique_types: list[type[T]] = []
    if player_type is None:
        if additional_player_types:
            raise TypeError(
                f"Role '{role_name}' cannot declare additional player types when the "
                "first player type is None"
            )
    else:
        for typ in (player_type, *additional_player_types):
            # Validate that we're not using library base classes directly
            self._validate_player_type(typ)
            if typ not in unique_types:
                unique_types.append(typ)

    self.player_entity_types: tuple[type[T], ...] = tuple(unique_types)
    self.player_entity_type: type[T] | None = unique_types[0] if unique_types else None
    # Get type name from the entity class(es)
    self.player_types = tuple(pt.get_type_name() for pt in self.player_entity_types)
    self.player_type = (
        self.player_entity_type.get_type_name() if self.player_entity_type else None
    )
    if plays_cardinality is not None and not self.player_entity_types:
        raise TypeError(
            f"Role '{role_name}' sets plays_cardinality but declares no player type. "
            "Plays-side cardinality constrains a player's plays edge; a relates-only "
            "role has no plays edge to constrain."
        )
    self.attr_name: str | None = None

is_multi_player property

is_multi_player

Check if this role allows multiple players.

Returns True if cardinality allows more than one player (max > 1 or unbounded).

is_optional property

is_optional

Check if this role allows zero players.

is_relates_only property

is_relates_only

Check if this role declares no player type.

__set_name__

__set_name__(owner, name)

Called when role is assigned to a class.

Source code in type_bridge/models/role.py
def __set_name__(self, owner: type, name: str) -> None:
    """Called when role is assigned to a class."""
    self.attr_name = name

__get__

__get__(obj: None, objtype: type[T_RelationOwner]) -> RoleRef[T, T_RelationOwner]
__get__(obj: Any, objtype: type[T_RelationOwner]) -> T
__get__(obj, objtype)

Get role player from instance or RoleRef from class.

When accessed from the class (obj is None), returns RoleRef for type-safe query building (e.g., Employment.employee.age.gt(Age(30))). When accessed from an instance, returns the entity playing the role.

Source code in type_bridge/models/role.py
def __get__(
    self, obj: Any, objtype: type[T_RelationOwner]
) -> T | RoleRef[T, T_RelationOwner] | None:
    """Get role player from instance or RoleRef from class.

    When accessed from the class (obj is None), returns RoleRef for
    type-safe query building (e.g., Employment.employee.age.gt(Age(30))).
    When accessed from an instance, returns the entity playing the role.
    """
    if obj is None:
        from type_bridge.fields.role import RoleRef, _mark_typed_query_role_reference

        return _mark_typed_query_role_reference(
            RoleRef(
                role_name=self.role_name,
                player_types=self.player_entity_types,
                owner_type=objtype,
                cardinality=self.cardinality,
                plays_cardinality=self.plays_cardinality,
            )
        )
    if self.is_relates_only:
        return None
    return obj.__dict__.get(self.attr_name)

__set__

__set__(obj, value)

Set role player(s) on instance.

For roles with cardinality > 1, accepts a list of entities. For single-player roles, accepts a single entity.

Source code in type_bridge/models/role.py
def __set__(self, obj: Any, value: T | list[T]) -> None:
    """Set role player(s) on instance.

    For roles with cardinality > 1, accepts a list of entities.
    For single-player roles, accepts a single entity.
    """
    if self.is_relates_only:
        raise TypeError(
            f"Role '{self.role_name}' is relates-only; it declares no player "
            "to bind on this relation"
        )

    if value is None:
        if not self.is_optional:
            allowed = ", ".join(pt.__name__ for pt in self.player_entity_types)
            raise TypeError(f"Role '{self.role_name}' expects types ({allowed}), got NoneType")
        obj.__dict__[self.attr_name] = None
        return

    if isinstance(value, list):
        # Multi-player role - validate each item in the list
        if not self.is_multi_player:
            raise TypeError(
                f"Role '{self.role_name}' does not allow multiple players. "
                f"Use cardinality=Card(...) to enable multi-player roles."
            )
        for item in value:
            if not isinstance(item, self.player_entity_types):
                allowed = ", ".join(pt.__name__ for pt in self.player_entity_types)
                raise TypeError(
                    f"Role '{self.role_name}' expects types ({allowed}), "
                    f"got {type(item).__name__} in list"
                )
        obj.__dict__[self.attr_name] = value
    else:
        # Single player
        if not isinstance(value, self.player_entity_types):
            allowed = ", ".join(pt.__name__ for pt in self.player_entity_types)
            raise TypeError(
                f"Role '{self.role_name}' expects types ({allowed}), got {type(value).__name__}"
            )
        obj.__dict__[self.attr_name] = value

multi classmethod

multi(role_name, player_type, *additional_player_types, cardinality=None, plays_cardinality=None, overrides=None, abstract=False, ordered=False, distinct=False)

Define a role playable by multiple entity types.

Parameters:

Name Type Description Default
role_name str

The name of the role in TypeDB

required
player_type type[T]

The first entity type that can play this role

required
additional_player_types type[T]

Additional entity types allowed to play this role

()
cardinality Card | None

Optional relates-side cardinality constraint for the role

None
plays_cardinality Card | None

Optional plays-side cardinality applied to every player's plays edge for this role

None
overrides str | None

Parent role name this role specializes (see Role.__init__).

None
abstract bool

When True, marks this role as abstract (see Role.__init__).

False
ordered bool

When True, declares this role as a list role (relates name[]).

False
distinct bool

When True, emits @distinct; requires ordered=True.

False
Source code in type_bridge/models/role.py
@classmethod
def multi(
    cls,
    role_name: str,
    player_type: type[T],
    *additional_player_types: type[T],
    cardinality: Card | None = None,
    plays_cardinality: Card | None = None,
    overrides: str | None = None,
    abstract: bool = False,
    ordered: bool = False,
    distinct: bool = False,
) -> Role[T]:
    """Define a role playable by multiple entity types.

    Args:
        role_name: The name of the role in TypeDB
        player_type: The first entity type that can play this role
        additional_player_types: Additional entity types allowed to play this role
        cardinality: Optional relates-side cardinality constraint for the role
        plays_cardinality: Optional plays-side cardinality applied to every player's
            plays edge for this role
        overrides: Parent role name this role specializes (see ``Role.__init__``).
        abstract: When ``True``, marks this role as abstract (see ``Role.__init__``).
        ordered: When ``True``, declares this role as a list role (``relates name[]``).
        distinct: When ``True``, emits ``@distinct``; requires ``ordered=True``.
    """
    if len((player_type, *additional_player_types)) < 2:
        raise ValueError("Role.multi requires at least two player types")
    return cls(
        role_name,
        player_type,
        *additional_player_types,
        cardinality=cardinality,
        plays_cardinality=plays_cardinality,
        overrides=overrides,
        abstract=abstract,
        ordered=ordered,
        distinct=distinct,
    )

__get_pydantic_core_schema__ classmethod

__get_pydantic_core_schema__(source_type, handler)

Define how Pydantic should validate Role fields.

Accepts either: - A single entity instance for single-player roles - A list of entity instances for multi-player roles (cardinality > 1)

Uses a custom validator that checks class names instead of isinstance, to handle generated code in different modules where the same class name exists but as a different Python object.

Source code in type_bridge/models/role.py
@classmethod
def __get_pydantic_core_schema__(
    cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
    """Define how Pydantic should validate Role fields.

    Accepts either:
    - A single entity instance for single-player roles
    - A list of entity instances for multi-player roles (cardinality > 1)

    Uses a custom validator that checks class names instead of isinstance,
    to handle generated code in different modules where the same class name
    exists but as a different Python object.
    """
    import types

    from pydantic_core import core_schema

    # Extract the entity type(s) from Role[T]
    # Handle both Role[Entity] and Role[Entity1 | Entity2] unions
    allowed_names: set[str] = set()

    if hasattr(source_type, "__args__") and source_type.__args__:
        for arg in source_type.__args__:
            # Check if it's a union type (e.g., Document | Email)
            if isinstance(arg, types.UnionType) or (
                hasattr(arg, "__origin__") and arg.__origin__ is type(int | str)
            ):
                # It's a union - get the individual types
                if hasattr(arg, "__args__"):
                    for union_arg in arg.__args__:
                        if hasattr(union_arg, "__name__"):
                            allowed_names.add(union_arg.__name__)
            elif hasattr(arg, "__name__"):
                allowed_names.add(arg.__name__)

    def validate_role_player(value: Any) -> Any:
        """Validate that value is an allowed entity type by class name.

        Checks the full inheritance chain (MRO) to support subclasses.
        E.g., if Document is allowed and Report is a subclass of Document,
        Report instances are accepted.
        """
        if value is None:
            return None

        if not allowed_names:
            # No type constraints - allow anything
            return value

        def is_allowed_type(obj: Any) -> bool:
            """Check if obj's class or any base class matches allowed names."""
            # Check entire MRO (Method Resolution Order) for inheritance support
            for cls in type(obj).__mro__:
                if cls.__name__ in allowed_names:
                    return True
            return False

        if isinstance(value, list):
            # List of entities for multi-player roles
            for item in value:
                if not is_allowed_type(item):
                    raise ValueError(
                        f"Expected one of {allowed_names}, got {type(item).__name__}"
                    )
            return value
        else:
            # Single entity
            if not is_allowed_type(value):
                raise ValueError(f"Expected one of {allowed_names}, got {type(value).__name__}")
            return value

    return core_schema.no_info_plain_validator_function(validate_role_player)