Charles Gaillard

Charles Gaillard

Maxime Churin

Maxime Churin

Prácticas de codificación en Python

alguien poniendo un pincel de pintura en una paleta de colores

En el desarrollo de software, los desarrolladores suelen trabajar en un entorno colaborativo y en constante evolución. Por esta razón, es muy recomendable que sigan pautas y prácticas de codificación comunes que garantizarán eficiencia, claridad, fácil detección de errores y una incorporación sencilla. Después de todo, es más probable que el código bien formateado y documentado sea compartido y utilizado por toda la comunidad de desarrolladores.

Aquí proponemos un conjunto de herramientas para desarrolladores Python que pueden ayudarte a alcanzar este objetivo, probadas con versiones de Python 3.8:

  • Black
  • Flake8
  • Isort
  • Mypy
  • Pydocstyle
  • Darglint

Black

Black es una herramienta de formato rápido que se puede usar muy fácilmente en la línea de comandos.

Reformatear tu código al estilo Black te permite producir código limpio y legible, facilitando la detección de errores.

En este ejemplo:

class Car:
  

   def __init__(self,
       brand: str ,  color: str  ) -> None:
       self.brand=   brand
       self.color=   color
   def __str__(self) -> str:
       return (
           f"Car(brand: {self.brand}, color: {self.color})"
       )

Ejecutando:

black sample.py

Devuelve:

reformatted sample.py

All done! ✨ 🍰 ✨
1 file reformatted.

Y transforma sample.py:

class Car:
  def __init__(self, brand: str, color: str) -> None:
      self.brand = brand
      self.color = color

  def __str__(self) -> str:
      return f"Car(brand: {self.brand}, color: {self.color})"

Configuración recomendada

[black]
line-length = 100
skip-magic-trailing-comma = true

Nota: Skip-magic-trailing-comma: para evitar que una colección se divida en un elemento por línea

Flake8

Flake8 es una herramienta que integra pycodestyle, pyflakes, y mccabe

  • Pycodestyle: detecta cualquier error relacionado con el formato en el código, en relación con el cumplimiento de PEP8. A continuación, se presenta una lista no exhaustiva de errores:

E para códigos de error y W para advertencias

  • E1: para errores de indentación.
  • E2: para errores de espacio en blanco.
  • E7: para errores de sentencia
  • W6: para advertencias de obsolescencia
  • Pyflakes: detecta cualquier inconsistencia en el código. A continuación, se presenta una lista no exhaustiva de errores:

F para códigos de error

  • F4: para errores de importación
  • F5: para errores de formato
  • F6: para errores de asignaciones/comparaciones incompatibles
  • F7: para errores de sintaxis
  • F8: para errores relacionados con variables
  • Mccabe: detecta errores de complejidad.

Código de error: C (siempre se lanza el error C901 por violación de complejidad)

En este sample.py:

import numpy as np
a=math.cos(math.pi)

Al ejecutar:

flake8 sample.py

Se obtienen los siguientes errores:

sample.py:2:1: F401 'numpy as np' imported but unused
sample.py:3:2: E225 missing whitespace around operator
sample.py:3:3: F821 undefined name 'math'
sample.py:3:12: F821 undefined name 'math'
sample.py:3:20: W291 trailing whitespace

Configuración recomendada

[flake8]
max-line-length = 100
extend-ignore = E203, E501
ban-relative-imports = parents

Ignoramos estos dos errores:

También disponemos de un conjunto de plugins de flake8 que se pueden instalar:

  • flake8-use-fstring: comprueba si hay % o .format y sugiere usar f-strings
  • flake8-print: comprueba si hay sentencias print
  • flake8-tidy-imports: escribe importaciones más ordenadas (prohíbe las importaciones de módulos padre y superiores, es decir, con más de un punto)

Isort

Isort es una herramienta que proporciona una utilidad de línea de comandos que ordena tus importaciones (alfabéticamente y separa las secciones en estándar, de terceros, propias y, finalmente, las importaciones de la carpeta local).

En este ejemplo:

import numpy as np
import cv2
from a import b
from c import f, e, d
from typing import List, Tuple, Dict, Any
import os
import json
from .z import x, y
from .w import u, v

Ejecutando:

isort sample.py

Transforma sample.py:

import json
import os
from typing import Any, Dict, List, Tuple

import cv2
import numpy as np
from a import b
from c import d, e, f

from .w import u, v
from .z import x, y

Configuración recomendada

[isort]
profile = "black"
line_length = 100

Necesitamos configurar el perfil a “black” para evitar interacciones negativas entre las dos herramientas.

Mypy

