Estructura de directorios de una librería Python

En Python hay fundamentalmente dos recomendaciones distintas de cómo estructurar los directorios (project layout) de una librería o herramienta, que queramos distribuir.

No incluyo en este artículo estructuras para proyectos. Por ejemplo una aplicación web monorepo con el backend y el front en el mismo repositorio. Proyectos que quieren desarrollar distintas herramientas bajo el mismo namespace. U otros casos particulares.

Sin directorio raíz

Es la estructura más sencilla, especialmente para proyectos pequeños o scripts individuales.

En algunos artículos llaman a esta estructura flat layout. Esto induce a error, porqué flat layout es el nombre más usado para la siguiente estructura que veremos. El artículo es antiguo pero en realpython llaman a esto one-off-script

my_project/
├── __init__.py
├── my_project.py
├── requirements.txt
├── tests/
│   └── test_my_project.py
└── README.md

Tiene a favor que es simple, no requiere subdirectorios, ni siquiera el fichero __init__.py, y todo está a la vista.

Pero nunca deberíamos usarlo. El boilerplate de generar una estructura mejor tiene un coste cero si usamos un cookiecutter, y con esta estructura enseguida aparecen problemas de cómo organizar el código, empaquetar el proyecto y a la mínima tendremos problemas con los import.

Módulo en la raíz (flat layout)

También llamado adhoc layout.

Es probablemente la más usada. El paquete (directorio) principal del proyecto está directamente en la raíz.

my-project/
├── my_project/
│   ├── __init__.py
│   ├── main.py
│   └── utils.py
├── tests/
│   ├── test_main.py
│   └── test_utils.py
├───.github
│   └───workflows
├───docs
│   └───mkdocs.yml
├── pyproject.toml
└── README.md

Pros:

  • Más directa que src layout al eliminar un nivel de anidación.
  • No es necesario instalar (pip install) para ejecutar el código.
  • Muchos proyectos siguen este patrón.

Contras:

  • Riesgo de import locales: Existe mayor riesgo de que Python importe el módulo en desarrollo (en el source tree digamos) en lugar del módulo instalado. Esto puede llevar a errores difíciles de detectar a la hora de ejecutar los tests, empaquetar e instalar el proyecto.
  • No es tan explícito como src layout sobre lo que se empaqueta y lo que no.

Subdirectorio src/ (src layout)

Es una de las más recomendadas.

my-project/
├── src/
│   └── my_project/
│       ├── __init__.py
│       ├── main.py
│       └── utils.py
├── tests/
│   ├── test_main.py
│   └── test_utils.py
├───.github
│   └───workflows
├───docs
│   └───mkdocs.yml
├── pyproject.toml
└── README.md

Pros:

  • Al colocar el código en src/my_project/, el intérprete de Python siempre importará la versión instalada del paquete, no la versión en desarrollo en el directorio raíz. Esto ayuda a asegurar que el comportamiento en desarrollo sea el mismo que en producción.
  • Claridad de empaquetado: Deja claro qué partes del proyecto son el código fuente que se va a empaquetar y distribuir. Otros archivos (tests, documentación, configuración de desarrollo) se mantienen fuera del paquete.
  • Fomenta buenas prácticas: Al requerir una "instalación" (aunque sea editable) para que el código funcione correctamente en desarrollo.
  • Mejor aislamiento de pruebas: Los tests se colocan fuera del directorio src/, lo que significa que no se incluyen en el paquete distribuido y evita dependencias de prueba en el código de producción.
  • Es el que prefiere uv

Contras:

  • Requiere más boilerplate. Para ejecutar el código se requiere una instalación editable o ajustar el PYTHONPATH.
  • La carpeta src/ adicional puede ser redundante.

Variantes y Comentarios

  • Alguna gente prefiere introducir un nivel extra de indirección donde por ejemplo incluir venv
  • Cuando escojamos un nombre para el proyecto, si vamos a publicarlo deberíamos comprobar que el nombre no está cogido en PyPy.
  • Separadores de nombre. Los módulos y paquetes de Python no admiten hyphens, -, sólo underscore, _. Es habitual usar kebab-case (hyphenated) para el nombre del repositorio (carpeta raíz) y snake_case (underscored) para los paquetes y módulos.
  • Alguna gente considera que lo correcto es empaquetar los tests dentro del distributable package. Yo no.

