Portando código de Python 2 a Python 3

autor

Brett Cannon

Resumen

Dado que Python 3 es el futuro de Python mientras Python 2 todavía está en uso activo, es bueno tener su proyecto disponible para ambas versiones principales de Python. Esta guía está diseñada para ayudarle a averiguar la mejor manera de admitir Python 2 y 3 simultáneamente.

Si está buscando portar un módulo de extensión en lugar de código Python puro, consulte Portar módulos de extensión a Python 3.

Si desea leer la opinión de un desarrollador central de Python sobre por qué Python 3 nació, puede leer las Python 3 Q & A de Nick Coghlan o el artículo de Brett Cannon Why Python 3 exists.

For help with porting, you can view the archived python-porting mailing list.

La breve explicación

Para que su proyecto sea compatible con Python 2/3 de una sola fuente, los pasos básicos son:

  1. Sólo preocúpate por admitir Python 2.7

  2. Asegúrese de tener una buena cobertura de prueba (coberturas.py puede ayudar; python -m pip install coverage)

  3. Aprende las diferencias entre Python 2 & 3

  4. Utilice Futurize (o Modernize) para actualizar su código (por ejemplo, python -m pip install future)

  5. Use Pylint para asegurarse de que no retrocede en su compatibilidad con Python 3 (python -m pip install pylint)

  6. Use caniusepython3 para averiguar cuáles de sus dependencias están bloqueando el uso de Python 3 (python -m pip install caniusepython3)

  7. Una vez que sus dependencias ya no lo bloqueen, use la integración continua para asegurarse de que sigue siendo compatible con Python 2 y 3 (tox puede ayudar a probar contra múltiples versiones de Python; python -m pip install tox)

  8. Considere usar la verificación de tipo estática opcional para asegurarse de que su uso de tipo funcione tanto en Python 2 como en 3 (por ejemplo, use mypy para verificar su escritura en Python 2 y Python 3; python -m pip install mypy).

Nota

Nota: El uso de python -m pip install garantiza que el pip que invoca es el que está instalado para el Python actualmente en uso, ya sea un pip de todo el sistema o uno instalado dentro de un entorno virtual.

Detalles

Un punto clave sobre el soporte de Python 2 & 3 simultáneamente es que se puede empezar hoy! Incluso si sus dependencias no son compatibles con Python 3 todavía eso no significa que no puede modernizar el código ahora para admitir Python 3. La mayoría de los cambios necesarios para admitir Python 3 conducen a código más limpio utilizando prácticas más recientes incluso en código Python 2.

Otro punto clave es que la modernización del código de Python 2 para que también admita Python 3 está en gran medida automatizada para usted. Si bien es posible que tenga que tomar algunas decisiones de API gracias a python 3 aclarando los datos de texto frente a los datos binarios, el trabajo de nivel inferior ahora se realiza principalmente por usted y, por lo tanto, al menos puede beneficiarse de los cambios automatizados inmediatamente.

Tenga en cuenta esos puntos clave mientras lee sobre los detalles de la migración del código para admitir Python 2 & 3 simultáneamente.

Compatibilidad con Python 2.6 y versiones anteriores

Si bien puede hacer que Python 2.5 funcione con Python 3, es mucho más fácil si solo tiene que trabajar con Python 2.7. Si eliminar Python 2.5 no es una opción, entonces el proyecto six puede ayudarlo a admitir Python 2.5 y 3 simultáneamente (python -m pip install six). Sin embargo, tenga en cuenta que casi todos los proyectos enumerados en este COMO no estarán disponibles para usted.

Si puede omitir Python 2.5 y versiones anteriores, los cambios necesarios en el código deben seguir pareciendo código Python idiomático. En el peor de los casos tendrá que utilizar una función en lugar de un método en algunos casos o tendrá que importar una función en lugar de usar una integrada, pero de lo contrario la transformación general no debería sentirse ajena a usted.

Pero usted debe apuntar a sólo apoyar Python 2.7. Python 2.6 ya no se admite libremente y, por lo tanto, no recibe correcciones de errores. Esto significa que usted tendrá que solucionar cualquier problema que encuentre con Python 2.6. También hay algunas herramientas mencionadas en este HOWTO que no son compatibles con Python 2.6 (por ejemplo, Pylint), y esto se volverá más común a medida que pasa el tiempo. Simplemente será más fácil para usted si sólo admite las versiones de Python que tiene que admitir.

