The paho mqtt 2.0 callback API version change is the single most common reason Python MQTT scripts suddenly stop working after a pip install --upgrade. This guide explains exactly what changed in paho-mqtt 2.0, gives you the one-line fix for the error, and then walks through migrating every callback to the recommended VERSION2 signatures. You will finish with a complete, working example connected to broker.connectmq.com that you can use as a template.
The Errors You Are Probably Seeing
If you landed here from a search engine, you most likely have one of these:
ValueError: Unsupported callback API version: version 2.0 added a callback_api_version, see docs/migrations.rst for details
TypeError: Client.__init__() missing 1 required positional argument: 'callback_api_version'
DeprecationWarning: Callback API version 1 is deprecated, update to latest version
TypeError: on_connect() takes 4 positional arguments but 5 were given
The first two come from creating the client the 1.x way. The ValueError appears when you pass a client ID as the first positional argument, as in mqtt.Client("my-device"), because that position now belongs to the callback API version. The third is a warning that you chose the old API explicitly. The fourth appears after you switch to VERSION2 but leave a callback with the old number of parameters.
What Changed in paho-mqtt 2.0
Version 2.0 of the Eclipse Paho Python client, released in early 2024, was the first major release in years. Its headline change is that callbacks are now versioned. In 1.x the parameters passed to callbacks differed between MQTT 3.1.1 and MQTT 5, and some used plain integers while others used reason code objects. Version 2 unifies them: every callback receives the same parameters regardless of protocol version, and result codes are always ReasonCode objects.
To avoid silently breaking code with new signatures, the maintainers made you choose explicitly. The first argument of Client() is now callback_api_version, with two values:
| Value | Behaviour | Recommended? |
|---|---|---|
CallbackAPIVersion.VERSION1 |
Old 1.x callback signatures, emits a deprecation warning | Only as a temporary bridge |
CallbackAPIVersion.VERSION2 |
New unified signatures, ReasonCode objects everywhere |
Yes, for all new and migrated code |
Other notable changes in 2.0 include dropping support for Python 2.7 and very old Python 3 releases, and renaming the ReasonCodes class to ReasonCode. The full list lives in the official paho-mqtt migration notes.
Fix 1: The One-Line Bridge (VERSION1)
If production is down and you need it back now, tell paho to keep the old behaviour:
import paho.mqtt.client as mqtt
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, client_id="my-device")
All your existing callbacks keep working unchanged. You will see the deprecation warning, which is a reminder that VERSION1 will be removed in a future major release. Treat this as a bandage, not a fix.
An alternative bandage is pinning the old library with pip install "paho-mqtt<2.0". That works, but it blocks security and bug fixes and tends to conflict with other packages that already require 2.x. Prefer the VERSION1 flag over pinning, and plan the real migration.
Fix 2: Migrate to the Paho MQTT 2.0 Callback API Version 2
The real fix is to pass VERSION2 and update callback signatures. Here is every callback side by side:
| Callback | 1.x / VERSION1 signature | VERSION2 signature |
|---|---|---|
on_connect |
(client, userdata, flags, rc) |
(client, userdata, flags, reason_code, properties) |
on_disconnect |
(client, userdata, rc) |
(client, userdata, disconnect_flags, reason_code, properties) |
on_subscribe |
(client, userdata, mid, granted_qos) |
(client, userdata, mid, reason_code_list, properties) |
on_unsubscribe |
(client, userdata, mid) |
(client, userdata, mid, reason_code_list, properties) |
on_publish |
(client, userdata, mid) |
(client, userdata, mid, reason_code, properties) |
on_message |
(client, userdata, message) |
unchanged |
on_log |
(client, userdata, level, buf) |
unchanged |
Under VERSION1 with MQTT 5, some of these already had extra parameters; VERSION2 removes that difference, which is the whole point of the change.
Migration checklist
Work through your code in this order:
- Change every
mqtt.Client(...)call tomqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=..., ...), passing all other arguments by keyword. - Add
propertiestoon_connectand renamerctoreason_code. Replacerc == 0withnot reason_code.is_failure(comparison with integers still works, but the attribute reads better). - Replace
flags["session present"]withflags.session_present. - Give
on_disconnectits newdisconnect_flagsparameter andproperties. - In
on_subscribe, replacegranted_qoswithreason_code_listand check each entry: success codes have values 0 to 2 (the granted QoS), failures haveis_failureset. - Add
reason_codeandpropertiestoon_publishandon_unsubscribe. - Search the code base for
ReasonCodesand change it toReasonCode. - Run your tests with warnings turned into errors (
python -W error::DeprecationWarning) to prove no VERSION1 client is left.
A Complete VERSION2 Example
This script uses every common callback with the new signatures. Change k7f3q2 to your own random string so you do not share topics with other users of the public MQTT test broker:
import time
import uuid
import paho.mqtt.client as mqtt
BROKER = "broker.connectmq.com"
PREFIX = "connectmq/demo/k7f3q2"
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"Connect failed: {reason_code}")
return
print(f"Connected (session present: {flags.session_present})")
client.subscribe(f"{PREFIX}/#", qos=1)
def on_subscribe(client, userdata, mid, reason_code_list, properties):
for rc in reason_code_list:
print("Subscribe rejected:" if rc.is_failure else "Granted QoS:", rc)
def on_publish(client, userdata, mid, reason_code, properties):
print(f"Publish {mid} acknowledged: {reason_code}")
def on_message(client, userdata, msg):
print(f"{msg.topic}: {msg.payload.decode()}")
def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
print(f"Disconnected: {reason_code}")
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id=f"migrate-{uuid.uuid4().hex[:8]}",
protocol=mqtt.MQTTv5,
)
client.on_connect = on_connect
client.on_subscribe = on_subscribe
client.on_publish = on_publish
client.on_message = on_message
client.on_disconnect = on_disconnect
client.connect(BROKER, 1883, keepalive=60)
client.loop_start()
time.sleep(1)
for i in range(3):
client.publish(f"{PREFIX}/test", f"hello {i}", qos=1)
time.sleep(1)
client.disconnect()
client.loop_stop()
Run it and you should see the connect, the granted QoS, three publish acknowledgments and the three messages echoed back by the ConnectMQ public broker. Switch protocol to mqtt.MQTTv311 and the same callbacks still work without edits, which is exactly what VERSION2 promises. If you are new to the library itself, start with the paho mqtt python tutorial first.
Pitfalls After Migrating
Third-party libraries pinned to 1.x. Some packages that wrap paho declared paho-mqtt<2.0 in their requirements. Check with pip show and pip check. Recent releases of most maintained wrappers support 2.x; upgrade them, or isolate legacy tools in their own virtual environment.
Positional arguments. Any mqtt.Client("id", False, None, mqtt.MQTTv311) call is now shifted by one. Convert all of them to keyword arguments; it makes future upgrades painless.
Callbacks registered with decorators. Code using @client.connect_callback() and similar decorators must update the decorated function signatures too.
Swallowed exceptions. By default paho logs exceptions raised inside callbacks instead of crashing your program. A wrong signature can therefore look like "nothing happens". Attach a logger with client.enable_logger() while migrating so the TypeError is visible.
Reconnect logic. If you compared rc integers in on_disconnect to decide whether to reconnect, rewrite it around reason_code. The paho-mqtt reconnect guide shows robust patterns, and the Python MQTT TLS guide covers the encrypted connection you should use in production.
Should You Consider aiomqtt Instead?
If your application is built on asyncio, migrating callbacks may be a good moment to move to an async API altogether. aiomqtt wraps paho and replaces callbacks with async for message in client.messages. Our aiomqtt and asyncio tutorial shows the same publish/subscribe flow written that way. For a broader comparison of Python and other language clients, see the MQTT client libraries overview.
Test Your Migrated Code on ConnectMQ
ConnectMQ runs a free public broker at broker.connectmq.com that is ideal for verifying a migration, and a fully managed MQTT service for production with MQTT 3.1.1 and 5.0, TLS on 8883 and generated credentials. Its portal includes a live topic explorer so you can confirm migrated clients still publish what you expect. When you are ready to move off the public broker, run your clients on a private cloud MQTT broker: sign up for the free Serverless plan with no credit card.
Frequently Asked Questions
What is callback_api_version in paho-mqtt?
It is the first argument of mqtt.Client() since paho-mqtt 2.0. It selects which callback signatures the client uses: VERSION1 for the old 1.x style or VERSION2 for the new unified style.
Should I use VERSION1 or VERSION2?
Use VERSION2. VERSION1 exists only to keep old code running and prints a deprecation warning, so it will eventually be removed.
Why do I get "Unsupported callback API version" when passing a client ID?
You passed the client ID as the first positional argument, which is now reserved for the callback API version. Pass mqtt.CallbackAPIVersion.VERSION2 first and the ID as client_id=....
Do I need to change on_message when migrating?
No. on_message(client, userdata, message) keeps the same signature in both versions. The changes affect connect, disconnect, subscribe, unsubscribe and publish callbacks.