Mi preferida

Mi preferida es scr layout.

Las funcionalidades de los editores (compact folders, quick open) y herramientas cómo uv o cookiecutter reducen las incomodidades que introduce esta estructura.

Visual y organizativamente me gusta cómo queda la raíz del proyecto.

Me molesta mucho perder tiempo en los "esto no puede estar pasando", y esta estructura minimiza esos subtle bugs

Creo que el simple hecho de preocuparse de entenderla ayuda a que la gente no se líe a tocar sys.path, o probar con python -m a ver si sus import funcionan de esa forma. Aunque de "los scripts dentro de proyectos" hablaremos en otro artículo.

Referencias

Escoger un Python Language Server

En un artículo anterior definimos que era Language Server Protocol y cómo funcionaban los Language Server.

En este nos centramos en escoger un Language Server para Python.

Criterios

Para escoger algo, hay que definir que criterios priorizamos. En el caso de un LS o herramientas relacionadas los criterios podrían ser:

  • Rendimiento. Queremos que la respuesta a la mayoría de acciones sea casi inmediata.
  • Software libre. Preferimos que la implementación sea software libre.
  • Capacidades. Cuantas más capacidades (que refactorings, ...) nos ofrezca el LS o una determinada combinación de LS mejor.
  • Fragmentación. Si un LS o herramienta integrada nos da todo lo que necesitamos, mejor que si tenemos que preocuparnos por ver los changelog de 5 herramientas distintas.
  • Mantenido. Con tracción en la comunidad, ...
  • Multiples entornos. Al menos el lint y el format debe ser consistente entre múltiples entornos: editor (uses NeoVim, PyCharm o vscode), pre-commit y ci. Cómo gestione un refactoring o la navegación no es necesario que sea consistente.

Opciones

Pylance / Pyright

Pylance es el LS privativo de Microsoft para Python, basado en la librería / LS (1) libre de tipado estático Pyright.

  1. 🙋‍♂️ La distinción entre librería, LS, ... no siempre es clara

Microsoft mantenía un LS libre python-language-server, pero lo discontinuaron en favor de Pylance. Pylance sólo se puede usar con los productos de Microsoft (vscode) y no con editores derivados como Cursor o VSCodium, ni alternativos como NeoVim.

Pyright es una buena librería para tipado estático y si bien se puede usar como LS en cualquier editor, no es una buena alternativa porqué carece de funcionalidades básicas como añadir imports de forma automática.

Este dúo tiene más cosas "molestas":

  • Están escritos en typescript. No es algo malo de por si, pero no deja de ser extraño
  • Las capacidades de refactoring son malas. Rename, Extract Variable, Move Symbol, y a veces se consigue usar el Extract Method.
  • Privativo y no usable en otros editores

basedpyright

basedpyright es un fork libre de Pyright que incorpora funcionalidades de Pylance.

Más o menos los mismos comentarios que para Pylance/Pyright

Personalmente he tenido malas experiencias al usarlo en Cursor. Probablemente se deba a una mala configuración, pero tengo la sensación de que se queda trabado a menudo.

Jedi / Rope

Ni Jedi, ni Rope son LS. Ambas son dos librerías históricas del ecosistema Python. Rope se centra en refactoring y Jedi en análisis estático, autocompletado y navegación.

Ninguna de las dos proporciona formatting, linting o tipado lo que debería alcanzarse mediante otras librerías cómo mypy (tipado) y ruff (formatting y linting).

Si bien no son un LS, si que son la base para varios Language Server.

python-lsp-server

python-lsp-server se puede decir que es el LS que proviene de la comunidad. Es mantenido por el equipo de Spyder IDE.

Tras instalarlo proporciona el LS bajo el comando pylsp.

Está basado en jedi que proporciona: Completions, Definitions, Hover, References, Signature Help, and Symbols. Si están instaladas en el entorno se integra de forma nativa con otras librerías cómo rope (capacidades básicas) y flake8. Y el propio LS admite plugins para integrarse con más librerías: mypy, ruff, más refactorings de rope, ...