Asegúrese de especificar el soporte de versión adecuado en su archivo setup.py

En su archivo setup.py debe tener el trove classifier adecuado especificando qué versiones de Python admite. Como su proyecto no es compatible con Python 3, al menos debe tener Programming Language :: Python :: 2 :: Only especificado. Idealmente también debe especificar cada versión principal/menor de Python que admita, por ejemplo, Programming Language :: Python :: 2.7.

Tener una buena cobertura de prueba

Una vez que tenga su código compatible con la versión más antigua de Python 2 que desee, querrá asegurarse de que su conjunto de pruebas tenga una buena cobertura. Una buena regla general es que si desea tener la suficiente confianza en su conjunto de pruebas, cualquier falla que aparezca después de que las herramientas reescriban su código son errores reales en las herramientas y no en su código. Si desea un número al que apuntar, intente obtener una cobertura superior al 80% (y no se sienta mal si le resulta difícil obtener una cobertura superior al 90%). Si aún no tiene una herramienta para medir la cobertura de la prueba, se recomienda cover.py.

Aprende las diferencias entre Python 2 & 3

Una vez que tenga su código bien probado, ¡está listo para comenzar a migrar su código a Python 3! Pero para comprender completamente cómo va a cambiar el código y qué desea tener en cuenta mientras codifica, querrá aprender qué cambios produce Python 3 en términos de Python 2. Típicamente las dos mejores maneras de hacer eso es leer la documentación «What’s New» para cada versión de Python 3 y el libro Porting to Python 3 (que es gratis en línea). También hay una práctica cheat sheet del proyecto Python-Future.</whatsnew-index>.

Actualiza tu código

Una vez que sientas que sabes lo que es diferente en Python 3 en comparación con Python 2, ¡es hora de actualizar tu código! Puede elegir entre dos herramientas para migrar el código automáticamente: Futurize y Modernize. La herramienta que elija dependerá de la cantidad similar a Python 3 que desea que sea el código. Futurize hace todo lo posible para que Python 3 modismos y prácticas existan en Python 2, por ejemplo, backporting el tipo bytes de Python 3 para que tenga paridad semántica entre las versiones principales de Python. Modernize, por otro lado, es más conservador y se dirige a un subconjunto de Python 2/3 de Python, basándose directamente en six para ayudar a proporcionar compatibilidad. Como Python 3 es el futuro, podría ser mejor considerar Futurize para comenzar a adaptarse a cualquier nueva práctica que Python 3 introduce a la que aún no está acostumbrado.

Independientemente de la herramienta que elija, actualizarán el código para que se ejecute en Python 3 mientras se mantienen compatibles con la versión de Python 2 con la que comenzó. Dependiendo de lo conservador que desee ser, es posible que desee ejecutar la herramienta sobre el conjunto de pruebas primero e inspeccionar visualmente la diferencia para asegurarse de que la transformación es precisa. Después de transformar el conjunto de pruebas y comprobar que todas las pruebas siguen pasando según lo esperado, puede transformar el código de la aplicación sabiendo que cualquier prueba que falle es un error de traducción.

Desafortunadamente, las herramientas no pueden automatizar todo para que su código funcione bajo Python 3 y por lo que hay un puñado de cosas que tendrá que actualizar manualmente para obtener soporte completo de Python 3 (cuáles de estos pasos son necesarios varían entre las herramientas). Lea la documentación de la herramienta que elige utilizar para ver lo que corrige de forma predeterminada y lo que puede hacer opcionalmente para saber lo que (no) se fijará para usted y lo que puede tener que corregir por su cuenta (por ejemplo, usando io.open() sobre la función incorporada open() está desactivada por defecto en Modernizar). Afortunadamente, sin embargo, sólo hay un par de cosas a tener en cuenta por las cuales se pueden considerar grandes problemas que pueden ser difíciles de depurar si no se observan.

División

