Skip to content

API Reference

Generated reference for PyChor's public objects. Everything listed here is re-exported from the pychor package, so pychor.Party and pychor.choreography.Party are the same class.

For the ideas behind these objects, read Concepts; for choosing and configuring a backend, read Backends.

Core API

The objects a choreography is written with: parties to own values, located values to carry them, and the three functions that run ordinary Python code at a party.

Party dataclass

A named participant in a choreography.

Party objects are used as ownership labels for located values. The expression value @ party locates an ordinary Python value at that party when a choreography backend is active.

Parties are frozen dataclasses, so they are hashable and compare by name, and the same Party object can be shared freely across a program. The order of the party list passed to a backend is significant for the TCP backends: it fixes the connection order and, for ForkingTCPBackend, the port assignment.

Parameters:

Name Type Description Default
name str

Human-readable party name used in representations and sequence diagrams.

required
Example
alice = pychor.Party("alice")
bob = pychor.Party("bob")

with pychor.SimulationBackend(parties=[alice, bob]):
    x = 5 @ alice

constant

constant(v: Any) -> Any

Locate a value or function at this party.

Ordinary values become LocatedVal objects owned by this party. Callables become wrappers that execute the callable locally through the active backend, so (lambda x: x + 1) @ alice is a function that runs at alice.

Parameters:

Name Type Description Default
v Any

The value to locate, or a callable to run at this party.

required

Returns:

Type Description
Any

A LocatedVal owned by this party, or — when v is callable — a wrapper that runs v as a local computation.

Raises:

Type Description
AssertionError

If no backend is currently active.

__rmatmul__

__rmatmul__(v)

Implement value @ party as shorthand for party.constant(value).

view

view() -> list

Return the values this party has received in the active backend.

A party's view is the ordered list of message payloads delivered to it by LocatedVal.send, and it is the audit trail that simulation-based security arguments inspect: a protocol is private with respect to a party if that party's view can be reproduced by a simulator that never sees the honest inputs. examples/protocol_sum.py compares protocol and simulator views statistically using this method.

Each backend context starts with empty views, so a program that samples views over many runs must re-enter the backend for each run.

Returns:

Type Description
list

The list of values received by this party so far. Under a TCP backend, only the calling process's own party has a populated view.

Raises:

Type Description
AttributeError

If no backend is currently active.

LocatedVal dataclass

A Python value together with the parties that can currently observe it.

Located values are produced by locating constants, sending values between parties, and running local computations. A located value is the unit of ownership in a choreography: parties records who knows the value, and the backend refuses to compute with a value at a party that does not own it.

Ownership grows when a value is sent — send adds the destination to parties — and is narrowed with only.

Under SimulationBackend, val always holds the real value because a single process plays every party. Under the TCP backends, val holds the real value only in the processes whose party appears in parties, and is None everywhere else; see pychor.tcp_backend and the Concepts page of the documentation.

Parameters:

Name Type Description Default
parties Set[Party]

Parties that currently know the underlying value.

required
val Any

The underlying Python value in the active backend.

required
note Optional[str]

Optional annotation for protocol explanations or diagrams.

None
Operators

+, -, *, /, % and unary - are defined on located values and are each shorthand for a local computation: x + y is equivalent to locally(lambda a, b: a + b, x, y), so it runs at the parties that own both operands and produces a new located value. Reflected forms (__radd__ and friends) are defined too, so a plain Python int, float, str, or bytes may appear on either side; a plain value places no ownership constraint of its own. This is what lets share arithmetic read like ordinary Python — see examples/protocol_beaver.py, where a Beaver multiplication is written r_1 = d * e + d * b1 + e * a1 + c1.

Comparisons use dataclass equality, which compares the underlying values and the owner sets; there is no located boolean type.

send

send(src: Party, dest: Party, note: Optional[str] = None) -> None

Send this located value from src to dest.

The active backend performs the communication and adds dest to this value's owner set, so the same LocatedVal object is afterwards readable at both parties. Sending mutates this value in place rather than returning a new one.

Parameters:

Name Type Description Default
src Party

The sending party, which must already own this value.

required
dest Party

The receiving party.

required
note Optional[str]

Optional label for this message. SimulationBackend appends it to the corresponding edge of the sequence diagram, which is useful for explaining what a message carries — see examples/protocol_commit.py.

None

Raises:

Type Description
AssertionError

If src does not own this value.

unlist

unlist(length: int) -> list

Un-structure a located list into a list of located values.

A local computation that returns a list produces a single located list. This splits it into length located values, each owned by the same parties, so the individual elements can be sent to different places.