Tiene dos problemas grandes:

Al menos en proyectos pequeños, parece ser que con proyectos grandes puede ser lento, no es una mala opción (si hubiera plugin), y es la única alternativa ahora mismo que proporciona un refactoring decente. La mejor forma de probarlo es con Zed.

Notas sobre la configuración no muy ordenadas

¿Tiene sentido instalar pylsp-ruff o mejor directamente ruff?. En principio ruff mejor individual

pip install python-lsp-server pylsp-rope pylsp-mypy

# ¿Mejor con wesockets?
# pip install 'python-lsp-server[websockets]'
# pylsp --ws --port [port]

Configuración para usar el plugin externo de rope

pylsp.plugins.pycodestyle.enabled = false
pylsp.plugins.flake8.enabled = false
pylsp.plugins.autopep8.enabled  = false
pylsp.plugins.mccabe.enabled    = false
pylsp.plugins.pycodestyle.enabled   = false
pylsp.plugins.pyflakes.enabled  =false
pylsp.plugins.pylint.enabled    =false
pylsp.plugins.yapf.enabled  = false

# set pylsp.plugins.rope_autoimport.enabled to true
# This enables both completions and code actions. You can switch them off by setting pylsp.plugins.rope_autoimport.completions.enabled and/or pylsp.plugins.rope_autoimport.code_actions.enabled to false

pylsp.plugins.rope_autoimport.enabled   = ???
pylsp.plugins.rope_completion.enabled   = ???

pylsp.plugins.rope_rename.enabled = false
pylsp.plugins.jedi_rename.enabled = false
pylsp.plugins.pylsp_rope.rename = true

Configuración de rope en pyproject.toml

# https://rope.readthedocs.io/en/latest/configuration.html
[tool.rope]
split_imports = true
autoimport.aliases = [
    ['dt', 'datetime'],
    ['mp', 'multiprocessing'],
]

Pyrefly

Pyrefly es el último en llegar al mercado. CLI, type checker y LS mantenido por Meta.

Es rápido, tiene plugins para Code y derivados y funciona bien.

Lo malo es que no tiene ningún refactoring en este momento.

Ruff

Ruff tiene desde hace tiempo un LS integrado para el formatting y linting.

En este momento están en proceso de integrar dentro de la propia herramienta el type checker y convertirlo en un posible substituto completo para los otros LS.

PyCharm

No cumple nuestros requisitos iniciales, pero a día de hoy es el IDE con mejor soporte para Python.

Merece la pena probarlo.

Otras alternativas

  • jedi-language-server es una propuesta minimalista que sólo implementa expone las capacidades que tenga jedi
  • anakin-language-server es poco popular. Se basa en jedi y permite integración con mypy, yapf, pyflakes y pycodestyle
  • PyDev on Visual Studio Code. PyDev es un plugin para trabajar con Python en Eclipse. Al parecer también funciona cómo una extensión para VSCode, pero necesita tener Java instalado.
  • palantir/python-language-server Si mantenimiento pylsp es un fork de este.
  • pylyzer un LS y type checker escrito en rust. Poco mantenimiento.

Otros LS específicos

Cómo se ve en el caso de ruff, un LS no tiene porqué proporcionar las funcionalidades completas de un lenguaje. Hay LS que cubren aspectos o librerías muy específicas:

  • https://github.com/joshuadavidthomas/django-language-server
  • https://www.fourdigits.nl/blog/django-template-lsp/

Conclusiones

PyCharm aparte, y con ánimo de polemizar se puede decir que no hay un soporte tan bueno cómo podría esperarse para Python en los IDEs.

Personalmente en este momento estoy usando Pyrefly con el plugin para Cursor como LS principale, y ruff con su plugin cómo el LS secundario que se encarga del formatting y linting.

Las pensiones cómo inversión

Las pensiones suelen ser consideradas cómo un gasto del ~15% del PIB. Pero qué pasa si en lugar de gasto lo llamamos inversión y tratamos de evaluar los ingresos que produce esa inversión.