En Python 3, 5 / 2 == 2.5 y no 2; toda división entre los valores int da lugar a un float. Este cambio ha sido planeado desde Python 2.2, que fue lanzado en 2002. Desde entonces, se ha alentado a los usuarios a añadir from __future__ import division a todos y cada uno de los archivos que utilizan los operadores / y // o que ejecuten el intérprete con el indicador -Q. Si no ha estado haciendo esto, entonces tendrá que ir a través de su código y hacer dos cosas:

  1. Añadir from __future__ import division a sus archivos

  2. Actualice cualquier operador de división según sea necesario para utilizar // para usar la división de suelo o continuar usando / y esperar un número flotante

La razón por la que / no se traduce simplemente a // automáticamente es que si un objeto define un método __truediv__ pero no __floordiv__ entonces su código comenzaría a fallar (por ejemplo, una clase definida por el usuario que utiliza / para significar alguna operación pero no // para la misma cosa o en absoluto).

Texto frente a datos binarios

En Python 2 puede usar el tipo str tanto para texto como para datos binarios. Desafortunadamente, esta confluencia de dos conceptos diferentes podría conducir a código frágil que a veces funcionaba para cualquier tipo de datos, a veces no. También podría dar lugar a API confusas si las personas no declaraban explícitamente que algo que aceptaba str aceptaba datos binarios o de texto en lugar de un tipo específico. Esto complicó la situación especialmente para cualquier persona que admita varios idiomas, ya que las API no se molestarían explícitamente en admitir explícitamente Unicode cuando reclamaban compatibilidad con datos de texto.

Para hacer la distinción entre texto y datos binarios más claros y pronunciados, Python 3 hizo lo que la mayoría de los lenguajes creados en la era de Internet han hecho y ha hecho texto y datos binarios distintos tipos que no se pueden mezclar ciegamente (Python es anterior al acceso generalizado a Internet). Para cualquier código que se ocupe solo de texto o solo de datos binarios, esta separación no plantea un problema. Pero para el código que tiene que lidiar con ambos, significa que es posible que tenga que preocuparse ahora cuando está utilizando texto en comparación con los datos binarios, por lo que esto no se puede automatizar por completo.

Para empezar, tendrá que decidir qué API toman texto y cuáles toman binario (es altamente recomendado no diseñar API que pueden tomar ambos debido a la dificultad de mantener el código funcionando; como se indicó anteriormente es difícil hacerlo bien). En Python 2 esto significa asegurarse de que las API que toman texto pueden trabajar con unicode y las que funcionan con datos binarios funcionan con el tipo bytes de Python 3 (que es un subconjunto de str en Python 2 y actúa como un alias para bytes tipo en Python 2). Por lo general, el mayor problema es darse cuenta de qué métodos existen en qué tipos en Python 2 y 3 simultáneamente (para el texto que es Unicode en Python 2 y str en Python 3, para binario que es str/bytes en Python 2 y bytes en Python 3). En la tabla siguiente se enumeran los métodos unicos de cada tipo de datos en Python 2 y 3 (por ejemplo, el método decode() se puede utilizar en el tipo de datos binarios equivalente en Python 2 o 3, pero no puede ser utilizado por el tipo de datos textuales consistentemente entre Python 2 y 3 porque str en Python 3 no tiene el método). Tenga en cuenta que a partir de Python 3.5 se agregó el método __mod__ al tipo bytes.

Datos de texto

Datos binarios

decode

encode

format

isdecimal

isnumeric

La creación de la distinción más fácil de controlar se puede realizar mediante la codificación y descodificación entre datos binarios y texto en el borde del código. Esto significa que cuando reciba texto en datos binarios, debe descodificarlo inmediatamente. Y si el código necesita enviar texto como datos binarios, codificarlo lo más tarde posible. Esto permite que el código funcione solo con texto internamente y, por lo tanto, elimina tener que realizar un seguimiento del tipo de datos con los que está trabajando.

El siguiente problema es asegurarse de saber si los literales de cadena en el código representan texto o datos binarios. Debe agregar un prefijo b a cualquier literal que presente datos binarios. Para el texto debe agregar un prefijo u al literal de texto. (hay una importación __future__ para forzar que todos los literales no especificados sean Unicode, pero el uso ha demostrado que no es tan eficaz como agregar un prefijo b o u a todos los literales explícitamente)

