Go-style concurrency for Python. No async/await. No brokers. No boilerplate.
Project description
ryou
Go-style concurrency for Python. No async/await. No brokers. No boilerplate.
from ryou import ry, Chan, wait
@ry
def fetch(url: str) -> str:
...
h = ~fetch("https://example.com")
result, err = h.result()
if err:
raise err
print(result)
Install
pip install ryou
Requires Python 3.12+, Linux/macOS only.
Concepts
Five primitives:
@ry— turns any function into a ryoutine.~fn()spawns it concurrently.Ry[T]— handle to a running ryoutine..result()blocks until done, always returns(value, err).Chan[T]— typed channel.send/recvblock naturally, no busy loops.select— wait on multiple channels, pick whichever fires first.Context— cancellation and timeout propagation.
Powered by gevent greenlets under the hood. IO ryoutines are cooperative greenlets. CPU ryoutines are forked processes. You never interact with gevent directly.
Quick Start
from ryou import ry, wait
@ry
def fetch(url: str) -> str:
import urllib.request
return urllib.request.urlopen(url).read(100).decode()
# spawn concurrently
h1 = ~fetch("https://example.com")
h2 = ~fetch("https://httpbin.org/html")
wait(h1, h2)
result, err = h1.result()
if err:
print("failed:", err)
else:
print(result)
~fn() spawns and returns a Ry[T] handle immediately. .result() blocks until the ryoutine finishes.
Channels
from ryou import ry, Chan, wait
ch = Chan[str]()
@ry
def producer():
for i in range(5):
ch.send(f"message {i}")
ch.close() # signal done
@ry
def consumer():
for msg in ch: # exits when channel is closed
print("got:", msg)
wait(~producer(), ~consumer())
Chan[T] is a typed, blocking queue. send blocks if full, recv blocks until an item is available. Greenlets yield while waiting — no busy loops.
Bounded channel:
ch = Chan[str](maxsize=10) # blocks sender when full
Sending on a closed channel raises RuntimeError. Receiving from a closed empty channel returns CLOSED.
Select
Wait on multiple channels — pick whichever fires first:
from ryou import ry, Chan, select, wait, CLOSED
ch1 = Chan[str]()
ch2 = Chan[str]()
@ry
def handler():
active = {ch1, ch2}
while active:
ch, msg = select(*active)
match (ch, msg):
case (_, msg) if isinstance(msg, type(CLOSED)):
active.remove(ch)
case (c, v) if c is ch1:
print(f"ch1: {v}")
case (c, v) if c is ch2:
print(f"ch2: {v}")
select is non-deterministic when multiple channels are ready simultaneously — same as Go.
Context
Cancel a ryoutine from outside:
from ryou import ry, Context, wait
@ry
def worker(ctx=None):
for i in range(10):
if ctx and ctx.cancelled():
return
time.sleep(0.5)
print(f"tick {i}")
ctx = Context()
h = ~worker(ctx=ctx)
@ry
def canceller():
time.sleep(1.5)
ctx.cancel()
wait(~canceller(), h)
print(ctx.err()) # "cancelled"
Auto-cancel with timeout:
ctx = Context(timeout=1.5)
h = ~worker(ctx=ctx)
wait(h)
print(ctx.err()) # "timeout"
ctx.cancel() kills the ryoutine immediately. ctx.cancelled() lets the ryoutine check cooperatively.
CPU Bound Work
from ryou import ry, wait
@ry(cpu=True)
def crunch(n: int) -> int:
return sum(range(n))
h = ~crunch(10**8)
result, err = h.result()
print(result)
cpu=True runs the function in a forked child process — true parallelism, no GIL. IO and CPU ryoutines can run simultaneously.
Limitations
cpu=True uses os.fork() — the function and its arguments must be picklable:
- No lambdas
- No closures (functions defined inside other functions)
- No local class instances
# works
@ry(cpu=True)
def crunch(n: int) -> int:
return sum(range(n))
# breaks — lambda not picklable
fn = lambda x: x * x
executor(fn) # don't do this
# breaks — closure
def make_worker(multiplier):
@ry(cpu=True)
def worker(n):
return n * multiplier # captures multiplier — not picklable
return worker
Also Linux/macOS only — os.fork() is not available on Windows.
Error Handling
.result() never raises. It always returns (value, err):
h = ~fetch("https://bad-url")
result, err = h.result()
if err:
print("error:", err)
else:
print(result)
Wait
wait() # wait for all running ryoutines
wait(h1, h2) # wait for specific handles
API Reference
@ry
@ry
def fn() -> T: ... # IO bound, runs on greenlet
@ry(cpu=True)
def fn() -> T: ... # CPU bound, runs in forked process
Ry[T]
h = ~fn() # spawn
val, err = h.result() # block until done, (T | None, Exception | None)
Chan[T]
ch = Chan[T]() # unbounded
ch = Chan[T](maxsize=10) # bounded
ch.send(value) # blocks if full, raises RuntimeError if closed
value = ch.recv() # blocks until item available, returns CLOSED if closed + empty
ch.close() # close channel, wakes blocked receivers
ch.done() # True if closed
for msg in ch: ... # drain until closed, must be inside @ry
select
ch, msg = select(ch1, ch2) # blocks until one fires, returns (channel, value)
Context
ctx = Context() # no timeout
ctx = Context(timeout=5.0) # auto cancel after 5s
ctx.cancel() # forceful kill
ctx.cancelled() # cooperative check
ctx.err() # "cancelled" | "timeout" | None
wait
wait() # wait for all
wait(h1, h2) # wait for specific handles
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ryou-0.3.0.tar.gz.
File metadata
- Download URL: ryou-0.3.0.tar.gz
- Upload date:
- Size: 5.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e226b9070a01d5af9f24d4bc96bde76b2a48df68a56fb8f1fd17b1c817ecf481
|
|
| MD5 |
91d34dae032e5a08ea35a43b89777096
|
|
| BLAKE2b-256 |
810661b3a3f72dbdcfd2c99a91e66988875859fdea6439fb0d928c18c4720980
|
File details
Details for the file ryou-0.3.0-py3-none-any.whl.
File metadata
- Download URL: ryou-0.3.0-py3-none-any.whl
- Upload date:
- Size: 7.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d382247d4a73466255076f4865c40792f41f5845b561a8d3a3e5db6994d6a05e
|
|
| MD5 |
799999e9ddcf3131ae2847c87787f0e2
|
|
| BLAKE2b-256 |
0f05854526f6f5b3ed223f13de19d718e2c5188da60933b41db4ab2c677eee4e
|