Mypy es un verificador de tipos estático. Para usarlo, necesitas tipar tus funciones y variables. Puedes configurar su nivel de rigurosidad si aún quieres resolver el tipo dinámicamente en algunas partes de tu código, pero ten en cuenta que tener un proyecto con tipado estático mejora la productividad y la claridad, y añade barreras de seguridad por todas partes para que puedas detectar errores incluso antes de ejecutar tu código.

En este ejemplo:

from typing import List, Tuple

a: str = "abc"
b: int = 3

c = a + b

d: List[int] = []
d.append(a)
d.append((a, b))

e: Tuple[int, int]
e = (a, b)

def sample_function(x: int, y: int) -> Tuple[int, List[int], str]:
  pass

e = sample_function(a, b)

Ejecutando:

mypy sample.py

Devoluciones:

sample.py:6: error: Unsupported operand types for + ("str" and "int")
sample.py:9: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"
sample.py:10: error: Argument 1 to "append" of "list" has incompatible type "Tuple[str, int]"; expected "int"
sample.py:13: error: Incompatible types in assignment (expression has type "Tuple[str, int]", variable has type "Tuple[int, int]")
sample.py:18: error: Incompatible types in assignment (expression has type "Tuple[int, List[int], str]", variable has type "Tuple[int, int]")
sample.py:18: error: Argument 1 to "sample_function" has incompatible type "str"; expected "int"
Found 6 errors in 1 file (checked 1 source file)

Configuración recomendada

[mypy]
python_version = "3.8"
exclude = ["tests"]
# --strict
disallow_any_generics = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
implicit_reexport = false
strict_equality = true
# --strict end

Optamos por exigir la tipificación solo para el código de producción y no para las pruebas.

Para una nueva base de código, deberías añadir las opciones estrictas pero para código heredado, deberías añadir las opciones de mypy de forma iterativa.

Pydocstyle

Pydocstyle es una herramienta que verifica el cumplimiento de la mayoría de PEP 257 en relación con tus docstrings. Un docstring es una herramienta esencial para documentar tu código. Optamos por adoptar la convención de estilo de Google.

Implementa diferentes grupos de errores:

  • D1: para Docstrings Faltantes
  • D2: para Problemas de Espacios en Blanco
  • D3: para Problemas de Comillas
  • D4: para Problemas de Contenido de Docstring

Configuración recomendada

[pydocstyle]
convention = "google"

En el ejemplo anterior sin documentar:

class Car:
  def __init__(self, brand: str, color: str) -> None:
      self.brand = brand
      self.color = color

  def __str__(self) -> str:
      return f"Car(brand: {self.brand}, color: {self.color})"

Ejecutando:

pydocstyle sample.py

Devuelve:

sample.py:1 at module level:        D100: Missing docstring in public modulesample.py:1 in public class `Car`:        D101: Missing docstring in public classsample.py:2 in public method `__init__`:        D107: Missing docstring in __init__sample.py:6 in public method `__str__`:        D102: Missing docstring in public method

Deberíamos tener:

"""A one line summary of the module or program, terminated by a period."""


class Car:
    """Summary of class here.

    Longer class information...
    """

    def __init__(self, brand: str, color: str) -> None:
        """Inits Car with blah.
        Args:
            brand: A string with the car brand.
            color: A string with the car color.        """
        self.brand = brand
        self.color = color

    def __str__(self) -> str:
        """Performs operation blah."""
        return f"Car(brand: {self.brand}, color: {self.color})"

Darglint

Darglint es una herramienta para verificar que el docstring coincide con la implementación de la función o método a lo largo de todo su ciclo de vida. Evita tener documentación obsoleta cuando la firma ha cambiado. Es mejor cuando se usa en combinación con un verificador de estilo de docstrings como pydocstyle.

Implementa diferentes grupos de errores:

  • DAR0: Sintaxis, formato y estilo
  • DAR1: Sección de argumentos
  • DAR2: Sección de retorno
  • DAR3: Sección de valores generados
  • DAR4: Sección de excepciones
  • DAR5: Sección de variables

En el ejemplo anterior documentado:

"""A one line summary of the module or program, terminated by a period."""


class Car:
    """Summary of class here.

    Longer class information...
    """

    def __init__(self, brand: str, color: str) -> None:
        """Inits Car with blah.
        Args:
            brand: A string with the car brand.
            color: A string with the car color.        """
        self.brand = brand
        self.color = color

    def __str__(self) -> str:
        """Performs operation blah."""
        return f"Car(brand: {self.brand}, color: {self.color})"

Ejecutando:

darglint sample.py

Devuelve:

sample.py:str:20: DAR201: - return

Deberíamos tener:

"""A one line summary of the module or program, terminated by a period."""


