MyPy: Como hacer que Python sea un lenguaje tipado

Después de mucho tiempo trabajando con Typescript el cambio a Python fue duro, muy duro sobre todo en lo que a los tipos se refiere.

¿Qué es MyPy?

Python es un lenguaje súper divertido y esta diversión se debe principalmente a su dinamismo, pero a su vez este dinamismo (al menos para mi parecer) tiene un gran problema y es que existirán probabilidades muy altas de que se produzcan errores en tiempo de ejecución.

Aquí entra MyPy en acción, una herramienta que nos va a permitir validar de forma estática los tipos de nuestros programas escritos en Python.

Cómo empezar a usar MyPy

Tanto para usar MyPy como un comando, como para añadirlo como dependencia dentro de nuestro proyecto es súper sencillo. Bastaría con ejecutar uno de los siguientes comandos:

pip3 install mypy # global
pip3 install -r requirements.txt # dependencia a nivel de proyecto
poetry add mypy # dependencia a nivel de proyecto usando poetry

Usar MyPy es tan sencillo como ejecutar:

  • mypy . para validar todos los ficheros de nuestro proyecto.
  • mypy my_directory para validar todos los fichero python dentro de un fichero.
  • mypy my_file.py para validar un fichero python en concreto.

Configurar MyPy

MyPy se puede configurar fácilmente mediante un fichero de configuración llamada mypy.ini. Este fichero tiene una serie de características que paso a explicar:

  • Una sección llamada [mypy] para especificar reglas a nivel general.
  • Pueden existir otras secciones con la forma [mypy-PATTERN1,PATTERN2,...] donde PATTERN1,… hacen referencia a los módulos del proyecto.
  • Podemos deshabilitar los tipos en una línea en concreto con # type: ignore

Este podría ser un ejemplo típico de un fichero de configuración:

# Global options:
[mypy]
python_version = 2.7
warn_return_any = True
warn_unused_configs = True

# Per-module options:
[mypy-mycode.foo.*]
disallow_untyped_defs = True
[mypy-mycode.bar]
warn_return_any = False
[mypy-somelibrary]
ignore_missing_imports = True

Los principales tipos que puedes usar en MyPy

Dentro del lenguaje de Python existen una serie de tipos que podremos usar para especificar a MyPy y que este nos valide si los estamos usando correctamente.

  • Tipos primitivos:
    • str
    • int
    • float
    • bool
    • bytes
  • Tipos complejos:
    • Any
    • None
    • List
    • Dict
    • Set
    • Tuple
    • Optional
    • Callable
    • Iterator
    • Union

Ejemplos de uso de MyPy

Caso sencillo

Después de tanta teoría vamos a practicar un poco! Imaginemos que tenemos el siguiente método en nuestro código:

def add_two_numbers(x, y):
    return x + y

¿Qué ocurriría si le pasamos como parámetros algo que no son números?

Pues en este caso en concreto, nada, pero no estaríamos usando correctamente nuestro método add_two_numbers ya que concatenariamos dos cadenas de texto en lugar de sumar dos números.

Esto se puede solucionar de forma muy sencilla añadiendo tipos:

def add_two_numbers(x: int, y: int) -> int:
    return x + y

De esta forma, si usamos MyPy seríamos capaces de detectar este tipo de errores de forma inmediata:

add_two_numbers(1, 2) # Todo ok!
add_two_numbers("a", "b") # Argument 1 to "add_two_numbers" has incompatible type "str"; expected "int"

Caso complejo

El caso anterior es muy sencillo, así que vamos a ver otro más complejo y seguro que súper habitual en nuestro día a día.

Imaginemos que tenemos una clase que se llama Person y otra Alien:

class Person:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"Hi {self.name}"

class Alien:
    def __init__(self, name):
        self.name = name

    def yell(self):
        return f"Hi {self.name}"

Finalmente tenemos un método que recibe una lista de personas y envía un saludo a cada una de ellas:

def say_hey_all(people):
    for person in people:
        person.speak()

¿Que ocurriría si por error le pasaramos a nuestro método say_hey_all una lista de aliens?

Pues que durante el desarrollo no encontraríamos ningún error, pero en tiempo de ejecución se producirá un error y se lanzaría una excepción ya que la clase Alien no tiene el método speak.

Para evitar este tipo de errores MyPy nos soluciona la vida ya que nos los va a detectar de forma inmediata. Bastaría con añadir los tipos a nuestro método y ejecutar MyPy:

def say_hey_all(people: List[Person]) -> None:
    for person in people:
        person.speak()

people = [Person("Peter"), Person("Brian"), Person("Stewie")]
aliens = [Alien("Alf"), Alien("ET"), Alien("Thanos")]

say_hey_all(people) # Todo ok
say_hey_all(aliens) # Argument 1 to "say_hey_all" has incompatible type "List[Alien]"; expected "List[Person]"

Conclusiones

Como hemos podido ver en este breve post, utilizar MyPy puede ser muy útil si quieres tener una red de seguridad extra en lo que a los tipos se refiere y dar un poco más de cuerpo a un lenguaje tan dinámico como Python.

¡Espero que os haya gustado!