This release is a pre-release and may not be stable for production use.
librats — Python bindings
Python bindings for librats. They call the library's C ABI
(src/librats/bindings/rats.h) through ctypes, so there is nothing to compile
on the Python side — only the librats shared library itself.
The model
A RatsNode is one librats node. On its own it gives you an encrypted
connection to any peer you dial (Noise XX, over UDP or TCP on the same port), a
peer id derived from the node's key, and raw messages on named channels.
Discovery, NAT traversal, pub/sub, JSON messages, file transfer, ping and
reconnection are subsystems you switch on one by one.
Three rules follow:
- Enable subsystems and register callbacks before
start(). Enabling after start raisesRatsAlreadyStartedError; registering a peer or channel callback after start does too. Using a subsystem you did not enable raisesRatsNotEnabledError. - Callbacks run on a librats thread. Keep them short. An exception raised in one is printed to stderr; it never reaches the library.
- Errors are exceptions. Each
rats_error_tcode has its ownRatsErrorsubclass (table below). Peer ids are 64-character lowercase hex strings.
Installing
Python 3.8 or newer, and the librats shared library:
cd python
python build.py --build-native # CMake, -DRATS_SHARED_LIBRARY=ON, into build/python,
# then copies the library next to the package
pip install -e .
The loader tries, in order: the directories in LIBRATS_LIBRARY_PATH, the
package directory, build/lib, build/bin and build/ of the repository, and
finally the system loader by bare name. The file is rats.dll or librats.dll
on Windows, librats.dylib on macOS, librats.so elsewhere. A MinGW build of
the DLL also needs libstdc++-6.dll, libgcc_s_seh-1.dll and
libwinpthread-1.dll beside it: Python does not search PATH for a DLL's
dependencies.
Quick start
from librats_py import RatsNode
with RatsNode(8080, protocol="myapp/1.0", data_dir="./state") as node:
node.on_peer_connected(lambda peer: print("+", peer))
node.on_peer_disconnected(lambda peer, why: print("-", peer, why.name))
node.on("chat", lambda peer, data: print(peer[:8], data.decode()))
node.enable_mdns() # find peers on the local network
node.start()
print("id", node.local_id, "port", node.listen_port)
node.connect("192.168.1.100", 8081) # or wait for discovery
node.broadcast("chat", b"hello")
input("Enter to quit\n")
with destroys the node on exit. Without it, call destroy() (or let the
garbage collector do it).
Configuration
Every keyword of RatsNode(...) maps to a field of rats_config_t; leave one
out and the library default applies.
| Keyword | Default | Meaning |
|---|---|---|
listen_port |
0 | port both transports listen on; 0 picks a free one |
protocol |
"librats/1.0" |
app id bound into the handshake; peers with a different one cannot connect |
data_dir |
none | keeps the identity (and DHT table, reconnect list) across runs; without it the id is new every run |
security |
Security.NOISE |
Security.PLAINTEXT turns encryption off |
bind_address |
"::" |
interface to listen on (dual-stack by default) |
dial_only |
False |
no listener at all |
max_peers |
0 | cap on established peers, enforced on inbound; 0 = none. Also a settable property |
transports |
both | TransportMask.TCP, TransportMask.UDP, or both |
preferred_transport |
Transport.UDP |
which one a dial tries first |
transport_fallback_ms |
1200 | how long the first transport dials alone before the other joins; FALLBACK_NEVER disables it |
send_queue_limit |
8 MiB | bytes a peer's queue may hold before a sender that keeps going loses the peer (see Backpressure) |
path_sample_ms |
off | how often path_stats() is refreshed; an on_peer_path callback turns it on at 100 ms |
congestion_control |
Congestion.BBR |
Congestion.RENO for a loss-based controller (UDP only) |
udp_receive_window |
16 MiB | ceiling of one UDP connection's flow-control window |
udp_receive_budget |
64 MiB | out-of-order bytes all UDP connections together may hold |
udp_offload |
True |
kernel UDP segmentation offload where available |
udp_idle_timeout_ms |
45 s | silence after which a UDP peer is dropped as lost (CloseReason.IDLE_TIMEOUT) |
reactor_threads |
1 | I/O threads |
network_monitor |
True |
watch the host for interface and address changes |
An impossible combination — no transport, or a preferred transport you turned
off — raises RatsInvalidArgError from the constructor.
Messages
node.send(peer_id, "chat", b"hi") # one peer
node.broadcast("chat", b"hi everyone") # every connected peer
node.send(peer_id, "video", frame, lifetime_ms=150)
A send returns once the message is queued; it does not wait for delivery.
Without lifetime_ms a message is reliable and arrives in order with
everything else the node sends on its channels. With lifetime_ms it is a
live message: it is worth delivering only within that many milliseconds, is
repaired while it can still make it, and is dropped whole once it cannot. Live
messages are never delivered partly or twice and are not ordered, so number them
if order matters. They are meant for audio and video frames; the limits are
LIVE_MAX_LIFETIME_MS (30 s) and LIVE_MAX_PAYLOAD (4 MiB). The receiving
on() handler is the same for both kinds.
Backpressure
send() accepts a message of any size, but a peer whose queue is already past
send_queue_limit when you send more is dropped with
CloseReason.SLOW_CONSUMER. To avoid that when sending in bulk:
node.send(peer_id, "bulk", chunk)
if not node.peer_writable(peer_id):
# stop here; resume when on_peer_writable(peer_id) fires
...
Path statistics
For a sender that sizes its output to the link (a live encoder),
path_stats(peer_id) returns a PathStats: the congestion controller's
bandwidth_bps, srtt_us, queued_bytes, loss, and counts of live messages
sent and expired. Over TCP most figures come from the kernel and exist only on
Linux. on_peer_path(cb) delivers the same samples periodically.
Peers
node.peer_count, node.peer_ids
info = node.peer_info(peer_id) # None if not connected
info.transport # Transport.UDP / TCP / RELAY
info.direction # Direction.OUTBOUND / INBOUND
info.address # "203.0.113.5:8080", "" until known
info.transports # what the peer says it accepts
node.disconnect(peer_id)
node.on_dial_failed(lambda address: print("unreachable", address))
on_peer_disconnected passes a CloseReason. A peer that left can be redialed;
SLOW_CONSUMER means this node sent faster than the link drained, and redialing
only repeats that.
Subsystems
All of these are enabled before start().
| Subsystem | Enable | Then |
|---|---|---|
| DHT discovery | enable_dht(dht_port=0, discovery_key=None) |
peers found are dialed automatically; dht_set_keys([...]) changes the keys, also while running |
| mDNS discovery | enable_mdns() |
— |
| Peer exchange | enable_peer_exchange() |
peers share the addresses of peers they hold |
| Port mapping | enable_port_mapping(upnp=True, natpmp=True) |
— |
| Hole punching | enable_hole_punch(serve_rendezvous=True) |
punch_peer(peer_id), nat_mapping, observed_addresses |
| Relaying | enable_relay(serve=False) |
connect_via_relay(peer_id) |
| Pub/sub | enable_pubsub() |
subscribe(topic, cb), unsubscribe(topic), publish(topic, data) |
| JSON messages | enable_json() |
on_json, once_json, off_json, send_json, broadcast_json |
| File transfer | enable_file_transfer(temp_dir=None) |
see below |
| Ping | enable_ping() |
peer_rtt_ms(peer_id) — None until measured |
| Reconnection | enable_reconnect() |
add_reconnect(host, port), remove_reconnect(host, port) |
| Reconnection, lost peers too | enable_reconnect_lost(lost_for_ms) |
as above; a peer lost (not one that left) is redialed where it was last seen for lost_for_ms |
| BitTorrent | enable_bittorrent(...) |
bt_add_magnet, bt_add_torrent_file, bt_remove_torrent — only in libraries built with RATS_SEARCH_FEATURES |
NAT traversal
node.enable_port_mapping() # ask the router first
node.enable_hole_punch() # punch through a peer both sides share
node.enable_relay() # last resort: route through such a peer
node.start()
...
node.punch_peer(peer_id) # success arrives as on_peer_connected
Hole punching needs every node involved to have it enabled, and some node in
the mesh to pass rendezvous messages along (serve_rendezvous=True, the
default). It cannot work from behind a symmetric NAT
(node.nat_mapping == NatMapping.ENDPOINT_DEPENDENT); a relay can. A relayed
peer stays encrypted end to end and peer_info() reports Transport.RELAY.
With both enabled, a punch that cannot work falls back to a relay, and a relayed
peer is moved to a direct connection when a later punch succeeds.
enable_relay(serve=True) also carries other peers' connections, which costs
this node their bandwidth; nobody does it unless asked.
punch_peer and connect_via_relay raise RatsDeclinedError when there is
nothing to do or nothing to try with (already connected, already trying, no peer
to help).
JSON messages
node.enable_json()
node.on_json("greeting", lambda peer, value: print(value["hi"]))
node.start()
node.broadcast_json("greeting", {"hi": "there"})
Values are encoded with json.dumps and handed to handlers parsed. on_json
adds a handler and may be called while running; once_json removes it after
its first message.
File transfer
The sender offers, the receiver accepts or rejects.
node.enable_file_transfer(temp_dir="./partial")
node.on_file_offer(lambda peer, tid, name, size, is_dir:
node.accept_file(peer, tid, f"./downloads/{name}"))
node.on_file_progress(lambda p: print(p.transfer_id, p.bytes_transferred, p.total_bytes,
p.status.name))
node.on_file_complete(lambda peer, tid, ok, path: print(tid, ok, path))
node.start()
tid = node.send_file(peer_id, "report.pdf") # or send_directory()
node.pause_file(peer_id, tid); node.resume_file(peer_id, tid); node.cancel_file(peer_id, tid)
A transfer is named by the pair (peer, transfer id). The offer, progress and
complete callbacks are one each; registering again replaces the previous one.
on_file_progress receives a FileProgress with direction, status, byte and
file counts and a smoothed rate.
Module functions
set_log_level(LogLevel), set_log_file(path or None), version(),
version_info(), git_describe(), abi(), error_str(code).
Errors
| Exception | ErrorCode |
Raised when |
|---|---|---|
RatsInvalidArgError |
INVALID_ARG |
malformed peer id, bad JSON, impossible config |
RatsNotStartedError |
NOT_STARTED |
connect() before start() |
RatsAlreadyStartedError |
ALREADY_STARTED |
enabling or registering a callback after start() |
RatsNotEnabledError |
NOT_ENABLED |
using a subsystem that was not enabled |
RatsNoSuchPeerError |
NO_SUCH_PEER |
the peer is not connected |
RatsNotFoundError |
NOT_FOUND |
no such file transfer, or a file to offer that cannot be read |
RatsDeclinedError |
DECLINED |
a punch or relay attempt has nothing to do |
RatsBindError |
BIND |
start() could not open its sockets |
All derive from RatsError, whose error_code holds the code.
Examples
python -m librats_py.examples.basic_client 8080
python -m librats_py.examples.file_transfer 8080 ./downloads
python -m librats_py.examples.gossipsub_chat 8080 alice general
Tests
pip install pytest
python -m pytest librats_py/tests
Tests that need the shared library are marked native and skipped when it
cannot be loaded.
License
MIT — see LICENSE.
Metadata
Release files for librats 3.0.5.dev2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| librats-3.0.5.dev2-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details for librats-3.0.5.dev2-py3-none-win_amd64.whl |
| librats-3.0.5.dev2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.27+ x86-64, Linux glibc 2.28+ x86-64 | Details for librats-3.0.5.dev2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
| librats-3.0.5.dev2-py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.26+ ARM64, Linux glibc 2.28+ ARM64 | Details for librats-3.0.5.dev2-py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl |
| librats-3.0.5.dev2-py3-none-macosx_11_0_universal2.whl | Python 3 | none | macOS 11.0+ universal2 (ARM64, x86-64) | Details for librats-3.0.5.dev2-py3-none-macosx_11_0_universal2.whl |
Total release size: 5.6 MB
Release files / librats-3.0.5.dev2-py3-none-win_amd64.whl
| Download URL | librats-3.0.5.dev2-py3-none-win_amd64.whl |
|---|---|
| Size | 1.0 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
18c5cf3d183eeec979b8cd37be40ea72e582ddf3e863a64cbda0df8d1548593a
|
|
BLAKE2b-256 checksum How to use checksums |
5a44b59559b4a05d53b1ee1fafac76bd840f7093d5338ae34a7a1c7859be040f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logRelease files / librats-3.0.5.dev2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
| Download URL | librats-3.0.5.dev2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 1.5 MB |
| Tags | Linux glibc 2.27+ x86-64 Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
0bf9171141e671bed89c4515526be8a5ba9b49bbd84eec94bf479b62e025e2fc
|
|
BLAKE2b-256 checksum How to use checksums |
7b5a915fa254a3efa811fd45e642b06d6614c73c3f53a1f8c3604eff6a61cadb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logRelease files / librats-3.0.5.dev2-py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
| Download URL | librats-3.0.5.dev2-py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 1.3 MB |
| Tags | Linux glibc 2.26+ ARM64 Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
d475fe22c53b689175c14fbb8cea0922816b5c2a96cdb8ee75adf422adba90a2
|
|
BLAKE2b-256 checksum How to use checksums |
4ed840efbecc36c473ac9c0cac55d35a69eaa5d8ec8bd03e1e6b05a61cfb507f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logRelease files / librats-3.0.5.dev2-py3-none-macosx_11_0_universal2.whl
| Download URL | librats-3.0.5.dev2-py3-none-macosx_11_0_universal2.whl |
|---|---|
| Size | 1.8 MB |
| Tags | Python 3 macOS 11.0+ universal2 (ARM64, x86-64) |
|
SHA-256 checksum How to use checksums |
4e329df4f063e869e51c91f742b4736331e2e31cb236f630f2e161bd52bb0b20
|
|
BLAKE2b-256 checksum How to use checksums |
5c15fcc33e1a314cbf82b70fedb2902d74615286677bb1563c0c353c7b8e2079
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logSecurity for librats 3.0.5.dev2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 4 files in this release have been uploaded with provenance metadata.
Attestation for librats-3.0.5.dev2-py3-none-win_amd64.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.