microsoft/bocpy
Python
Captured source
source ↗microsoft/bocpy
Description: Behavior-Oriented Concurrency in Python
Language: Python
License: MIT
Stars: 154
Forks: 8
Open issues: 8
Created: 2026-02-09T10:36:46Z
Pushed: 2026-06-10T22:20:20Z
Default branch: main
Fork: no
Archived: no
README:
bocpy
Behavior-Oriented Concurrency (*BOC*) is a new paradigm for parallel and concurrent programming which is particularly well-suited to Python. In a BOC program, data is shared such that each behavior has unique temporal ownership of the data, removing the need for locks to coordinate access. For Python programmers, this brings a lot of benefits. Behaviors are implemented as decorated functions, and from the programmer's perspective, those functions work like normal. Importantly, the programmer's task shifts from solving concurrent data access problems to organizing data flow through functions. The resulting programs are easier to understand, easier to support, easier to extend, and unlock multi-core performance due to the ability to schedule behaviors to run efficiently across multiple sub-interpreters.
BOC has been implemented in several languages, including as a foundational aspect of the research language Verona, and now has been implemented in Python.
Getting Started
You can install bocpy via PyPi:
pip install bocpy
We provide pre-compiled wheels for Python 3.10 onwards on most platforms, but if you have problems with your particular platform/version combination, please file an issue on this repository.
> [!NOTE] > We provide wheels for Python 3.10 and newer, but bocpy only achieves true > parallelism on Python 3.12+, where each sub-interpreter has its own GIL. On > 3.10 and 3.11 behaviors still run, but they are serialised by the global GIL. > The library may not work on Python versions older than 3.10.
Python version support
gitGraph commit id: "3.10" commit id: "3.11" commit id: "3.12" tag: "true parallelism" commit id: "3.13" branch "free-threaded" commit id: "3.13t" checkout main commit id: "3.14" checkout "free-threaded" commit id: "3.14t" checkout main commit id: "3.15" checkout "free-threaded" commit id: "3.15t"
The mainline (main) branch in the diagram is the standard CPython build:
- 3.10 / 3.11 — wheels are published and
@whenworks, but every
sub-interpreter still shares one process-wide GIL, so behaviors execute one at a time. Use these versions for portability rather than performance.
- 3.12+ — each sub-interpreter gets its own GIL ([PEP 684][pep684]), so
worker behaviors run in parallel across cores. This is where bocpy delivers on its concurrency story.
- 3.14 is the current default development and CI target; 3.15 is
validated as it stabilises.
The free-threaded branch tracks the no-GIL CPython builds (informally "3.13t", "3.14t", "3.15t" — see [PEP 703][pep703]). bocpy runs unmodified on these interpreters today: we don't re-enable the GIL, and the cown / 2PL protocol gives the same data-race-free guarantees you get on the GIL build. The catch is overhead — on free-threaded Python, the sub-interpreter and XIData machinery is pure ceremony, since plain threads in the main interpreter would already run in parallel.
[Issue #5][issue5] tracks adding an alternative direct-threading backend that detects a free-threaded interpreter at runtime and skips the sub-interpreter / transpiler / XIData path entirely, while keeping the public Cown / @when API unchanged. We're holding off on that work until the free-threaded build and the relevant CPython APIs stabilise.
[pep684]: https://peps.python.org/pep-0684/ [pep703]: https://peps.python.org/pep-0703/ [issue5]: https://github.com/microsoft/bocpy/issues/5
Scaling with cores
The chart below shows BOC runtime throughput as the worker count grows from 1 to 8, plotted as speedup relative to a single worker. Numbers come from [examples/benchmark.py](examples/benchmark.py) — a chain-ring workload that exercises the scheduler, two-phase locking, sub-interpreter crossings and the message queue together — run on CPython 3.14 (mean of 3 repeats, 8 s each).
--- config: xyChart: width: 700 height: 360 --- xychart-beta title "bocpy speedup vs. worker count (chain-ring benchmark, CPython 3.14)" x-axis "Workers" [1, 2, 3, 4, 5, 6, 7, 8] y-axis "Speedup vs. 1 worker" 0 --> 9 line [1.00, 1.97, 2.94, 3.90, 4.87, 5.82, 6.75, 7.54]
Up to 8 workers, BOC delivers roughly linear scaling on this microbenchmark (≈7.5× at 8 workers). Real applications carry serial costs that this benchmark deliberately strips out — see the docstring at the top of [examples/benchmark.py](examples/benchmark.py) for the load-bearing caveats. To reproduce:
python examples/benchmark.py \ --sweep-axis workers --sweep-values 1,2,3,4,5,6,7,8 \ --duration 8 --warmup 2 --repeats 3 \ --output scaling.json
A behavior can be thought of as a function which depends on zero or more concurrently-owned data objects (which we call cowns). As a programmer, you indicate that you want the function to be called once all of those resources are available. For example, let's say that you had two complex and time-consuming operations, and you needed to act on the basis of both of their outcomes:
def buy_cheese():
logger = logging.getLogger("cheese_shop")
for name in all_known_cheeses():
if is_available(logger, name):
return name
cleanup_shop(logger)
return None
def order_meal(exclude: str):
logger = logging.getLogger("greasy_spoon")
for dish in menu():
logger.info(dish)
if exclude.lower() not in dish.lower():
logger.info(f"That doesn't have much {exclude} in it")
return dish
vikings(logger)
if random.random() ")
return None
cheese = buy_cheese()
meal = order_meal(exclude="spam")
if meal is not None:
eat(meal)
elif cheese is not None:
eat(cheese)
if meal is not None:
print("I really wanted some cheese...")
elif cheese is not None:
print("Cheesy comestibles")
return_to_library()The code above will work, but requires the purveying of cheese and the navigation of the menu for non-spam options to happen sequentially. If we wanted to do these tasks in parallel, we will end up with some version of nested waiting, which can result in deadlock. With BOC, we would write the above like this:
from bocpy import wait, when, Cown # ... def buy_cheese(): cheese = Cown(None)…
Excerpt shown — open the source for the full document.
Notability
notability 3.0/10Routine new repo, low stars.