Parameters:

Name Type Description Default
length int

The expected length of the underlying list.

required

Returns:

Type Description
list

A Python list of length located values.

untup

untup(length: int) -> tuple

Un-structure a located tuple into a tuple of located values.

This is the usual way to return several results from one local computation, as in s1, s2 = share(x).untup(2).

Parameters:

Name Type Description Default
length int

The expected length of the underlying tuple.

required

Returns:

Type Description
tuple

A Python tuple of length located values.

undict

undict(keys: Iterable) -> dict

Un-structure a located dict into a dict of located values.

Parameters:

Name Type Description Default
keys Iterable

The expected keys of the underlying dict.

required

Returns:

Type Description
dict

A Python dict mapping each key to a located value.

only

only(parties: Union[Party, Iterable[Party]]) -> LocatedVal

Return this value restricted to a subset of its current owners.

Narrowing ownership is how a choreography says "treat this as bob's value from here on". It matters because a local computation runs at the intersection of its arguments' owners: after x.send(src=alice, dest=bob), x is owned by both parties, so a computation over x would run at both. Passing x.only(bob) instead pins the computation to bob.

This does not communicate anything and does not modify this value; it returns a new LocatedVal sharing the same underlying value.

Parameters:

Name Type Description Default
parties Union[Party, Iterable[Party]]

A single Party, or a collection of parties, all of which must already own this value.

required

Returns:

Type Description
LocatedVal

A new located value owned by exactly parties.

Raises:

Type Description
AssertionError

If any requested party does not already own this value, or if an empty collection is given.

TypeError

If parties is neither a Party nor a collection of parties.

constant

constant(party: Party, v: Any) -> LocatedVal

Create a located value owned by party.

party.constant(v) and v @ party are the usual spellings; this function is the explicit form, useful when the party is computed.

Parameters:

Name Type Description Default
party Party

The owning party, which must belong to the active backend.

required
v Any

The value to locate.

required

Returns:

Type Description
LocatedVal

A LocatedVal owned by party.

Raises:

Type Description
AssertionError

If no backend is currently active.

ValueError

If party is not part of the active backend.

locally

locally(f: Callable, *args: Any, **kwargs: Any) -> LocatedVal

Run f as a local computation in the active backend.

The computation runs at the parties that own every located argument, and the result is located at exactly those parties. Plain Python scalars may be mixed in freely and place no constraint on where the computation runs.

Under a TCP backend this call is a no-op in processes whose party is not among the owners, and the returned located value carries None there.

Parameters:

Name Type Description Default
f Callable

An ordinary Python function.

required
*args Any

Located values, plain scalars, or tuples, lists, and dicts of those.

()
**kwargs Any

Keyword arguments, handled the same way as args.

{}

Returns:

Type Description
LocatedVal

A located value holding f's result, owned by the parties that could see all of the located inputs.

Raises:

Type Description
AssertionError

If the located arguments have no owner in common.

Example
y = pychor.locally(lambda value: value + 1, x)

local_function

local_function(func: Callable) -> Callable

Decorate a Python function so it becomes backend-aware.

Outside a backend context, the decorated function behaves like the original function, which keeps helpers testable on their own. Inside a backend context, calling it runs locally and returns a located value, so the same definition serves both purposes.

Parameters:

Name Type Description Default
func Callable

The function to make backend-aware.

required

Returns:

Type Description
Callable

A wrapper that dispatches to locally whenever a backend is active.

Example
@pychor.local_function
def add_bonus(value, bonus):
    return value + bonus

with pychor.SimulationBackend(parties=[alice, bob]):
    y = add_bonus(5 @ alice, 2)

get_val

get_val(lv: Any) -> tuple

Unwrap located values and determine where a computation may run.

This is the helper backends use to implement locally. It walks an argument structure, replaces each LocatedVal with its underlying value, and intersects the owner sets it encounters to decide which parties can perform the computation. Tuples, lists, and dicts are traversed recursively, so a local function may take nested structures of located values.

Plain int, float, str, and bytes values pass through with no owner set, meaning they place no constraint on where the computation runs; this is what allows x + 1 for a located x.

Parameters:

Name Type Description Default
lv Any

A located value, a plain scalar, or a tuple, list, or dict of those.

required

Returns:

Type Description
tuple

A (values, parties) pair, where values mirrors the structure of lv with located values replaced by their contents, and parties is the set of parties able to run the computation — or None if lv contained no located values at all.

Raises:

Type Description
AssertionError

If the located values have no owner in common.

Exception