Dos investigadores concluyen que gracias a los impuestos IVA, IRPF de trabajadores, ... vinculados al gasto producido con el dinero de las pensiones sale una relación 1 a 1,1. Es decir, cada 1€ gastado en pensiones genera 1,1€. Me faltan conocimientos para interpretar el estudio, y lógicamente no se puede llevar al extremo, el estado no tiene infinito dinero para gastar aunque la devolución sea mayor que el gasto. Pero en esta época de campaña anti pensiones, me gusta que haya otra forma de entender las cosas (recordemos el peligro de una sola historia).

Referencias

Language Server Protocol

Language Server Protocol (LSP) define un conjunto de mensajes JSON-RPC y estructuras de datos que permiten que el servidor (Language Server) exponga que capacidades (language services) ofrece y un cliente (generalmente un IDE) las consuma.

Estas capacidades son las habituales para una herramienta de programación: autocomplete, refactoring, navegación (go to definition, ...), type checking, ...

Este no es un artículo en profundidad sobre herramientas de desarrollo, me he tomado algunas licencias poéticas porqué sólo quiero situar el contexto para artículos posteriores.

Antes de LSP

Se puede argumentar que, antes, la diferencia principal entre un IDE y un editor, era que el IDE incluía de serie y dentro de su propio código funcionalidades que interpretaban el texto cómo "código fuente" y no cómo otro texto cualquiera. Así proporcionaban capacidades de formateado del código, análisis estático (lint), autocomplete, moverse entre la definición y el uso de un símbolo, ...

Los editores en cambio no tenían estas capacidades de forma nativa. Pensemos en PyCharm o PyDev (alguien se acuerda de la versión de Eclipse para Python), frente a Vim, Emacs o incluso Atom.

Lo que había era librerías (o aplicaciones) que daban estas capacidades en el "terminal". En el mundo Python teníamos (tenemos) cosas cómo jedi (para autocomplete y navegación), rope (para refactorings y autocomplete), flake8 (para linting), yapf (para formateado), ...

La solución natural era que los editores tuvieran plugins que usando la API nativa de cada editor se comunicaran de alguna forma no estándar con la herramienta de turno. Así aparecieron millones de plugins. Podíamos tener cosas cómo (nombres inventados) atom-yapf (que sólo daba formato con yaps), atom-python-linter (que te dejaba escoger entre flake8 o pylint o los dos a la vez), atom-formatter (que vale para distintos lenguajes), ...

Mantener ese ecosistema tanto a nivel desarrollo cómo a nivel uso, que funciona con que, que instala el plugin y que tengo que instalar yo, ... era un caos. Y cada implementación era específica para un servidor y una librería.

Porqué un linter es mejor que un IDE

Hay acciones cómo el autocomplete, navegación o refactoring que sólo tiene sentido en el entorno de desarrollo. Cada persona del equipo decide que herramientas se le adaptan mejor.

Pero también hay acciones, lint y format sobre todo, que son decisiones de equipo y hay valor en que estén disponibles en cualquier entorno: editor, pre-commit, ci, ...

Por ello resulta útil que ciertas herramientas sean independientes del IDE y se puedan usar vía plugin o LSP.

Nace LSP

En 2016 de la mano de VSCode, Microsoft publica LSP que se acaba convirtiendo en un estándar abierto de facto de la industria.

La principal ventaja es desacoplar las herramientas de los editores:

  • Consistencia en diferentes entornos: Las mismas reglas pueden aplicarse en distintos editores, pre-commit, ...
  • Evita duplicación de esfuerzos
    • Los desarrolladores de lenguajes solo necesitan crear un servidor de lenguaje y funcionará con cualquier editor
    • Los editores sólo necesitan implementar soporte para LSP y no para cada herramienta por separado
  • No sólo lenguajes de programación. Un LSP puede servir para DSL y otros, por ejemplo la configuración de Ansible.
  • Arquitectura cliente servidor. El servidor ni siquiera tiene que correr en el mismo entorno que el cliente

Descripción de alto nivel

Este artículo de VSCode nos da una buena idea del funcionamiento a alto nivel:

Here’s an example of how a tool and a language server could communicate semantic information during a routine editing session:

