paho-mqtt 2.0 cambió la forma de crear un cliente y lo que reciben tus callbacks. Si tu script dejó de funcionar tras un pip install --upgrade, la solución casi siempre es la misma: pasa mqtt.CallbackAPIVersion.VERSION2 como primer argumento de mqtt.Client() y actualiza las firmas de los callbacks. Esta guía muestra los errores exactos, qué cambió y un ejemplo completo de antes y después.
Los errores que probablemente estás viendo
| Error | Causa | Solución |
|---|---|---|
ValueError: Unsupported callback API version: version 2.0 added a callback_api_version, see docs/migrations.rst for details | Se pasó el client ID como primer argumento posicional: 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() sin versión, o con VERSION1 | Pasa VERSION2 y actualiza los callbacks |
TypeError: on_connect() takes 4 positional arguments but 5 were given | VERSION2 con un on_connect(client, userdata, flags, rc) al estilo 1.x | Añade el parámetro properties (ver abajo) |
TypeError: on_disconnect() takes 3 positional arguments but 5 were given | Lo mismo con on_disconnect(client, userdata, rc) | Nueva firma con disconnect_flags, reason_code, properties |
AttributeError: module 'paho.mqtt.client' has no attribute 'CallbackAPIVersion' | Tu código es para 2.x pero el entorno sigue teniendo 1.x | pip install "paho-mqtt>=2.0" y comprueba qué intérprete ejecuta el script |
Comprueba primero la versión instalada; los entornos virtuales y el Python del sistema a menudo no coinciden:
python -c "import paho.mqtt; print(paho.mqtt.__version__)"
pip show paho-mqttQué cambió en 2.0
La versión 1.x tenía un único estilo de callbacks cuyos argumentos dependían de la versión del protocolo: los callbacks de MQTT 3.1.1 recibían un entero rc y los de MQTT 5 recibían reason_code y properties. Era fácil escribir código que funcionaba con un protocolo y fallaba con el otro.
2.0 introduce las versiones de la API de callbacks. Con CallbackAPIVersion.VERSION2, cada callback tiene la misma firma en MQTT 3.1.1 y MQTT 5, los reason codes son siempre objetos ReasonCode y las properties se pasan siempre (como None cuando el protocolo no tiene). El constructor queda así:
mqtt.Client(
callback_api_version, # nuevo, primer argumento posicional
client_id="",
clean_session=None,
userdata=None,
protocol=mqtt.MQTTv311,
transport="tcp",
reconnect_on_failure=True,
manual_ack=False,
)Otras cosas que conviene saber: 2.x requiere Python 3.7 o superior, y los helpers publish.single() / subscribe.simple() gestionan la versión por ti, así que el código que solo los usa suele seguir funcionando.
Firmas de callbacks: 1.x vs VERSION2
| Callback | 1.x (MQTT 3.1.1) | 2.x con 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) | sin cambios |
on_log | (client, userdata, level, buf) | sin cambios |
En la práctica importan tres detalles:
flagsya no es un dict. Enon_connectes un objetoConnectFlags: usaflags.session_presenten lugar deflags["session present"].disconnect_flags.is_disconnect_packet_from_serverte dice si el broker envió un DISCONNECT (MQTT 5) o si la propia librería detectó la pérdida.on_subscriberecibe reason codes, no enteros de QoS. Para una suscripción concedida con QoS 1, elvaluedel reason code es1; una suscripción rechazada tiene un valor de0x80o superior, así querc.is_failurees verdadero.
Los reason codes ahora son objetos
Con VERSION2, reason_code es un ReasonCode. Se compara como igual a un entero y a su nombre, se imprime como su nombre y tiene la propiedad is_failure:
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"conexión fallida: {reason_code} ({reason_code.value})")
return
print("conectado, sesión presente:", flags.session_present)reason_code == 0 y reason_code == "Success" siguen funcionando para el caso de éxito, así que la comprobación más habitual sobrevive a la actualización sin cambios.
Un ejemplo completo de antes y después
Este es código típico de 1.x; en 2.x falla con el ValueError de arriba:
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()El mismo cliente migrado a 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"conexión fallida: {reason_code}")
return
print("conectado, sesión presente:", 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("suscripción:", "rechazada" if rc.is_failure else f"QoS concedido {rc.value}")
def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
print(f"desconectado: {reason_code} (desde el servidor: {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()Los mismos callbacks funcionan sin cambios con protocol=mqtt.MQTTv5: esa es la principal ventaja de VERSION2. Solo cambian los argumentos del constructor: MQTT 5 no acepta clean_session (lanza ValueError: Clean session is not used for MQTT 5.0); usa clean_start y la expiración de sesión en connect(). La guía de migración de MQTT 3.1.1 a MQTT 5 cubre esas opciones.
Checklist de migración
- Fija la versión a la que migras, por ejemplo
paho-mqtt>=2.1,<3enrequirements.txt, para que la próxima versión mayor no te sorprenda. - Busca
mqtt.Client(en el código y pasa primeromqtt.CallbackAPIVersion.VERSION2, conclient_id=como argumento con nombre. - Actualiza todos los callbacks a la nueva firma, incluidos los asignados en otros módulos o librerías.
- Sustituye las comparaciones de enteros sobre
rcporreason_code.is_failureo comparaciones por nombre. - Sustituye
flags["session present"]porflags.session_present. - Ejecuta con los warnings visibles (
python -W error::DeprecationWarning app.py) para detectar cualquier cliente que siga en VERSION1.
Si de todos modos vas a reescribir mucho código, plantéate si te conviene más un cliente asíncrono: aiomqtt para asyncio está construido sobre paho-mqtt 2.x. Para otras excepciones de paho, consulta errores de paho-mqtt en Python.
Prueba el cliente migrado
Ejecuta client_v2.py, abre el cliente MQTT online en tu navegador, conéctate a broker.connectmq.com y publica en testmqtt/paho2/hello. Tu script debería imprimir el topic y el payload. Para probar los caminos de error, conéctate con una contraseña incorrecta a tu propio broker y comprueba que on_connect imprime un motivo legible como Bad user name or password en lugar de un número suelto.
Publica un mensaje de prueba para tu cliente migrado
La migración a paho-mqtt 2.0 es sobre todo mecánica: un nuevo argumento en el constructor, cinco firmas de callbacks y unas cuantas comprobaciones de enteros que pasan a ser comprobaciones de reason codes. Hazla una vez con VERSION2, fija la versión mayor y tus callbacks se comportarán igual en MQTT 3.1.1 y MQTT 5. Para un recorrido completo por la librería, consulta la guía de MQTT con Python.
Preguntas frecuentes
¿Qué significa 'Unsupported callback API version' en paho-mqtt?
Desde paho-mqtt 2.0, el primer argumento de mqtt.Client() es la versión de la API de callbacks. El código escrito para 1.x que pasa el client ID como primer argumento posicional lanza ahora este ValueError. Pasa primero mqtt.CallbackAPIVersion.VERSION2 y el client ID como client_id=.
¿Puedo mantener mis callbacks antiguos con paho-mqtt 2.x?
Sí, de forma temporal. Pasa mqtt.CallbackAPIVersion.VERSION1 y las firmas de callbacks de 1.x siguen funcionando, pero la librería emite un DeprecationWarning y está previsto eliminar la versión 1 en una futura versión mayor. Migra a VERSION2 en cuanto puedas.
¿Por qué mi comprobación 'if rc == 5' dejó de funcionar tras actualizar?
Con VERSION2, on_connect recibe un objeto ReasonCode incluso en MQTT 3.1.1, y los códigos de retorno de 3.1.1 se traducen a reason codes de MQTT 5. 'Not authorized' es 135 (0x87), no 5. Usa reason_code.is_failure o compara con el nombre, por ejemplo reason_code == 'Not authorized'.