If lv contains a value that is neither located, a supported scalar, nor a supported container.

Backends

A backend decides what locating, computing, and sending mean, and is entered as a context manager. ChoreographyBackend is the interface to implement for a new one — see Writing a custom backend.

ChoreographyBackend

Base context manager for choreography backends.

Entering a backend context makes it the active backend for Party, LocatedVal, locally, and local_function operations. Subclasses provide the actual execution and communication semantics.

A backend must implement constant, send, locally, unwrap, unlist, untup, and undict. The methods defined here are the interface: all but constant are stubs, and a subclass that leaves one unimplemented will silently return None from the corresponding operation rather than raise. SimulationBackend is the reference implementation; pychor.tcp_backend.TCPBackend shows the same interface implemented across processes.

Subclasses that override __enter__ or __exit__ must call super().__enter__() / super().__exit__() so that the active-backend global is installed and cleared.

Attributes:

Name Type Description
parties

The backend's parties, as a tuple, in the order given. Order is significant for the TCP backends.

party_set

The same parties as a set, for membership tests.

views

Maps each party to the ordered list of values it has received. This is the protocol transcript from each party's point of view, and it is what a simulation-based security argument compares; see Party.view. Views start empty in each new backend context.

Parameters:

Name Type Description Default
parties Iterable[Party]

An iterable of Party objects with unique names. At least one party is required.

required

constant

constant(party: Party, v: Any) -> LocatedVal

Create a located value owned by party.

send

send(party_from: Party, party_to: Party, lv: LocatedVal, note: Optional[str] = None) -> None

Send a located value from one party to another.

Implementations must add party_to to lv.parties and record the received value in self.views[party_to].

locally

locally(f: Callable, *args: Any, **kwargs: Any) -> LocatedVal

Compute a function locally using located arguments.

Implementations must evaluate f at the intersection of the owner sets of the located arguments, and return a located value owned by exactly that intersection.

unwrap

unwrap(lv: LocatedVal, parties: Set[Party]) -> Any

Return the raw value when all requested parties can observe it.

Implementations return None rather than raising when the value is not available.

unlist

unlist(ls, length)

Un-structure a located list into a list of located values.

untup

untup(ls, length)

Un-structure a located tuple into a list of located values.

undict

undict(d, keys)

Un-structure a located dict into a dict of located values.

SimulationBackend

Bases: ChoreographyBackend

Run a choreography in a single local Python process.

SimulationBackend is useful for tutorials, tests, and protocol sketches. One process plays every party, so every located value is genuinely present and a choreography always runs to completion quickly, with no network setup.

The trade-off is that this backend cannot detect a choreography that reads a value it does not own: nothing is actually distributed, so an illegal read still finds a real value in memory. Run a protocol under pychor.tcp_backend.ForkingTCPBackend to check that it is genuinely executable one-party-per-process.

In exchange, this backend can observe the whole protocol at once. It records each send into views and accumulates a Mermaid sequence diagram in uml, which print_sequence_diagram prints and which can be pasted directly into Mermaid-aware Markdown.

Attributes:

Name Type Description
uml

The accumulated Mermaid sequence diagram source, beginning with the line sequenceDiagram.

Example
with pychor.SimulationBackend(parties=[alice, bob]) as backend:
    x = 5 @ alice
    x.send(src=alice, dest=bob)
    backend.print_sequence_diagram()

send

send(party_from: Party, party_to: Party, lv: LocatedVal, note: Optional[str] = None) -> None

Record a local send and add the destination to the value owners.

The value is appended to the destination party's view and an edge is added to the sequence diagram. Values whose string form exceeds ten characters are truncated with an ellipsis in the diagram only; the value delivered to the destination is never truncated.

locally

locally(f: Callable, *args: Any, **kwargs: Any) -> LocatedVal

Evaluate f on the raw values of co-located arguments.

The result is owned by the intersection of the owner sets of the located arguments. Plain Python scalars among the arguments contribute no ownership constraint.

Raises:

Type Description
AssertionError

If the located arguments have no owner in common.

unwrap

unwrap(lv: LocatedVal, p: Set[Party]) -> Any

Return a raw value when all requested parties own it.

Returns:

Type Description
Any

The underlying value, or None if any requested party is not an owner.

unlist

unlist(ls, length)

Split a located list of length items into that many located values.

untup

untup(ls, length)

Split a located tuple of length items into that many located values.

undict

undict(d, keys)

Split a located dict into a dict of located values, one per key.

emit_to_sequence

emit_to_sequence(string: str) -> None