The user opens a file (referred to as a document) in the tool: The tool notifies the language server that a document is open (didOpen) and that the information about that document is maintained by the tool in memory.

The user makes edits: The tool notifies the server about the document change (didChange) and the semantic information of the program is updated by the language server. As this happens, the language server analyses this information and notifies the tool with the errors and warnings (diagnostics) that are found.

The user executes 'Go To Definition' on a symbol: The tool sends a definition request to the server. The server responds with a uri of the document that holds the definition and the range inside the document. Based on this information, the tool can open the corresponding document at the defining position.

The user closes the document (file): A didClose notification is sent from the tool, informing the language server that the document is now no longer in memory and instead maintained by (i.e. stored on) the file system.

This communication, which takes place over JSON-RPC, happens many times over the course of a typical session.

Capacidades del LSP y el Editor

En este artículo se describe bastante bien el proceso de inicialización e intercambio de capacidades.

Ni el editor, ni el LS tienen porqué implementar todas las posibles capacidades definidas en el estándar. Cuando arranca el servidor se lleva a cabo un intercambio de mensajes en el que cada parte dice lo que quiere y lo que puede hacer.

Imaginemos por ejemplo un LS puede tener la capacidad de hacer un refactoring de Extract function pero en la GUI no hay ese menú.

Tenemos un ejemplo claro en el mundo Python, ruff incorpora su propio LS, pero (a día de hoy) sólo provee capacidades de format y lint, y no por ejemplo de refactoring. El editor (y la desarrolladora que tiene que configurar el editor) es responsable de no liarse y coordinar que el refactoring debe hacerse a través de un LS (PyLance, python-lsp-server, ...) y el formateado a través de ruff.

Fragmentación

Los LSP suenan al conocido comic de XKCD sobre los standards. Es una nueva tecnología y port tanto fragmenta más al mercado porqué nadie está obligado a usarla.

Sigue habiendo editores con plugins independientes, en Python hay un montón de LS: ruff, PyLance, BasedPyRight, jedi-language-server, python-lsp-server, PyDev.

Además en muchos casos los LS no implementan las capacidades por si mismos. Algunas las implementan y otras las delegan a otras herramientas de forma "nativa" o mediante plugins.

Por ejemplo PyLance es un LS privativo de Microsoft para Python que sólo funciona en VSCode (y no en otras versiones como Cursor o VSCodium). Muchas funcionalidades (syntax errors, ...) provienen de PyRight que es una librería libre también de Microsoft pero hay funcionalidades como los imports automáticos que están sólo en PyLance, y otras funcionalidades cómo formateado PyLance las ofrece a través de integración con yapf, ruff, ...

Conclusión

Entender cómo funcionan las herramientas que usamos para desarrollar no está de más, y los LS se han convertido en una de las más útiles.

Referencias

Vídeo: Deep Dive into LLMs like ChatGPT. By Andrej Karpathy

Andrej Karpathy fue uno de los fundadores de OpenAI y Director de IA en Tesla.

En este vídeo de tres horas y media desgrana todo el funcionamiento de un LLM, desde que descargas los datos de internet hasta que puedes conversar con ella. Si hace unas semanas enlazaba la interesante, pero complicada explicación de Stephen Wolfram este es un vídeo para todos los públicos.

Uno de los pocos momentos en que sale sale de la parte "técnica" es sobre la 1h:10min. Explica que tras el pre-training (entrenar al modelo con todo internet), se pasa al post-training. En el post-training "especialistas humanos" redactan conversaciones en base a unas ideas generales de "personalidad" que la empresa que construye el modelo quiere que tenga. Más o menos dice los siguiente:

lo que pasa cuando abres chatgpt y haces una pregunta, no es que una especie de IA mágica te responda, es un proceso estadístico en la que las respuestas se parecen a lo que unos editores humanos siguiendo las instrucciones de una empresa han decidido que es correcto.

Si preguntas "Cuales son los 5 lugares que no me puedo perder de París", la respuesta no se basa en una comparativa "científica" de que lugar es mejor y porqué, se parece más bien a la respuesta que estatisticamente habría respondido el editor.