Python
Async (asyncio) bindings generated by UniFFI's built-in Python backend and packaged by
maturin. Client and broker are separate wheels. Read the
shared concepts first for the option records and semantics.
Install
pip install stem-mqtt-client stem-mqtt-broker # either one on its own works too
The distribution names are stem-mqtt-client / stem-mqtt-broker (mqtt-client is taken on PyPI),
the import names are not:
import mqtt_client
import mqtt_broker
Requirements
- Python 3.9+ and
asyncio. The wheels arepy3-none-<platform>(the library is loaded withctypes), so there is one wheel per OS/arch, not per Python version. - Wheels bundle the native library: Linux (manylinux_2_28, x86_64) from the Gitea release; the GitHub release adds macOS and Windows. Other platforms build from source (below).
Quick start
import asyncio
import mqtt_broker
import mqtt_client
def options(port, client_id, username=None):
return mqtt_client.ConnectOptions(
host="127.0.0.1", port=port, client_id=client_id,
version=mqtt_client.MqttVersion.V5, clean_start=True, keep_alive_secs=30,
username=username, password=None, will=None,
connect_timeout_secs=10, operation_timeout_secs=5,
auto_reconnect=False, reconnect_backoff_secs=0, reconnect_max_backoff_secs=0, tls=None,
)
class Inbox(mqtt_client.MqttMessageListener):
def __init__(self):
self.queue = asyncio.Queue()
self.loop = asyncio.get_running_loop()
def on_message(self, message): # called from a Rust thread
self.loop.call_soon_threadsafe(self.queue.put_nowait, message)
def on_disconnected(self, reason):
print("disconnected:", reason)
async def main():
broker = mqtt_broker.MqttBroker(mqtt_broker.MqttBrokerConfig(
bind_address="127.0.0.1", port=0, ws_port=None, allow_anonymous=True, max_clients=0,
max_qos=mqtt_broker.QoS.EXACTLY_ONCE, max_retained_messages=100,
max_queued_per_client=100, redelivery_interval_secs=0, tls=None,
))
await broker.start()
port = broker.bound_port()
inbox = Inbox()
sub = mqtt_client.MqttClient(options(port, "py-sub"))
sub.set_message_listener(inbox)
await sub.connect()
await sub.subscribe("demo/#", mqtt_client.QoS.AT_LEAST_ONCE)
pub = mqtt_client.MqttClient(options(port, "py-pub"))
await pub.connect()
await pub.publish("demo/hello", b"hello", mqtt_client.QoS.AT_LEAST_ONCE, False)
message = await asyncio.wait_for(inbox.queue.get(), 5)
print(message.topic, bytes(message.payload))
await pub.disconnect(); await sub.disconnect(); await broker.stop()
asyncio.run(main())
max_packet_size, max_outbound_queue, session_expiry_secs, auth_method and auth_data have
defaults and can be omitted, as above.
Client
- Every I/O method is a coroutine:
await client.connect(),subscribe(filter, qos),unsubscribe(filter),publish(topic, payload, qos, retain),disconnect().is_connected(),set_message_listener(),enable_message_queue()andlast_disconnect_reason()are plain calls.payloadisbytes; receivedmessage.payloadisbytes-like — wrap it inbytes(...). - Enums are
UPPER_SNAKE:mqtt_client.QoS.AT_MOST_ONCE | AT_LEAST_ONCE | EXACTLY_ONCE,mqtt_client.MqttVersion.V311 | V5. - Listener callbacks run on a Rust thread, not on the event loop. Hand work back with
loop.call_soon_threadsafe(...)as above (or athreading.Event). Keep them short: an exception raised in a callback is caught and logged so it cannot take down the I/O task, but it is not propagated to you. - Or pull:
client.enable_message_queue(16)thenawait client.next_message(5000)returns a message orNone— no threads involved.
polled = mqtt_client.MqttClient(options(port, "py-polled"))
polled.enable_message_queue(16)
await polled.connect()
await polled.subscribe("demo/#", mqtt_client.QoS.AT_LEAST_ONCE)
msg = await polled.next_message(5000) # None on timeout
Broker
mqtt_broker.MqttBroker(config) — await start(), await stop(), is_running(), bound_port(),
bound_ws_port(), bound_tls_port(), client_count(). Use port 0 and read bound_port() for an
ephemeral port. The broker owns its own mqtt_broker.QoS; don't pass a mqtt_client.QoS to it.
class Events(mqtt_broker.MqttBrokerEventListener):
def on_client_connected(self, client_id): print("+", client_id)
def on_client_disconnected(self, client_id, reason): print("-", client_id, reason)
def on_message_published(self, client_id, topic, qos): print(client_id, "->", topic)
broker.set_event_listener(Events())
Or poll: broker.enable_event_queue(64) then await broker.next_event(1000) returns a
BrokerEvent (ClientConnected, ClientDisconnected, MessagePublished) or None.
Authentication
class Auth(mqtt_broker.MqttAuthProvider):
def authenticate(self, client_id, username, password): # password: bytes | None
return username == "demo" and password == b"demo"
broker.set_auth_provider(Auth())
A refused client raises from await client.connect() (reason 0x86). MQTT 5 enhanced authentication:
class AddOne(mqtt_broker.MqttEnhancedAuthProvider):
def step(self, client_id, method, data, round):
if round == 0:
return mqtt_broker.EnhancedAuthStep(outcome=mqtt_broker.EnhancedAuthOutcome.CONTINUE, data=b"\x07")
ok = data is not None and bytes(data) == b"\x08"
return mqtt_broker.EnhancedAuthStep(
outcome=mqtt_broker.EnhancedAuthOutcome.SUCCESS if ok else mqtt_broker.EnhancedAuthOutcome.FAILURE,
data=b"welcome" if ok else b"")
class AddOneHandler(mqtt_client.MqttAuthHandler):
def respond(self, method, challenge):
return bytes(b + 1 for b in challenge)
broker.set_enhanced_auth_provider(AddOne())
client = mqtt_client.MqttClient(options(port, "py-eauth")) # with auth_method="X-ADD-ONE", auth_data=b"hello"
client.set_auth_handler(AddOneHandler())
TLS
ca = open("ca.pem", "rb").read()
tls = mqtt_client.TlsOptions(ca_cert_pem=ca, client_cert_pem=None, client_key_pem=None,
insecure_skip_certificate_verification=False)
# pass tls=tls in ConnectOptions
broker_tls = mqtt_broker.BrokerTlsConfig(port=8883, cert_pem=open("server.pem", "rb").read(),
key_pem=open("server.key", "rb").read(), client_ca_pem=None)
# pass tls=broker_tls in MqttBrokerConfig
Language notes
- Optional values are
None; byte values arebytes. Errors are exceptions (mqtt_client.MqttError.*,mqtt_broker.MqttBrokerError.*); catchExceptionfor "refused". - A broker started in a script keeps running until
await broker.stop(); closing the loop without stopping leaves the listener threads alive until the process exits.
Building from source
pip install maturin
cd crates/mqtt-client && maturin build --release # wheel in target/wheels/
cd ../mqtt-broker && maturin build --release
pip install ../../target/wheels/*.whl
maturin runs uniffi-bindgen itself; there is no separate generation step. More:
packaging/python/README.md. The end-to-end test is
tests/bindings/python/smoke.py.
Troubleshooting
ImportError: cannot import name 'mqtt_client'— you installed the broker wheel only (or vice versa); install both, or import just the component you have.ModuleNotFoundErrorafterpip install mqtt-client— wrong package; the name isstem-mqtt-client.- A callback never fires — it is invoked from another thread; make sure the loop you hand work to is still running and that the listener object is kept referenced (don't let it be garbage collected).
no matching distribution— no wheel for your platform yet; build from source.
Metadata
Release files for stem-mqtt-client 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| stem_mqtt_client-0.4.2-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details for stem_mqtt_client-0.4.2-py3-none-win_amd64.whl |
| stem_mqtt_client-0.4.2-py3-none-manylinux_2_34_x86_64.whl | Python 3 | none | Linux glibc 2.34+ x86-64 | Details for stem_mqtt_client-0.4.2-py3-none-manylinux_2_34_x86_64.whl |
| stem_mqtt_client-0.4.2-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details for stem_mqtt_client-0.4.2-py3-none-macosx_11_0_arm64.whl |
Total release size: 3.7 MB
Release files / stem_mqtt_client-0.4.2-py3-none-win_amd64.whl
| Download URL | stem_mqtt_client-0.4.2-py3-none-win_amd64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
b563a22a6dbbd98a2e991d9244fe7875d5aa9e040bfce63e7d9ec8512309d68d
|
|
BLAKE2b-256 checksum How to use checksums |
0419c0eed7cc6e42ce983f4ee7639233f629e704c814fe00ad547ddb610bfb74
|
| 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 6, 2026.
Transparency logRelease files / stem_mqtt_client-0.4.2-py3-none-manylinux_2_34_x86_64.whl
| Download URL | stem_mqtt_client-0.4.2-py3-none-manylinux_2_34_x86_64.whl |
|---|---|
| Size | 1.3 MB |
| Tags | Linux glibc 2.34+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
147ed0239966f29c1d99737736a17be93fb4eb649666e2f2a3ad1bc7170e8993
|
|
BLAKE2b-256 checksum How to use checksums |
69d9df17e9a7bc21baf5fab5e58a0e1b19f72475b0933cf9847d6bde53884745
|
| 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 6, 2026.
Transparency logRelease files / stem_mqtt_client-0.4.2-py3-none-macosx_11_0_arm64.whl
| Download URL | stem_mqtt_client-0.4.2-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
f3909c1838f57fe2c3a06412488d0ea8b89a11dd7777a0b0e9c8a036ef713b6d
|
|
BLAKE2b-256 checksum How to use checksums |
2c99373dc96190798c0d2eae0debfe1880cf65abac887bd724d208a3a51bba78
|
| 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 6, 2026.
Transparency logSecurity for stem-mqtt-client 0.4.2
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 3 files in this release have been uploaded with provenance metadata.
Attestation for stem_mqtt_client-0.4.2-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 6, 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.