class Car:
    """Summary of class here.

    Longer class information...
    """

    def __init__(self, brand: str, color: str) -> None:
        """Inits Car with blah.

        Args:
            brand: A string with the car brand.
            color: A string with the car color.
        """
        self.brand = brand
        self.color = color

    def __str__(self) -> str:
        """Performs operation blah.

        Returns:
            str: object representation
        """
        return f"Car(brand: {self.brand}, color: {self.color})"

Integración

Nos gustaría reunir todas estas configuraciones de herramientas en un solo archivo. Para el almacenamiento de la configuración, nuestra recomendación es usar .flake8 + pyproject.toml.

[flake8]
max-line-length = 100
extend-ignore = E203, E501
ban-relative-imports = parents
[tool.black]
line-length = 100
skip-magic-trailing-comma = true

[tool.isort]
profile = "black"
line_length = 100

[tool.mypy]
python_version = "3.8"
exclude = ["tests"]
# --strict
disallow_any_generics = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
implicit_reexport = false
strict_equality = true
# --strict end

[tool.pydocstyle]
convention = "google"

Luego, si queremos ejecutarlos todos usando un solo comando, tenemos diferentes opciones:

  • Script Sh o Makefile

Necesitamos definir una lista de dependencias de desarrollo como en requirements-dev.txt.

isort~=5.10.1
black~=22.3.0
flake8~=4.0.1
flake8-print==4.0.0flake8-use-fstring==1.3flake8-tidy-imports==4.7.0pydocstyle[toml]=6.1.1mypy=0.960darglint~=1.8.1

Creamos un archivo lint.sh (darglint es lanzado automáticamente por flake8 cuando está instalado en el mismo entorno, por lo que no es necesario especificarlo)

#!/bin/bash
set -o pipefail

flake8
black .
isort .
mypy .
pydocstyle

Y podemos ejecutarlo:

./lint.sh

Si quieres usar un Makefile:

lint:
  flake8
  black .
  isort .
  mypy .
  pydocstyle

Luego ejecuta:

make lint
  • Pre-commit [Preferido]

Pre-commit es otra alternativa para agrupar todas las herramientas y ejecutarlas dentro de un entorno cerrado y dedicado, por lo que ni siquiera las necesitas en un entorno de desarrollo.

pip install pre-commitputtingpre-commit run -a my-hook

Además, puede interactuar con el hook de git en relación con los archivos modificados en el commit, push, etc., antes de la subida al almacenamiento remoto de código y evitar saturar la CI por problemas de lint y estilo.

pre-commit install
# follow by git add/commit/push which trigger pre-commit

Configuración recomendada

.pre-commit-config.yaml

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.2.0
    hooks:
      - id: check-ast
      - id: check-builtin-literals
      - id: check-docstring-first
        exclude: tests
      - id: check-merge-conflict
      - id: check-yaml
      - id: check-toml
      - id: debug-statements
      - id: end-of-file-fixer
      - id: trailing-whitespace
  - repo: https://github.com/pycqa/isort
    rev: 5.10.1
    hooks:
      - id: isort
  - repo: https://github.com/psf/black
    rev: 22.3.0
    hooks:
      - id: black
  - repo: https://github.com/pycqa/flake8
    rev: "4.0.1"
    hooks:
      - id: flake8
        additional_dependencies:
          - flake8-use-fstring
          - flake8-print
          - flake8-tidy-imports
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: "v0.960"
    hooks:
      - id: mypy
        exclude: tests
        args: []
  - repo: https://github.com/pycqa/pydocstyle
    rev: 6.1.1
    hooks:
      - id: pydocstyle
        exclude: tests
        additional_dependencies: [toml]
  - repo: https://github.com/terrencepreilly/darglint
    rev: v1.8.1
    hooks:
    - id: darglint

Conclusión

Una vez que hayas llegado a una configuración y hayas elegido un método de integración, tu flujo de trabajo será estable y productivo. Además, lo más probable es que no evolucionen mucho en el futuro, por lo que mantener una base de código con estas herramientas es una obviedad.

Además, cuando se utiliza desde el inicio de un proyecto, el coste es casi nulo, pero cuando se aplica a una base de código existente, puede llevar bastante tiempo resolver todos los errores. ¡Cuanto antes empieces, mejor!

Imagen destacada de Kuznetcov_Konstantin

Acerca de

Desde fotos sencillas hasta PDF complejos o archivos manuscritos, la API de Mindee convierte los datos de tus documentos en JSON estructurado con alta fiabilidad. No se requiere entrenamiento de modelos. Compatible con cualquier alfabeto y cualquier idioma.

,
,

Punto clave

Punto clave