PEPs clave que todo pythonista debería haber leído

Los PEPs (Python Enhancement Proposals) son el lugar donde Python se piensa antes de existir. No son documentación de usuario ni tutoriales: son el registro histórico de por qué el lenguaje tomó cada decisión de diseño. Leer un PEP completo te da algo que ningún resumen de terceros puede darte: la motivación original, los trade-offs que se descartaron y el contexto en que la decisión tenía sentido. Cuando lees un artículo de blog que explica PEP 484, ya pasó por un filtro de interpretación. El PEP original no.

La estructura de un PEP es siempre la misma: encabezado con metadatos, Abstract (qué propone), Motivation (por qué el estado actual es insuficiente), Specification (la definición técnica precisa), Backwards Compatibility, y a veces Examples. Cuando encuentres algo confuso en typing o en packaging, ve directo a la sección Specification del PEP relevante. Es más rápido que buscar en Stack Overflow.

Vamos a ver los PEPs que realmente cambian cómo piensas y cómo escribes Python.


PEP 8: consistencia por encima del gusto

PEP 8 no es una lista de preferencias estéticas. Es una declaración de que la legibilidad es una propiedad del código compartido, no del código personal. La parte que la gente suele ignorar está en la introducción misma: “A style guide is about consistency. Consistency within a project is more important than consistency with this guide.”

Esto significa que flake8 o ruff no son árbitros absolutos; son herramientas para que un equipo no tome decisiones cosméticas en cada revisión de código. El costo real de ignorar PEP 8 no es estético, es cognitivo: cada desviación es ruido que el lector tiene que filtrar antes de entender la lógica.

PEP 20: el Zen como criterio de diseño

import this

Esto imprime los 19 aforismos del Zen de Python. Pero su valor no está en memorizarlos, sino en usarlos como criterio cuando tienes dos implementaciones igualmente correctas. “Explicit is better than implicit” es la razón por la que __init__.py existió durante años y por la que los imports relativos implícitos desaparecieron. “Errors should never pass silently” es por qué un except: pass en producción es un olor de código serio.

PEP 257: docstrings que las herramientas pueden leer

Un docstring es una cadena literal como primera instrucción de un módulo, clase o función. PEP 257 define cómo escribirlos de forma que pydoc, Sphinx y los IDEs puedan procesarlos de manera predecible.

La distinción más importante que establece: docstring de una línea vs. docstring multilínea. Para funciones simples, una línea basta. Para APIs públicas, el formato multilínea con resumen, línea en blanco y descripción extendida es lo que Sphinx transforma en documentación navegable.

PEP 484: el sistema de tipos tiene fundación escrita

PEP 484 introdujo las type hints como sintaxis oficial y definió las reglas del juego: las anotaciones son metadata, no enforcement en runtime (salvo que uses beartype o similar). Esto es una decisión de diseño deliberada: Python no quería romper código existente ni convertirse en Java.

Lo que PEP 484 sí establece es el contrato para herramientas como mypy, pyright y pytype: pueden analizar código estáticamente usando esas anotaciones. La sección de motivación del PEP explica explícitamente que el objetivo era hacer posible la verificación estática sin sacrificar el dinamismo del lenguaje.

PEP 517/518: packaging sin supuestos implícitos

Antes de PEP 517 y 518, pip asumía que todo proyecto usaba setuptools y que podía importarlo para construir el paquete. Eso creaba una dependencia circular implícita y hacía imposible usar otros build backends como flit o hatch.

PEP 518 introdujo pyproject.toml con la tabla [build-system], que le dice a pip qué herramienta necesita instalar antes de intentar construir. PEP 517 definió la interfaz estándar que esa herramienta debe implementar. Juntos rompieron el monopolio de setuptools de forma compatible hacia atrás.

PEP 621: metadatos de proyecto estandarizados

PEP 621 movió los metadatos del proyecto (nombre, versión, dependencias, autores) a la tabla [project] dentro de pyproject.toml, en un formato que cualquier build backend puede leer sin saber cómo funciona internamente cada uno. Antes, cada herramienta tenía su propia sección con sus propias claves.


Ahora vamos al código. Este ejemplo muestra un módulo con todas las convenciones aplicadas correctamente, porque la mejor forma de ver PEP 8, 257 y 484 juntos es en código real que podrías encontrar en producción:

"""
Utilidades para manipulación de colecciones con soporte de tipos.

Este módulo demuestra las convenciones de PEP 8, PEP 257 y PEP 484
aplicadas en conjunto sobre código real.
"""

from __future__ import annotations  # PEP 563: evaluación diferida de anotaciones

from collections.abc import Callable, Sequence
from typing import TypeVar

T = TypeVar("T")
U = TypeVar("U")


