Saltar a contenido

2025

AGENTS.md and the @ to reference documents

This article is just a quick experiment on:

  • The difference between referencing a document that the agent reads with the @ symbol in front or simply by including the path. @./docs/rule1.md vs simple ./docs/rule1.md.
  • How to split the rules of the LLM into different documents to avoid "burning" context".

The ideas are based on these two articles:

The first test is with gemini-cli, which by default does not use AGENTS.md. In this ticket, there are also people asking if the use of @ is inherent to the model or the tool.

mkdir -p /tmp/experiment/docs
cd /tmp/experiment
echo "Start all your answers with 'Hello fpuga'.

If the user ask any question about Spain read ./docs/foo.md
" > GEMINI.md
echo "Always add a joke at the end of your answers" > docs/foo.md

gemini "What is the capital of France"

>>> Hello fpuga
>>> Paris.

gemini "What is the capital of Spain"

>>> Hello fpuga
>>> Hello fpuga
>>> I need to find the capital of Spain. I will use a web search tool for this.
>>> Hello fpuga
>>> The capital of Spain is Madrid.
>>>
>>> Why did the invisible man turn down the job offer?
>>>
>>> He couldn't see himself doing it!

We see that when we don't use @, the tool/model only loads GEMINI.md, but it's able to load the additional rules when needed.

Let's try it with @.

echo "Start all your answers with 'Hello fpuga'.

If the user ask any question about Spain read @./docs/foo.md
" > GEMINI.md

gemini "What is the capital of France"

>>> Hello fpuga,
>>> The capital of France is Paris.
>>>
>>> Why don't scientists trust atoms? Because they make up everything!

And indeed, when we use @ it follows the references and loads them automatically into the context.

I replicated the experiment with Cursor and Cursor CLI, and in this case, the use of the @ symbol has no effect. It only reads foo.md when asked for Spain. I open a feature request about this.

Conclusions

  • It's a shame there isn't more standardization in how the tools work.
  • If the @ trick were standardized, it would prevent duplication. From AGENTS.md, we could link a simple CONTRIBUTING.md with rules like "Before committing, run ./scripts/format.sh and ./scripts/lint.sh; it must not return any errors." Or "All contributions must maintain test coverage above 75%."
  • The idea of ​​a rules index telling the model when to load additional documents is really good for avoiding context clutter.

Entry Point para una CLI Python

La flexibilidad del ecosistema Python le ha permitido convertirse en uno de los lenguajes más utilizados. El coste de esa flexibilidad es que en contra del Zen of Python

There should be one, and preferably only one, obvious way to do it.

incluso la gente con experiencia encuentra confusión y falta de ejemplos canónicos en aspectos que deberían ser obvios cómo el empaquetado.

En este artículo tocamos uno de esos aspectos, el entry point para herramientas de consola (ClI).

Breve descripción de las "piezas" que entran en juego

__main__.py

Cuando en un python package (directorio que tiene dentro ficheros python), hay un fichero con el nombre __main__.py y ejecutamos el módulo desde la línea de comandos (python -m my_cli), es ese fichero el que se ejecuta. De la documentación oficial:

The __main__.py file is used to provide a command-line interface for a package.

Si bien en este module no es necesario usar el idiom de __name__ == "__main__": es recomendable usarlo para evitar errores absurdos.

[proyect.scripts]

La sección [proyect.scripts] de pyproject.toml indica a la herramienta de instalación (pip, uv, ...) que debe crear un ejecutable con el nombre que se le indique apuntando (wrapper script, shim, ...) a un determinado módulo y función.

Es decir, es un mecanismo para la distribución de nuestra herramienta. Los usuarios no necesitarán ejecutar python -m my-cli, si no simplemente my-cli.

# El ejecutable `my-cli` apunta a la función `main()` de `my_cli/cli.py`
[project.scripts]
my-cli = "my_cli.cli:main"

Si instalamos en un entorno virtual el ejemplo anterior (python -m pip install -e .[all], uv sync, ...) se creará un fichero .venv/bin/my-cli con este contenido:

#!/tmp/example/my-cli/.venv/bin/python3
# -*- coding: utf-8 -*-
import sys
from my_cli.cli import main
if __name__ == "__main__":
    if sys.argv[0].endswith("-script.pyw"):
        sys.argv[0] = sys.argv[0][:-11]
    elif sys.argv[0].endswith(".exe"):
        sys.argv[0] = sys.argv[0][:-4]
    sys.exit(main())

Si se instala a nivel global sería parecido.

Fijémonos en que:

  • Apunta a una ruta de python absoluta. De modo que si lo ejecutamos fuera de un virtualenv /tmp/example/my-cli/.venv/bin/my-cli estará apuntando al python correcto.
  • Al activar un virtualenv se modifica "${PATH}" incluyendo cómo primera entrada /tmp/example/my-cli/.venv/bin/, así que el ejecutable estará directamente disponibel cómo my-cli
  • Importa el módulo y función que hemos indicado
  • Es multiplataforma. En linux podremos usar my-cli, en windows my-cli.exe

__init__.py

Es un fichero que marca un directorio cómo un python package. Su contenido se ejecuta siempre que se hace un import del paquete. No debería ser usado para escribir lógica, ni cómo entry point para un ejecutable.

Mi forma preferida

Las piezas se pueden combinar de muchas formas. Algunas combinaciones serán más correctas que otras, según que criterios o necesidades se tengan en cuenta.

Mis criterios son:

  • src-layout
  • Siempre instalar el paquete, sea para usarlo o desarrollarlo
  • Asumir que el paquete puede ser distribuido
  • Minimizar tiros-en-el-pie, errores absurdos y subtle bugs

Con lo que llegamos a la siguiente estructura:

# my-cli/src/my_cli/__init__.py
__version__ = "1.0.0"

# my-cli/src/my_cli/lib.py
def do_things():
    print("do things")

# my-cli/src/my_cli/cli.py
from my_cli.lib import do_things

def main():
    print("Argument parsing logic goes here")
    do_things()

# my-cli/src/my_cli/__main__.py
from my_cli.cli import main

if __name__ == "__main__":
    main()
# my-cli/pyproject.toml
[project.scripts]
my-cli = "my_cli.cli:main"

Algunas dudas

Tengo algunas dudas con esta aproximación que modifico según el caso:

  • cli.main o cli.cli. El mejor nombre para esta función también me causa dudas. Depende del naming del resto código, y de si la herramienta es fundamentalmente una cli, una librería o ambas.
  • Cuando se usan librerías cómo Typer o Click y subcomandos la estructura de directorios debería ser algo distinta pero la idea base es la misma.
  • Es realmente necesario proporcionar un __main__.py. Significa duplicar lógica para un uso muy concreto sólo necesario durante el desarrollo.

Prácticas "incorrectas"

Usar __init__.py

Al importar nuestro paquete (import my_tool) estaremos:

  • Polucionando el espacio de nombres con el cli.main. Podríamos usar __all__, pero eso queda para otro día.
  • Fomentando antipatrones
  • Corriendo el riesgo de side effects indeseados.
# WRONG
# src/my_cli/__init__.py

from my_cli.cli import main

__version__ = "1.0.0"

if __name__ == "__main__":
    main()

Jugar con el nombre del ejecutable

En pyproject.toml se puede asignar un nombre al ejecutable. Salvo que haya un motivo realmente bueno, cómo tener más de un ejecutable, debe tener el mismo nombre que el paquete para evitar confusiones.

# WRONG
# pyproject.toml
[project.scripts]
# Asignamos el nombre my-tool al ejecutable
my-tool = "my_cli.cli:main"

Ejecutar directamente cli.py

Salvo scripts muy pequeños, ejecutar directamente un módulo Python debería ser considerado un antipatrón. Obligar a instalar el paquete y ejecutar los módulos ahorra problemas, sobre todo con los imports. Por ello no permitiremos la ejecución directa del módulo.

# WRONG
# src/my_cli/cli.py
from my_cli.lib import do_things

def main():
    print("Argument parsing logic goes here")
    do_things()

# Permitimos ejecutar directamente este módulo con `python src/my_cli/cli.py`
if __name__ == "__main__":
    main()

Apuntar a __main__.py desde project.scripts