Append a line to the backend's Mermaid sequence diagram.

Use this to add Mermaid directives of your own — notes, activations, or participant declarations — alongside the edges send records.

Parameters:

Name Type Description Default
string str

One line of Mermaid sequenceDiagram source, without a trailing newline.

required

print_sequence_diagram

print_sequence_diagram()

Print the Mermaid sequence diagram recorded so far.

The diagram is also available as uml if you would rather write it to a file or embed it in Markdown. For the choreography in examples/protocol_simple.py, this prints:

sequenceDiagram
party1 ->> party2 : 5
party2 ->> party1 : 11

TCPBackend

Bases: ChoreographyBackend

Run one party of a choreography over TCP.

This backend does not fork. Each party should run the same choreography program in a separate Python process, passing its local party as me and a shared address map for all parties.

Every process must agree on the parties list and its order, because the order determines which side of each connection dials and which listens. Entering the context establishes a full mesh between all parties and blocks until it is complete, so the processes may be started in any order as long as they all start within connect_timeout.

Only this process's own party's values are materialized: constant and locally produce None for values this party does not own, and send transfers the real value to the destination process. See the module documentation for what that means for control flow.

Attributes:

Name Type Description
party

The party this process is playing, as passed in me.

party_index

That party's position in parties.

addresses

The validated address map.

streams

One ObjectStream per remote party, valid inside the context.

Parameters:

Name Type Description Default
parties Iterable[Party]

All parties in the choreography, in an order every process agrees on. Names must be unique.

required
me Party

The party this process plays. Must appear in parties.

required
addresses Mapping[Party, tuple[str, int]]

Maps every party to the (host, port) its process listens on. Must cover exactly parties — no missing and no unknown parties. Each party binds its own entry, so the hosts must be addresses the corresponding process can bind and the others can reach.

required
connect_timeout float

Seconds to spend establishing the mesh before raising TimeoutError. This bounds the whole setup, so it doubles as the window in which every party's process must have started.

10.0

Raises:

Type Description
TypeError

If me is not a Party, or addresses is malformed.

ValueError

If me is not part of parties, or addresses does not cover exactly parties.

Example

Run this once per party, changing only me:

addresses = {
    alice: ("10.0.0.1", 10000),
    bob: ("10.0.0.2", 10000),
}

with pychor.TCPBackend(parties=[alice, bob], me=alice, addresses=addresses):
    x = 5 @ alice
    x.send(src=alice, dest=bob)

__enter__

__enter__()

Establish the party mesh, then become the active backend.

Raises:

Type Description
TimeoutError

If the mesh is not complete within connect_timeout.

__exit__

__exit__(exception_type, exception_value, traceback)

Close every connection, then stand down as the active backend.

constant

constant(party: Party, v: Any) -> LocatedVal

Create a located value, materialized only in its owning process.

Every process creates a located value with the same owner set, but only the process playing party stores v; the others store None.

Parameters:

Name Type Description Default
party Party

The owning party.

required
v Any

The value, which is ignored in processes other than party's.

required

Returns:

Type Description
LocatedVal

A located value owned by party.

Raises:

Type Description
ValueError

If party is not part of this backend.

send

send(party_from: Party, party_to: Party, lv: LocatedVal, note: Optional[str] = None) -> None

Send a located value from one party process to another.

This is a synchronization point between exactly two processes: the sender writes the value to its stream and the receiver blocks until it arrives. Every process — including uninvolved ones — adds party_to to the owner set, so all processes keep identical ownership information.

A send from a party to itself moves no data.

Parameters:

Name Type Description Default
party_from Party

The sending party, which must already own lv.

required
party_to Party

The receiving party.

required
lv LocatedVal

The located value to send. The receiving process fills in its val in place.

required
note Optional[str]

Accepted for interface compatibility with SimulationBackend; these backends draw no diagram, so it is unused.

None

locally

locally(f: Callable, *args: Any, **kwargs: Any) -> LocatedVal

Evaluate f in every process that can observe all located inputs.

The owner set of the result is computed identically in every process, but f itself runs only where the local party is one of those owners. Elsewhere the call is skipped and the result holds None.

Parameters:

Name Type Description Default
f Callable

An ordinary Python function.

required
*args Any

Located values, plain scalars, or containers of those.

()
**kwargs Any

Keyword arguments, handled the same way.

{}

Returns:

Type Description
LocatedVal

A located value owned by the parties that could see every located input.

Raises:

Type Description
AssertionError

If the located arguments have no owner in common.

unwrap

unwrap(lv: LocatedVal, parties: Set[Party]) -> Any

Return this process's raw value when it belongs to parties.

Returns:

Type Description
Any

The underlying value, or None if the requested parties are not all owners or if this process's party is not among them.

unlist

unlist(ls, length)

Split a located list, using None placeholders where unowned.

Non-owning processes have no list to split, so they produce length located values holding None — keeping the structure, and therefore the control flow, identical in every process.

untup

untup(ls, length)

Split a located tuple, using None placeholders where unowned.

undict

undict(d, keys)

Split a located dict, using None placeholders where unowned.

ForkingTCPBackend

Run all parties as forked local processes connected by TCP.

This is the testing counterpart to TCPBackend: it gives a choreography genuine one-party-per-process execution without any deployment, so it catches protocols that only work because SimulationBackend happens to hold every party's data in one place.

Entering the context forks one child per party after the first — the parent plays parties[0] — and each process then enters a TCPBackend for its own party. Parties are assigned deterministic localhost ports by their order in parties: the first party uses base_port, the second uses base_port + 1, and so on.

Two details are worth knowing. First, this class is a context-manager wrapper rather than a ChoreographyBackend subclass, and __enter__ returns the inner TCPBackend; so with ForkingTCPBackend(...) as b binds b to that backend, whose views contain only the local party's messages. Second, the code inside the with block runs once per party, in different processes — so its output is interleaved, and side effects such as writing files happen once per party.

On a clean exit each child flushes its output and exits immediately, while the parent waits for all children and raises RuntimeError if any of them failed. A child's traceback appears on stderr as usual.

Parameters:

Name Type Description Default
parties Iterable[Party]

All parties in the choreography. Order fixes the port assignment and decides which party the parent process plays.

required
host str

Address every party binds and connects to.

'127.0.0.1'
base_port int

Port of the first party; party i uses base_port + i. Concurrent runs need non-overlapping windows — the test suite gives each example its own 100-port range.

10000
connect_timeout float

Seconds to spend establishing the mesh before raising TimeoutError.

10.0

Raises:

Type Description
RuntimeError

On entry, if the platform has no os.fork; on exit, if a child process failed.

Example
with pychor.ForkingTCPBackend(parties=[alice, bob], base_port=10100):
    x = 5 @ alice
    x.send(src=alice, dest=bob)

__enter__

__enter__() -> TCPBackend

Fork one process per party and enter a TCPBackend in each.

Returns:

Type Description
TCPBackend

The inner TCPBackend for this process's party — not this wrapper.

Raises:

Type Description
RuntimeError

If the platform does not provide os.fork.

TimeoutError

If the party mesh cannot be established.

__exit__

__exit__(exception_type, exception_value, traceback)

Tear down this process's backend and reap the forked children.

In a child, this never returns on a clean exit: the child flushes its streams and exits so that it does not continue running the rest of the enclosing program. In the parent, it waits for every child and surfaces their failures.

Raises:

Type Description
RuntimeError

In the parent, if a child exited non-zero and no exception is already propagating.

Transport

The wire format used by the TCP backends. Choreographies do not use this directly; it is documented for anyone implementing a backend of their own or auditing what travels over the network.

ObjectStream

A bidirectional stream of pickled Python objects over one socket.

Each object is framed as a 4-byte big-endian unsigned length followed by that many bytes of pickle data. Writes are flushed immediately, so a put_obj on one side unblocks a get_obj on the other without further prompting.

The stream is not internally synchronized: a single stream should be used by one thread at a time. The TCP backends satisfy this by giving each pair of parties its own stream and doing all sends from the choreography's single thread of control.

Instances can be used as context managers, and close themselves when garbage collected.

Parameters:

Name Type Description Default
sock socket

A connected stream socket. The stream takes ownership of it and closes it in close.

required
Example
with ObjectStream(sock) as stream:
    stream.put_obj({"share": 42})
    reply = stream.get_obj()

get_obj

get_obj() -> Any

Read the next object from the stream, blocking until it arrives.

Returns:

Type Description
Any

The unpickled object, or None if the peer closed the connection before sending another frame. Note that a legitimately sent None is indistinguishable from end-of-stream.

put_obj

put_obj(obj: Any) -> None

Write one object to the stream and flush it.

Parameters:

Name Type Description Default
obj Any

Any picklable object. Objects of a class defined in the choreography are fine as long as both processes can import that class, which is automatic when every party runs the same program.

required

Raises:

Type Description
PicklingError

If obj cannot be pickled.

close

close() -> None

Close the reader, the writer, and the underlying socket.

Closing twice is harmless.