MyPy - Python y un sistema de tipado estático

Esta notebook fue creada originalmente como un blog post por Raúl E. López Briega en Mi blog sobre Python. El contenido esta bajo la licencia BSD.

Una de las razones por la que solemos amar a Python, es por su sistema de tipado dinámico, el cual lo convierte en un lenguaje de programación sumamente flexible y fácil de aprender; al no tener que preocuparnos por definir los tipos de los objetos, ya que Python los infiere por nosotros, podemos escribir programas en una forma mucho más productiva, sin verbosidad y utilizando menos líneas de código.

Ahora bien, este sistema de tipado dinámico también puede convertirse en una pesadilla en proyectos de gran escala, requiriendo varias horas de pruebas unitarias para evitar que los objetos adquieran un tipo de datos que no deberían y complicando el su mantenimiento o futura refactorización.

Por ejemplo, en un código tan trivial como el siguiente:


In [1]:
def saludo(nombre):
    return 'Hola {}'.format(nombre)

Esta simple función nos va a devolver el texto 'Hola' seguido del nombre que le ingresemos; pero como no contiene ningún control sobre el tipo de datos que pude admitir la variable nombre, los siguientes casos serían igualmente válidos:


In [2]:
print (saludo('Raul'))
print (saludo(1))


Hola Raul
Hola 1

En cambio, si pusiéramos un control sobre el tipo de datos que admitiera la variable nombre, para que siempre fuera un string, entonces el segundo caso ya no sería válido y lo podríamos detectar fácilmente antes de que nuestro programa se llegara a ejecutar.

Obviamente, para poder detectar el segundo error y que nuestra función saludo solo admita una variable del tipo string como argumento, podríamos reescribir nuestra función, agregando un control del tipo de datos de la siguiente manera:


In [3]:
def saludo(nombre):
    if type(nombre) != str:
        return "Error: el argumento debe ser del tipo String(str)"
    return 'Hola {}'.format(nombre)

print(saludo('Raul'))
print(saludo(1))


Hola Raul
Error: el argumento debe ser del tipo String(str)

Pero una solución más sencilla a tener que ir escribiendo condiciones para controlar los tipos de las variables o de las funciones es utilizar MyPy

MyPy

MyPy es un proyecto que busca combinar los beneficios de un sistema de tipado dinámico con los de uno de tipado estático. Su meta es tener el poder y la expresividad de Python combinada con los beneficios que otorga el chequeo de los tipos de datos al momento de la compilación.

Algunos de los beneficios que proporciona utilizar MyPy son:

  • Chequeo de tipos al momento de la compilación: Un sistema de tipado estático hace más fácil detectar errores y con menos esfuerzo de debugging.
  • Facilita el mantenimiento: Las declaraciones explícitas de tipos actúan como documentación, haciendo que nuestro código sea más fácil de entender y de modificar sin introducir nuevos errores.
  • Permite crecer nuestro programa desde un tipado dinámico hacia uno estático: Nos permite comenzar desarrollando nuestros programas con un tipado dinámico y a mediada que el mismo vaya madurando podríamos modificarlo hacia un tipado estático de forma muy sencilla. De esta manera, podríamos beneficiarnos no solo de la comodidad de tipado dinámico en el desarrollo inicial, sino también aprovecharnos de los beneficios de los tipos estáticos cuando el código crece en tamaño y complejidad.

Tipos de datos

Estos son algunos de los tipos de datos más comunes que podemos encontrar en Python:

  • int: Número entero de tamaño arbitrario
  • float: Número flotante.
  • bool: Valor booleano (True o False)
  • str: Unicode string
  • bytes: 8-bit string
  • object: Clase base del que derivan todos los objecto en Python.
  • List[str]: lista de objetos del tipo string.
  • Dict[str, int]: Diccionario de string hacia enteros
  • Iterable[int]: Objeto iterable que contiene solo enteros.
  • Sequence[bool]: Secuencia de valores booleanos
  • Any: Admite cualquier valor. (tipado dinámico)

El tipo Any y los constructores List, Dict, Iterable y Sequence están definidos en el modulo typing que viene junto con MyPy.

Ejemplos

Por ejemplo, volviendo al ejemplo del comienzo, podríamos reescribir la función saludo utilizando MyPy de forma tal que los tipos de datos sean explícitos y puedan ser chequeados al momento de la compilación.


In [4]:
%%writefile typeTest.py
import typing

def saludo(nombre: str) -> str:
    return 'Hola {}'.format(nombre)

print(saludo('Raul'))
print(saludo(1))


Overwriting typeTest.py