# src/my_cli/__main__.py
from my_cli.cli import main

if __name__ == "__main__":
    main()
[project.scripts]
my-tool = "my_cli.__main__:main"

Parece una buena idea, porqué un cambio grande en la herramienta, que toque cli.py, puede obligar a cambiar tanto el fichero __main__.py cómo el pyproject.toml. Si pyproject.toml apunta directamente a __main__.py sólo hay que hacer el cambio en un sitio.

Pero no lo es.

  • __main__.py debería estar reservado para ejecutar con python -m my_tool por Separation of Concerns.
  • El stack trace al usar my-tool irá directamente a cli.py sin pasar con __main__.py que es sólo un wrapper.
  • Los imports se vuelven confusos, y podemos acabar con dependencias circulares. En este ejemplo concreto, aunque podría ser escrito de otra forma, "main" es en realidad cli.main, no __main__.main.
  • __main__ tiene dos significados distintos en python, lo que lleva a confusión.
  • Evitamos indirecciones. Con leer pyproject.toml podemos saltar al código real, y el shim también queda más limpio.
  • Es una práctica más habitual (black, pytest, pip).
  • Si el proyecto crece y proporcionamos más ejecutables, el resultado es más limpio.
[project.scripts]
my-tool = "my_cli.cli:main"
my-tool-db = "my_cli.commands.database:main"
my-tool-serve = "my_cli.commands.server:main"

Referencias

Estilo de Markdown generado por los LLM

He hecho un estudio no científico del estilo de Markdown que emplean los modelos/aplicaciones más populares.

Claude.ai - Sonnet 4

  • CommonMark
  • backticks para código inline y triple backticks (fenced blocks) para bloques de código
  • * y ** para italic emphasis y bold emphasis y no _
  • - para listas, con un sólo espacio de separación
  • - listas anidadas con dos espacios
  • 1. uno \n 2. dos para listas ordenadas
  • # para cabeceras y underline style

Gemini 2.5

Cuando le preguntas a 2.5 tanto flash cómo pro es poco claro dice por ejemplo Unordered Lists (bullet points) with hyphens (-) or asterisks (*).. Si insistes dice que usa -, pero si luego copias una de sus salidas en realidad usa *.

Parece menos consistente que Claude

En general lo que parece ser es:

  • No declara un tipo concreto de sabor de markdown
  • backticks para código inline y triple backticks (fenced blocks) para bloques de código
  • * y ** para italic emphasis y bold emphasis y no _
  • * para listas, con un sólo espacio de separación
  • * listas anidadas con cuatro espacios
  • Curiosamente gemini parece usar 1. mi texto, pero * mi texto, mezcla indentación para ordenadas y desordenadas
  • 1. uno \n 2. dos para listas ordenadas
  • # para cabeceras y underline style

ChatGPT - GPT5

La respuesta de ChatGPT parece buena pero no lo es. Indica que no tiene reglas claras pero que OpenAI le da unas system level rules que seguir. Pero luego dice que usa * y - indistintamente. Si insistes dice que usa - para listas siempre cuando es mentira.

En general lo que parece ser es:

  • Prefiere GFM
  • backticks para código inline y triple backticks (fenced blocks) para bloques de código
  • * y ** para italic emphasis y bold emphasis y no _
  • * para listas, con un sólo espacio de separación
  • * listas anidadas con dos espacios
  • 1. uno \n 2. dos para listas ordenadas
  • # para cabeceras y underline style

Conclusiones

No he conseguido extraer reglas claras en esta prueba rápida. El objetivo era saber que usaban para configurar el linter/formatter de un modo que los diff no lancen muchas diferencias.

En general este sería el estilo más compatible que se podría usar:

# Ejemplo de texto

Un texto con _italic emphasis_ y también **bold emphasis**, en el que tenemos:

-   Una lista desordenada
-   Con sólo dos items
    -   y anidación

## Y una lista ordenada

1. Primer item

-   lista desordenada anidada
-   con otro item

2. Segundo item con `codigo inline`

## Blockquotes

> Siguen este
> estilo

## Tablas

| Column A | Column B |
| -------- | -------- |
| A1       | B1       |
| A2       | B2       |

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 src 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.