def chunk(sequence: Sequence[T], size: int) -> list[list[T]]:
    """Divide una secuencia en sublistas de longitud fija.

    El último chunk puede ser más corto si la longitud de la secuencia
    no es divisible entre ``size``.

    Args:
        sequence: Cualquier secuencia indexable (lista, tupla, str…).
        size: Tamaño máximo de cada chunk. Debe ser mayor que cero.

    Returns:
        Lista de sublistas. Si ``sequence`` está vacía, retorna ``[]``.

    Raises:
        ValueError: Si ``size`` es menor o igual a cero.

    Examples:
        >>> chunk([1, 2, 3, 4, 5], 2)
        [[1, 2], [3, 4], [5]]
    """
    if size <= 0:
        # Fallo explícito: PEP 20 — "Errors should never pass silently"
        raise ValueError(f"size debe ser > 0, recibido: {size}")

    return [list(sequence[i : i + size]) for i in range(0, len(sequence), size)]


def flat_map(func: Callable[[T], Sequence[U]], items: Sequence[T]) -> list[U]:
    """Aplica ``func`` a cada elemento y aplana el resultado un nivel.

    Args:
        func: Función que recibe un elemento y retorna una secuencia.
        items: Secuencia de entrada.

    Returns:
        Lista plana con todos los resultados concatenados.

    Examples:
        >>> flat_map(lambda x: [x, x * 2], [1, 2, 3])
        [1, 2, 2, 4, 3, 6]
    """
    # La comprensión anidada es más legible que reduce() aquí — PEP 20
    return [element for item in items for element in func(item)]


# pyproject.toml mínimo que sigue PEP 517/518 y PEP 621:
#
# [build-system]
# requires = ["hatchling"]          # PEP 518: dependencias de build declaradas
# build-backend = "hatchling.build" # PEP 517: interfaz estándar del backend
#
# [project]                         # PEP 621: metadatos estándar
# name = "mi-paquete"
# version = "0.1.0"
# dependencies = ["requests>=2.28"]

Qué hace este código y por qué está estructurado así

El módulo abre con un docstring de módulo siguiendo PEP 257: resumen breve, línea en blanco, descripción extendida. Sphinx lo tomará como la página principal del módulo en la documentación generada.

from __future__ import annotations habilita la evaluación diferida de anotaciones (PEP 563), lo que permite usar list[list[T]] sin importar List de typing. En Python 3.10+ esto es innecesario para tipos built-in, pero en 3.9 sigue siendo útil y en 3.8 es obligatorio si quieres usar la sintaxis moderna.

Los TypeVar T y U siguen la convención de PEP 484: letras únicas en mayúscula, definidos a nivel de módulo. chunk retorna list[list[T]], lo que le dice a mypy que si pasas Sequence[int], recibes list[list[int]]. La inferencia de tipos viaja a través de la función sin que el llamador tenga que hacer cast manual.

flat_map usa Callable[[T], Sequence[U]] con dos TypeVars distintos, modelando correctamente que la función transforma el tipo de entrada. Si hubieras usado T para ambos, mypy rechazaría usos legítimos donde el tipo de entrada y salida difieren.

El raise ValueError en chunk ilustra el principio del Zen: en lugar de retornar silenciosamente una lista vacía cuando size=0, fallamos de forma explícita con un mensaje que incluye el valor recibido. Eso es diagnóstico gratuito en producción.

El bloque comentado de pyproject.toml muestra cómo PEP 517, 518 y 621 trabajan juntos: [build-system] le dice a pip qué instalar antes de construir (PEP 518) y qué interfaz usar (PEP 517); [project] centraliza los metadatos en un formato neutro que cualquier backend entiende (PEP 621). Si cambias de hatchling a flit, los metadatos en [project] no cambian.

Errores que debes conocer

Error: ignorar que PEP 484 no hace enforcement en runtime y asumir que las anotaciones validan los argumentos.

# ❌ Wrong
def add(a: int, b: int) -> int:
    return a + b

add("hello", "world")  # No lanza error, retorna "helloworld" en runtime
# ✅ Right
from typing import get_type_hints
# Las anotaciones son para herramientas estáticas (mypy, pyright).
# Si necesitas validación en runtime, usa una librería como `beartype`
# o valida explícitamente en el cuerpo de la función.

def add(a: int, b: int) -> int:
    if not isinstance(a, int) or not isinstance(b, int):
        raise TypeError(f"Se esperaban int, recibidos: {type(a)}, {type(b)}")
    return a + b

Las anotaciones son contratos para el análisis estático; runtime no las aplica salvo instrumentación explícita.

Error: mezclar setup.py/setup.cfg con pyproject.toml parcialmente, sin declarar el [build-system].

# ❌ Wrong — pyproject.toml sin [build-system]
[project]
name = "mi-paquete"
version = "0.1.0"
# ✅ Right
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mi-paquete"
version = "0.1.0"

Sin [build-system], pip no sabe qué herramienta usar y cae en el fallback implícito de setuptools, que puede no estar disponible ni ser la versión correcta en todos los entornos.


La diferencia entre un pythonista que sigue convenciones porque “así lo aprendí” y uno que las aplica con criterio está exactamente en haber leído, aunque sea una vez, la sección Motivation de los PEPs que gobiernan su trabajo diario.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio