Les presentamos la tercera parte de la traducción del material sobre el camino que ha recorrido la empresa Dropbox al implementar un sistema de verificación de tipos de código Python.
→ Partes anteriores: y
Lograr 4 millones de líneas de código tipado
Otra tarea importante (la segunda más mencionada en las encuestas internas) fue aumentar la cantidad de código en Dropbox que estaba cubierto por verificaciones de tipos. Probamos varios enfoques para abordar este desafío, desde el crecimiento natural de la base de código tipado hasta la concentración de los esfuerzos del equipo de mypy en la inferencia automática de tipos estáticos y dinámicos. Al final, parecía que no había una estrategia ganadora simple, pero logramos un crecimiento rápido en la cantidad de código anotado al combinar múltiples enfoques.
Como resultado, en nuestro repositorio Python más grande (con código de backend), la cantidad de líneas de código anotado alcanzó casi 4 millones. El trabajo de tipado estático del código se realizó en aproximadamente tres años. Mypy ahora admite varios tipos de informes sobre la cobertura del código con tipos, que facilitan el seguimiento del progreso de la tipificación. En particular, podemos generar informes sobre el código con incertidumbres en los tipos, tales como el uso explícito de tipo Any en las anotaciones que no se pueden verificar, o como importaciones de bibliotecas externas que carecen de anotaciones de tipo. Dentro del proyecto para mejorar la precisión de la verificación de tipos en Dropbox, contribuimos a la mejora de las definiciones de tipos (los llamados archivos stub) para algunas bibliotecas de código abierto populares en el repositorio centralizado de Python .
Implementamos (y estandarizamos en las siguientes PEP) nuevas características del sistema de tipos que permiten el uso de tipos más precisos para ciertos patrones específicos de Python. Un ejemplo notable de esto es TypeDict, que proporciona tipos para diccionarios similares a JSON que tienen un conjunto fijo de claves de tipo cadena, cada una con su propio tipo de valor. Continuaremos expandiendo el sistema de tipos. Probablemente, nuestro próximo paso será mejorar el soporte para las capacidades de Python en el manejo de números.

Número de líneas de código anotado: servidor

Número de líneas de código anotado: cliente

Número total de líneas de código anotado
Aquí tienes un resumen de las características principales de las acciones que hemos llevado a cabo para aumentar la cantidad de código anotado en Dropbox:
Rigor en la anotación. Hemos ido aumentando gradualmente los requisitos de rigor para la anotación de nuevo código. Comenzamos con consejos de linters que sugerían añadir anotaciones en archivos que ya contenían algunas anotaciones. Ahora exigimos la presencia de anotaciones de tipos en nuevos archivos de Python y en la mayoría de los archivos existentes.
Informes de tipado. Enviamos semanalmente a los equipos informes sobre el nivel de tipado de su código y les damos consejos acerca de qué debería ser anotado primero.
Promoción de mypy. Hablamos sobre mypy en diversos eventos y nos comunicamos con los equipos, ayudándoles a comenzar a utilizar anotaciones de tipos.
Encuestas. Realizamos encuestas periódicas a los usuarios para identificar los principales problemas. Estamos dispuestos a ir lo suficientemente lejos para resolver estos problemas (incluso hasta el punto de crear un nuevo lenguaje para acelerar mypy!).
Rendimiento. Hemos mejorado significativamente el rendimiento de mypy gracias al uso de un demonio y mypyc. Esto se hizo para suavizar las molestias que surgen durante el proceso de anotación y para permitir trabajar con grandes volúmenes de código.
Integración con editores. Creamos herramientas para soportar la ejecución de mypy en los editores que son populares en Dropbox. Esto incluye PyCharm, Vim y VS Code. Esto ha simplificado considerablemente el proceso de realizar trabajos de anotación de código y de verificar su funcionalidad. Estas acciones son habituales cuando se anota código existente.
Análisis estático. Creamos una herramienta para extraer firmas de funciones utilizando técnicas de análisis estático. Esta herramienta puede funcionar solo en situaciones relativamente simples, pero nos ayudó a aumentar la cobertura del código con tipos sin grandes esfuerzos.
Soporte para bibliotecas de terceros. En muchos de nuestros proyectos utilizamos un conjunto de herramientas de SQLAlchemy. Este aplica las capacidades dinámicas de Python, que los tipos PEP 484 no pueden modelar directamente. Hemos creado un archivo stub correspondiente y escrito un plugin para mypy, de acuerdo con PEP 561,), que mejora el soporte de SQLAlchemy.
Dificultades que encontramos
El camino hacia 4 millones de líneas de código tipificado no siempre fue fácil. En este trayecto nos encontramos con varios baches y cometimos varios errores. Aquí hay algunos de los problemas que enfrentamos. Esperamos que contar sobre ellos ayude a otros a evitar problemas similares.
Archivos faltantes. Comenzamos a trabajar con la revisión de un número pequeño de archivos. Todo lo que no estaba en el conjunto de estos archivos no se revisaba. Los archivos se añadían a la lista de revisión cuando aparecían las primeras anotaciones. Si se importaba algo de un módulo ubicado fuera del ámbito de revisión, se trataba de trabajar con valores de tipo Any, que no se revisaban en absoluto. Esto llevó a una pérdida significativa de precisión en la tipificación, especialmente en las primeras etapas de la migración. Este enfoque funcionó sorprendentemente bien, aunque era típico que añadir archivos al ámbito de revisión descubriera problemas en otras partes de la base de código. En el peor de los casos, cuando se unían dos áreas de código aisladas, en las que las tipificaciones se verificaron de forma independiente, resultaba que los tipos de estas áreas eran incompatibles entre sí. Esto llevaba a la necesidad de realizar muchos cambios en las anotaciones. Ahora, mirando hacia atrás, entendemos que debimos añadir a la revisión de tipos mypy los módulos de biblioteca base tan pronto como fuera posible. Esto habría hecho nuestro trabajo mucho más predecible.
Anotación del código antiguo. Cuando comenzamos a trabajar, teníamos alrededor de 4 millones de líneas de código Python existente. Era claro que anotar todo ese código era una tarea difícil. Creamos una herramienta llamada PyAnnotate, que puede recolectar información sobre tipos durante la ejecución de pruebas y puede agregar anotaciones de tipos al código, basándose en la información recolectada. Sin embargo, no notamos una adopción especialmente amplia de esta herramienta. La recolección de información sobre tipos era lenta, y las anotaciones generadas automáticamente a menudo requerían muchas correcciones manuales. Pensamos en ejecutar automáticamente esta herramienta en cada verificación de código, o en recolectar información sobre tipos basándonos en el análisis de un pequeño volumen de solicitudes reales de la red, pero decidimos no hacerlo, ya que cualquiera de estos enfoques era demasiado arriesgado.
Como conclusión, se puede señalar que la mayor parte del código fue anotado manualmente por sus propietarios. Para guiar este proceso en la dirección correcta, preparamos informes sobre módulos y funciones especialmente importantes que deben ser anotados. Por ejemplo, es crucial proporcionar anotaciones de tipos para el módulo de biblioteca que se utiliza en cientos de lugares. En cambio, anotar un servicio antiguo que está siendo reemplazado por uno nuevo no es tan importante. Además, estamos experimentando con el uso de análisis estático para generar anotaciones de tipos para el código antiguo.
Importaciones cíclicas. Anteriormente mencioné las importaciones cíclicas (los "enredos de dependencias"), cuya existencia complicó la aceleración de mypy. Además, tuvimos que trabajar seriamente para dotar a mypy de soporte para todos los tipos de idiomáticas, cuya causa son estas importaciones cíclicas. Recientemente completamos un gran proyecto de rediseño del sistema, que solucionó la mayoría de los problemas de mypy relacionados con las importaciones cíclicas. Estos problemas, de hecho, se originaron en los primeros días del proyecto, incluso en Alore, el lenguaje educativo para el cual originalmente se diseñó mypy. La sintaxis de Alore permite resolver fácilmente los problemas de los comandos de importación cíclicos. El mypy moderno heredó algunas limitaciones de su primera implementación rudimentaria (que era adecuada para Alore). Python complica el trabajo con importaciones cíclicas principalmente debido a la ambigüedad de las expresiones. Por ejemplo, durante una operación de asignación de valores, un alias de tipo puede ser determinado. Mypy no siempre puede detectar tales cosas hasta que gran parte del ciclo de importación haya sido procesada. En Alore no había tales ambigüedades. Las decisiones desafortunadas tomadas en los primeros días de desarrollo del sistema pueden presentar al programador una sorpresa desagradable muchos años después.
Conclusiones: el camino hacia 5 millones de líneas de código y nuevos horizontes
El proyecto mypy ha recorrido un largo camino, desde los primeros prototipos hasta un sistema que controla los tipos de código de producción que abarca 4 millones de líneas. A medida que mypy ha evolucionado, se ha estandarizado las sugerencias de tipos en Python. Hoy en día, ha surgido un ecosistema robusto en torno a la tipificación del código Python. Hay soporte para bibliotecas, herramientas auxiliares para IDE y editores, y existen varios sistemas de control de tipos, cada uno con sus ventajas y desventajas.
A pesar de que la verificación de tipos ya se considera por defecto en Dropbox, estoy convencido de que aún estamos en los inicios de la tipificación del código Python. Creo que las tecnologías de verificación de tipos seguirán desarrollándose y mejorando.
Si aún no has utilizado las verificaciones de tipos en tu proyecto de Python a gran escala, considera que ahora es un momento muy adecuado para comenzar la transición hacia la tipificación estática. He hablado con quienes han realizado esta transición, y nadie se ha arrepentido. El control de tipos convierte a Python en un lenguaje que se adapta mucho mejor para el desarrollo de grandes proyectos que el 'Python común'.
¡Estimados lectores! ¿Utilizas el control de tipos en tus proyectos de Python?
Fuente: habr.com