Como parte de esta dicotomía también hay que tener cuidado con la apertura de archivos. A menos que haya estado trabajando en Windows, existe la posibilidad de que no siempre se haya molestado en agregar el modo b al abrir un archivo binario (por ejemplo, rb para la lectura binaria). En Python 3, los archivos binarios y los archivos de texto son claramente distintos y mutuamente incompatibles; ver el módulo io para más detalles. Por lo tanto, debe tomar una decisión de si un archivo se utilizará para el acceso binario (permitiendo que los datos binarios se lean y/o escriban) o el acceso textual (permitiendo que los datos de texto sean leídos y/o escritos). También debe utilizar io.open() para abrir archivos en lugar de la función incorporada open() como el módulo io es consistente de Python 2 a 3, mientras que la función incorporada open() no es (en Python 3 es en realidad io.open()). No se moleste con la práctica obsoleta de usar codecs.open() ya que sólo es necesario para mantener la compatibilidad con Python 2.5.

Los constructores de str y bytes tienen una semántica diferente para los mismos argumentos entre Python 2 y 3. Pasar un entero a bytes en Python 2 le dará la representación de cadena de texto del entero: bytes(3) == '3'. Pero en Python 3, un argumento entero para “”bytes”” le dará un objeto bytes siempre y cuando el entero especificado, lleno de bytes nulos: bytes(3) == b'\x00\x00\x00'. Una preocupación similar es necesaria cuando se pasa un objeto bytes a str. En Python 2, solo se obtiene el objeto bytes: str(b'3') == b'3'. Pero en Python 3 se obtiene la representación de cadena de texto del objeto bytes: str(b'3') == "b'3'".

Por último, la indexación de datos binarios requiere un control cuidadoso (el corte no requiere ningún control especial). En Python 2, b'123'[1] == b'2' mientras que en Python 3 b'123'[1] == 50. Dado que los datos binarios son simplemente una colección de números binarios, Python 3 devuelve el valor entero para el byte en el que indexa. Pero en Python 2, ya que bytes == str, la indexación devuelve un segmento de bytes de un solo elemento. El proyecto six tiene una función denominada six.indexbytes() que devolverá un entero como en Python 3: six.indexbytes(b'123', 1).

Para resumir:

  1. Decida cuál de sus API toma texto y cuáles toman datos binarios

  2. Asegúrese de que el código que funciona con texto también funciona con unicode y el código para datos binarios funciona con bytes en Python 2 (consulte la tabla anterior para los métodos que no puede usar para cada tipo)

  3. Marque todos los literales binarios con un prefijo b, literales textuales con un prefijo u

  4. Descodificar datos binarios en texto tan pronto como sea posible, codificar texto como datos binarios tan tarde como sea posible

  5. Abra los archivos con io.open() y asegúrese de especificar el modo b cuando sea apropiado

  6. Tenga cuidado al indexar en datos binarios

Utilice la detección de funciones en lugar de la detección de versiones

Inevitablemente tendrá código que tiene que elegir qué hacer en función de qué versión de Python se está ejecutando. La mejor manera de hacerlo es con la detección de características de si la versión de Python en la que se ejecuta es compatible con lo que necesita. Si por alguna razón eso no funciona, entonces usted debe hacer que la comprobación de la versión sea contra Python 2 y no Python 3. Para ayudar a explicar esto, veamos un ejemplo.

Supongamos que necesita acceso a una característica de importlib que está disponible en la biblioteca estándar de Python desde Python 3.3 y disponible para Python 2 a través de importlib2 en PyPI. Es posible que tenga la tentación de escribir código para acceder, por ejemplo, al módulo importlib.abc haciendo lo siguiente:

import sys

if sys.version_info[0] == 3:
    from importlib import abc
else:
    from importlib2 import abc

El problema con este código es ¿qué sucede cuando sale Python 4? Sería mejor tratar Python 2 como el caso excepcional en lugar de Python 3 y asumir que las futuras versiones de Python serán más compatibles con Python 3 que Python 2:

import sys

if sys.version_info[0] > 2:
    from importlib import abc
else:
    from importlib2 import abc

La mejor solución, sin embargo, es no hacer ninguna detección de versiones en absoluto y en su lugar confiar en la detección de características. Esto evita cualquier problema potencial de conseguir la detección de la versión incorrecta y le ayuda a mantenerse compatible con el futuro:

try:
    from importlib import abc
except ImportError:
    from importlib2 import abc

Evitar regresiones de compatibilidad

Una vez que haya traducido completamente el código para que sea compatible con Python 3, querrá asegurarse de que el código no retroceda y deje de funcionar bajo Python 3. Esto es especialmente cierto si tiene una dependencia que le está bloqueando para que no se ejecute realmente en Python 3 en este momento.

Para ayudar a mantenerse compatible, los módulos nuevos que cree deben tener al menos el siguiente bloque de código en la parte superior del misma:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

También puede ejecutar Python 2 con el indicador -3 para recibir una advertencia sobre varios problemas de compatibilidad que el código desencadena durante la ejecución. Si convierte las advertencias en errores con -Werror, puede asegurarse de que no se pierda accidentalmente una advertencia.

También puede usar el proyecto de Pylint y su indicador --py3k para lintar el código para recibir advertencias cuando el código comienza a desviarse de la compatibilidad con Python 3. Esto también evita que tenga que ejecutar Modernize o Futurize sobre el código con regularidad para detectar las regresiones de compatibilidad. Esto requiere que solo admita Python 2.7 y Python 3.4 o posterior, ya que es la compatibilidad mínima de la versión mínima de Python de Pylint.

Compruebe qué dependencias bloquean la transición

Después de que haya hecho que su código sea compatible con Python 3, debe empezar a preocuparse por si sus dependencias también se han portado. El proyecto caniusepython3 se creó para ayudarle a determinar qué proyectos – directa o indirectamente – le impiden admitir Python 3. Hay una herramienta de línea de comandos, así como una interfaz web en https://caniusepython3.com.

El proyecto también proporciona código que puede integrar en el conjunto de pruebas para que tenga una prueba con errores cuando ya no tenga dependencias que le impidan usar Python 3. Esto le permite evitar tener que comprobar manualmente sus dependencias y recibir notificaciones rápidamente cuando puede empezar a ejecutarse en Python 3.

Actualice su archivo setup.py para denotar compatibilidad con Python 3

Una vez que el código funciona en Python 3, debe actualizar los clasificadores en su setup.py para que contenga Programming Language :: Python :: 3 y no especificar solo compatibilidad con Python 2. Esto le dirá a cualquier persona que use su código que admite Python 2 y 3. Lo ideal es que también desee agregar clasificadores para cada versión principal/menor de Python que ahora admita.

Utilice la integración continua para seguir siendo compatible

Una vez que pueda ejecutar completamente bajo Python 3, querrá asegurarse de que el código siempre funciona en Python 2 y 3. Probablemente la mejor herramienta para ejecutar las pruebas en varios intérpretes de Python es tox. A continuación, puede integrar tox con su sistema de integración continua para que nunca interrumpa accidentalmente la compatibilidad con Python 2 o 3.

También es posible que desee utilizar el indicador -bb con el intérprete de Python 3 para desencadenar una excepción cuando se comparan bytes con cadenas o bytes con un int (este último está disponible a partir de Python 3.5). De forma predeterminada, las comparaciones de tipos diferentes simplemente devuelven False, pero si cometió un error en la separación del control de datos de texto/binario o la indexación en bytes, no encontraría fácilmente el error. Esta marca generará una excepción cuando se produzcan este tipo de comparaciones, lo que hace que el error sea mucho más fácil de rastrear.

¡Y eso es sobre todo! En este punto, la base de código es compatible con Python 2 y 3 simultáneamente. Las pruebas también se configurarán para que no interrumpa accidentalmente la compatibilidad de Python 2 o 3, independientemente de la versión en la que ejecute normalmente las pruebas durante el desarrollo.

Considere la posibilidad de usar la comprobación de tipos estáticos opcionales

Otra forma de ayudar a transferir el código es usar un comprobador de tipos estáticos como mypy o pytype en el código. Estas herramientas se pueden utilizar para analizar el código como si se estuviera ejecutando en Python 2, puede ejecutar la herramienta por segunda vez como si el código se ejecutara en Python 3. Al ejecutar un comprobador de tipos estáticos dos veces como este, puede descubrir si, por ejemplo, está usando incorrectamente el tipo de datos binarios en una versión de Python en comparación con otra. Si agrega sugerencias de tipo opcionales al código, también puede indicar explícitamente si las API usan datos textuales o binarios, lo que ayuda a asegurarse de que todo funciona según lo esperado en ambas versiones de Python.