El módulo ast (Abstract Syntax Tree) de Python permite analizar, recorrer y transformar código fuente Python como un árbol de objetos. Es la herramienta que usan linters, formateadores, generadores de documentación y transpiladores. Con él puedes escribir tus propias reglas de calidad de código, detectar patrones peligrosos o transformar código automáticamente.
ast.parse(): convertir código en árbol
import ast
codigo = """
x = 10
y = x * 2 + 5
print(f"Resultado: {y}")
"""
arbol = ast.parse(codigo)
print(type(arbol)) # <class 'ast.Module'>
print(ast.dump(arbol, indent=2))
El resultado de ast.dump muestra la estructura completa del árbol: nodos, campos y valores. Es la mejor forma de entender qué tipo de nodo corresponde a cada construcción Python.
Explorar el árbol con ast.walk
import ast
codigo = """
def calcular(a, b):
resultado = a + b * 2
return resultado
class Punto:
def __init__(self, x, y):
self.x = x
self.y = y
"""
arbol = ast.parse(codigo)
# Encontrar todas las definiciones de función
funciones = [nodo.name for nodo in ast.walk(arbol) if isinstance(nodo, ast.FunctionDef)]
print(f"Funciones: {funciones}") # ['calcular', '__init__']
# Encontrar todas las clases
clases = [nodo.name for nodo in ast.walk(arbol) if isinstance(nodo, ast.ClassDef)]
print(f"Clases: {clases}") # ['Punto']
# Encontrar todas las llamadas a funciones
llamadas = [
nodo.func.id
for nodo in ast.walk(arbol)
if isinstance(nodo, ast.Call) and isinstance(nodo.func, ast.Name)
]
print(f"Llamadas: {llamadas}")
NodeVisitor: recorrer el árbol con patrón Visitor
import ast
from collections import defaultdict
class AnalizadorImports(ast.NodeVisitor):
"""Recoge todos los imports del código fuente."""
def __init__(self):
self.imports: list[str] = []
self.from_imports: dict[str, list[str]] = defaultdict(list)
def visit_Import(self, nodo: ast.Import):
for alias in nodo.names:
self.imports.append(alias.name)
self.generic_visit(nodo) # visitar hijos
def visit_ImportFrom(self, nodo: ast.ImportFrom):
modulo = nodo.module or ''
for alias in nodo.names:
self.from_imports[modulo].append(alias.name)
self.generic_visit(nodo)
codigo = """
import os
import sys
from pathlib import Path
from collections import defaultdict, OrderedDict
from typing import Optional, List
"""
arbol = ast.parse(codigo)
analizador = AnalizadorImports()
analizador.visit(arbol)
print(f"Imports directos: {analizador.imports}")
for modulo, nombres in analizador.from_imports.items():
print(f" from {modulo} import {', '.join(nombres)}")
Linter personalizado: detectar patrones problemáticos
import ast
from dataclasses import dataclass, field
@dataclass
class Aviso:
linea: int
columna: int
codigo: str
mensaje: str
def __str__(self) -> str:
return f"L{self.linea}:C{self.columna} [{self.codigo}] {self.mensaje}"
class LinterSimple(ast.NodeVisitor):
"""Detecta malas prácticas comunes."""
def __init__(self):
self.avisos: list[Aviso] = []
def _avisar(self, nodo: ast.AST, codigo: str, mensaje: str):
self.avisos.append(Aviso(nodo.lineno, nodo.col_offset, codigo, mensaje))
def visit_Compare(self, nodo: ast.Compare):
"""Detecta comparaciones con True/False/None sin is/is not."""
for op, comparador in zip(nodo.ops, nodo.comparators):
if isinstance(comparador, ast.Constant) and comparador.value is None:
if isinstance(op, (ast.Eq, ast.NotEq)):
self._avisar(nodo, "E001", "Usa 'is None' o 'is not None' en lugar de == None")
if isinstance(comparador, ast.Constant) and isinstance(comparador.value, bool):
if isinstance(op, (ast.Eq, ast.NotEq)):
self._avisar(nodo, "E002", "Usa 'is True'/'is False' en lugar de == True/False")
self.generic_visit(nodo)
def visit_FunctionDef(self, nodo: ast.FunctionDef):
"""Detecta funciones sin docstring."""
tiene_docstring = (
nodo.body and
isinstance(nodo.body[0], ast.Expr) and
isinstance(nodo.body[0].value, ast.Constant) and
isinstance(nodo.body[0].value.value, str)
)
if not tiene_docstring and not nodo.name.startswith('_'):
self._avisar(nodo, "E003", f"Función '{nodo.name}' sin docstring")
self.generic_visit(nodo)
def visit_Raise(self, nodo: ast.Raise):
"""Detecta 'raise Exception' genérico."""
if isinstance(nodo.exc, ast.Call):
if isinstance(nodo.exc.func, ast.Name) and nodo.exc.func.id == 'Exception':
self._avisar(nodo, "E004", "Usa excepciones específicas en lugar de Exception genérico")
self.generic_visit(nodo)
codigo_a_analizar = """
def calcular(x, y):
if x == None:
raise Exception("x no puede ser None")
if y == True:
return x
return x + y
"""
arbol = ast.parse(codigo_a_analizar)
linter = LinterSimple()
linter.visit(arbol)
for aviso in linter.avisos:
print(aviso)
NodeTransformer: modificar el árbol
import ast
class TransformadorPrint(ast.NodeTransformer):
"""Reemplaza print(x) por logging.info(x)."""
def visit_Call(self, nodo: ast.Call):
self.generic_visit(nodo) # transformar hijos primero
if isinstance(nodo.func, ast.Name) and nodo.func.id == 'print':
nodo.func = ast.Attribute(
value=ast.Name(id='logging', ctx=ast.Load()),
attr='info',
ctx=ast.Load()
)
return nodo
codigo_original = """
print("Iniciando...")
resultado = 42
print(f"Resultado: {resultado}")
"""
arbol = ast.parse(codigo_original)
arbol_modificado = TransformadorPrint().visit(arbol)
ast.fix_missing_locations(arbol_modificado)
# Compilar y ejecutar el árbol modificado
import logging
logging.basicConfig(level=logging.INFO)
exec(compile(arbol_modificado, '<string>', 'exec'))
ast.literal_eval: evaluar expresiones de forma segura
import ast
# PELIGROSO: eval() puede ejecutar cualquier código
# resultado = eval("[1, 2, 3]")
# SEGURO: literal_eval solo acepta literales Python
datos = ast.literal_eval("[1, 2, {'clave': 'valor'}, (3, 4)]")
print(datos) # [1, 2, {'clave': 'valor'}, (3, 4)]
print(type(datos)) # <class 'list'>
# Valores aceptados: str, bytes, int, float, bool, None, list, tuple, dict, set
configuracion = ast.literal_eval("{'host': 'localhost', 'puerto': 5432, 'debug': True}")
print(configuracion['host']) # localhost
# Rechaza código arbitrario:
try:
ast.literal_eval("__import__('os').system('rm -rf /')")
except ValueError as e:
print(f"Rechazado correctamente: {e}")
El módulo ast es la puerta de entrada al metaprogramación estática en Python. Herramientas como black, flake8, pylint, mypy y bandit se construyen sobre él. Dominarlo te permite escribir herramientas propias de análisis de código adaptadas exactamente a las convenciones de tu proyecto.