En este ejemplo estoy creando un pequeño script y guardando en un archivo con el nombre 'typeTest.py', en la primer línea del script estoy importando la librería typing que viene con MyPy y es la que nos agrega la funcionalidad del chequeo de los tipos de datos. Luego simplemente ejecutamos este script utilizando el interprete de MyPy y podemos ver que nos va a detectar el error de tipo de datos en la segunda llamada a la función saludo.


In [5]:
!mypy typeTest.py


typeTest.py, line 7: Argument 1 to "saludo" has incompatible type "int"; expected "str"

Si ejecutáramos este mismo script utilizando el interprete de Python, veremos que obtendremos los mismos resultados que al comienzo de este notebook; lo que quiere decir, que la sintaxis que utilizamos al reescribir nuestra función saludo es código Python perfectamente válido!


In [6]:
!python3 typeTest.py


Hola Raul
Hola 1

Tipado explicito para variables y colecciones

En el ejemplo anterior, vimos como es la sintaxis para asignarle un tipo de datos a una función, la cual utiliza la sintaxis de Python3, annotations.

Si quisiéramos asignarle un tipo a una variable, podríamos utilizar la función Undefined que viene junto con MyPy.


In [7]:
%%writefile typeTest.py
from typing import Undefined, List, Dict

# Declaro los tipos de las variables
texto = Undefined(str)
entero = Undefined(int)
lista_enteros = List[int]()
dic_str_int = Dict[str, int]()

# Asigno valores a las variables.
texto = 'Raul'
entero = 13
lista_enteros = [1, 2, 3, 4]
dic_str_int = {'raul': 1, 'ezequiel': 2}

# Intento asignar valores de otro tipo.
texto = 1
entero = 'raul'
lista_enteros = ['raul', 1, '2']
dic_str_int = {1: 'raul'}


Overwriting typeTest.py

In [8]:
!mypy typeTest.py


typeTest.py, line 16: Incompatible types in assignment (expression has type "int", variable has type "str")
typeTest.py, line 17: Incompatible types in assignment (expression has type "str", variable has type "int")
typeTest.py, line 18: List item 1 has incompatible type "str"
typeTest.py, line 18: List item 3 has incompatible type "str"
typeTest.py, line 19: List item 1 has incompatible type "Tuple[int, str]"

Otra alternativa que nos ofrece MyPy para asignar un tipo de datos a las variables, es utilizar comentarios; así, el ejemplo anterior lo podríamos reescribir de la siguiente forma, obteniendo el mismo resultado:


In [9]:
%%writefile typeTest.py
from typing import List, Dict

# Declaro los tipos de las variables
texto = ''              # type: str
entero = 0              # type: int
lista_enteros = []      # type: List[int]
dic_str_int = {}        # type: Dict[str, int]

# Asigno valores a las variables.
texto = 'Raul'
entero = 13
lista_enteros = [1, 2, 3, 4]
dic_str_int = {'raul': 1, 'ezequiel': 2}

# Intento asignar valores de otro tipo.
texto = 1
entero = 'raul'
lista_enteros = ['raul', 1, '2']
dic_str_int = {1: 'raul'}


Overwriting typeTest.py

In [10]:
!mypy typeTest.py


typeTest.py, line 16: Incompatible types in assignment (expression has type "int", variable has type "str")
typeTest.py, line 17: Incompatible types in assignment (expression has type "str", variable has type "int")
typeTest.py, line 18: List item 1 has incompatible type "str"
typeTest.py, line 18: List item 3 has incompatible type "str"
typeTest.py, line 19: List item 1 has incompatible type "Tuple[int, str]"

Instalando MyPy

Instalar MyPy es bastante fácil, simplemente debemos seguir los siguientes pasos:

Si utilizan git, pueden clonar el repositorio de mypy:

$ git clone https://github.com/JukkaL/mypy.git

Si no utilizan git, como alternativa, se pueden descargar la última versión de mypy en el siguiente link:

https://github.com/JukkaL/mypy/archive/master.zip

Una vez que ya se lo descargaron, se posicionan dentro de la carpeta de mypy y ejecutan el script setup.py para instalarlo:

$ python3 setup.py install

Reemplacen 'python3' con su interprete para python3.

MyPy como parte de Python 3.5 ?

Guido van Rossum, el creador de Python, ha enviado reciente una propuesta a la lista de correo de python-ideas, en la cual sugiere agregar en la próxima versión de Python la sintaxis de MyPy para las functions annotations. Pueden encontrar la propuesta en el siguiente link:

https://mail.python.org/pipermail/python-ideas/2014-August/028618.html

También pueden seguir las discusiones que se generaron sobre este tema en Reddit

Saludos!

Este post fue escrito utilizando IPython notebook. Pueden descargar este notebook o ver su version estática en nbviewer.