paho-mqtt 2.0 changed how you create a client and what your callbacks receive. If your script broke after a pip install --upgrade, the fix is almost always the same: pass mqtt.CallbackAPIVersion.VERSION2 as the first argument to mqtt.Client() and update the callback signatures. This guide shows the exact errors, what changed, and a complete before/after example.
The errors you are probably seeing
| Error | Cause | Fix |
|---|---|---|
ValueError: Unsupported callback API version: version 2.0 added a callback_api_version, see docs/migrations.rst for details | The client ID was passed as the first positional argument: mqtt.Client("my-id") | mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="my-id") |
DeprecationWarning: Callback API version 1 is deprecated, update to latest version | mqtt.Client() without a version, or with VERSION1 | Pass VERSION2 and update callbacks |
TypeError: on_connect() takes 4 positional arguments but 5 were given | VERSION2 with a 1.x-style on_connect(client, userdata, flags, rc) | Add the properties parameter (see below) |
TypeError: on_disconnect() takes 3 positional arguments but 5 were given | Same for on_disconnect(client, userdata, rc) | New signature with disconnect_flags, reason_code, properties |
AttributeError: module 'paho.mqtt.client' has no attribute 'CallbackAPIVersion' | Your code is for 2.x but the environment still has 1.x | pip install "paho-mqtt>=2.0" and check which interpreter runs the script |
Check the installed version first — virtual environments and system Python often disagree:
python -c "import paho.mqtt; print(paho.mqtt.__version__)"
pip show paho-mqttWhat changed in 2.0
Version 1.x had a single callback style whose arguments depended on the protocol version: MQTT 3.1.1 callbacks received an integer rc, MQTT 5 callbacks received reason_code and properties. That made it easy to write code that worked on one protocol and crashed on the other.
2.0 introduces callback API versions. With CallbackAPIVersion.VERSION2, every callback has the same signature on MQTT 3.1.1 and MQTT 5, reason codes are always ReasonCode objects, and properties are always passed (as None when the protocol has none). The constructor now looks like this:
mqtt.Client(
callback_api_version, # new, first positional argument
client_id="",
clean_session=None,
userdata=None,
protocol=mqtt.MQTTv311,
transport="tcp",
reconnect_on_failure=True,
manual_ack=False,
)Other things to know: 2.x requires Python 3.7 or newer, and the publish.single() / subscribe.simple() helpers handle the version for you, so code that only uses them usually keeps working.
Callback signatures: 1.x vs VERSION2
| Callback | 1.x (MQTT 3.1.1) | 2.x with VERSION2 |
|---|---|---|
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 |
Three details matter in practice:
flagsis no longer a dict. Inon_connectit is aConnectFlagsobject: useflags.session_presentinstead offlags["session present"].disconnect_flags.is_disconnect_packet_from_servertells you whether the broker sent a DISCONNECT (MQTT 5) or the library detected the loss itself.on_subscribegets reason codes, not plain QoS integers. For a granted QoS 1 subscription, the reason code'svalueis1; a refused subscription has a value of0x80or higher, sorc.is_failureis true.
Reason codes are objects now
With VERSION2, reason_code is a ReasonCode. It compares equal to an integer and to its name, prints as its name, and has an is_failure property:
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"connect failed: {reason_code} ({reason_code.value})")
return
print("connected, session present:", flags.session_present)reason_code == 0 and reason_code == "Success" both still work for the success case, so the most common check survives the upgrade unchanged.
A complete before/after example
This is typical 1.x code — it breaks on 2.x with the ValueError above:
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, rc):
if rc == 0:
client.subscribe("testmqtt/paho2/#", qos=1)
else:
print("connect failed, rc =", rc)
def on_disconnect(client, userdata, rc):
print("disconnected, rc =", rc)
def on_message(client, userdata, msg):
print(msg.topic, msg.payload.decode())
client = mqtt.Client("testmqtt-paho2-demo")
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, 60)
client.loop_forever()The same client migrated to VERSION2:
import os
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"connect failed: {reason_code}")
return
print("connected, session present:", flags.session_present)
client.subscribe("testmqtt/paho2/#", qos=1)
def on_subscribe(client, userdata, mid, reason_code_list, properties):
for rc in reason_code_list:
print("subscribe:", "refused" if rc.is_failure else f"granted QoS {rc.value}")
def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
print(f"disconnected: {reason_code} (from server: {disconnect_flags.is_disconnect_packet_from_server})")
def on_message(client, userdata, msg):
print(msg.topic, msg.payload.decode())
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="testmqtt-paho2-demo")
if os.environ.get("MQTT_USER"):
client.username_pw_set(os.environ["MQTT_USER"], os.environ.get("MQTT_PASS"))
client.on_connect = on_connect
client.on_subscribe = on_subscribe
client.on_disconnect = on_disconnect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_forever()The same callbacks work unchanged with protocol=mqtt.MQTTv5 — that is the main benefit of VERSION2. Only the constructor arguments differ: MQTT 5 does not accept clean_session (it raises ValueError: Clean session is not used for MQTT 5.0); use clean_start and session expiry in connect() instead. The MQTT 5 vs 3.1.1 migration guide covers those options.
A migration checklist
- Pin the version you migrate to, for example
paho-mqtt>=2.1,<3inrequirements.txt, so the next major release cannot surprise you. - Search the codebase for
mqtt.Client(and passmqtt.CallbackAPIVersion.VERSION2first, withclient_id=as a keyword. - Update every callback to the new signature — including ones assigned in other modules or libraries.
- Replace integer comparisons on
rcwithreason_code.is_failureor name comparisons. - Replace
flags["session present"]withflags.session_present. - Run with warnings visible (
python -W error::DeprecationWarning app.py) to catch any client still on VERSION1.
If you are rewriting a lot of code anyway, consider whether an async client fits better — aiomqtt for asyncio is built on paho-mqtt 2.x. For other paho exceptions, see paho-mqtt Python errors.
Test the migrated client
Run client_v2.py, then open the online MQTT client in your browser, connect to broker.connectmq.com and publish to testmqtt/paho2/hello. Your script should print the topic and payload. To exercise the error paths, connect with a wrong password to your own broker and check that on_connect prints a readable reason such as Bad user name or password instead of a bare number.
Publish a test message to your migrated client
The paho-mqtt 2.0 migration is mostly mechanical: one new constructor argument, five callback signatures and a few integer checks that become reason code checks. Do it once with VERSION2, pin the major version, and your callbacks will behave the same on MQTT 3.1.1 and MQTT 5. For a full walkthrough of the library, see the Python MQTT guide.
Frequently asked questions
What does 'Unsupported callback API version' mean in paho-mqtt?
Since paho-mqtt 2.0 the first argument of mqtt.Client() is the callback API version. Code written for 1.x that passes the client ID as the first positional argument now raises this ValueError. Pass mqtt.CallbackAPIVersion.VERSION2 first and the client ID as client_id=.
Can I keep my old callbacks with paho-mqtt 2.x?
Yes, temporarily. Pass mqtt.CallbackAPIVersion.VERSION1 and the 1.x callback signatures keep working, but the library emits a DeprecationWarning and version 1 is planned to be removed in a future major release. Migrate to VERSION2 when you can.
Why does my 'if rc == 5' check no longer work after upgrading?
With VERSION2, on_connect receives a ReasonCode object even on MQTT 3.1.1, and 3.1.1 return codes are mapped to MQTT 5 reason codes. 'Not authorized' is 135 (0x87), not 5. Use reason_code.is_failure or compare against the name, for example reason_code == 'Not authorized'.

