Cómo Python lee tu código: tokenización y parsing

Cuando escribes x = 1 + 2 y presionas Enter, Python no ejecuta eso directamente. Pasa por un viaje de cinco etapas antes de que ocurra cualquier cálculo. Entender ese viaje te da un modelo mental mucho más sólido de por qué ciertos errores aparecen antes de que tu programa arranque, y por qué otros solo explotan en medio de la ejecución.

El viaje del código fuente

Todo empieza como texto plano —una cadena de caracteres. Python necesita convertir esa sopa de letras en algo con estructura. Lo hace en pasos:

Tokenización es el primero. El tokenizador recorre tu código carácter por carácter y lo agrupa en tokens: las unidades mínimas de significado. for es un token (de tipo NAME). = es un token (tipo OP). 42 es un token (tipo NUMBER). Un espacio entre palabras no es un token —es simplemente separador. Si escribes @@@, el tokenizador falla inmediatamente porque esa secuencia no forma ningún token reconocible.

Parsing toma esa lista de tokens y verifica que estén en un orden gramaticalmente válido, construyendo el AST (Abstract Syntax Tree, árbol sintáctico abstracto). El AST es una representación jerárquica de tu código: un árbol donde cada nodo representa una construcción del lenguaje. Una asignación tiene un nodo hijo izquierdo (el nombre) y uno derecho (el valor). Un if tiene nodos para la condición y el cuerpo. Si la gramática está rota —por ejemplo def def:—, el parser falla aquí con un SyntaxError.

Después viene el compilador, que transforma el AST en bytecode: instrucciones simples para una máquina virtual. Finalmente el intérprete (la CPython VM) ejecuta ese bytecode, instrucción por instrucción.

La clave de todo esto: la tokenización y el parsing ocurren completos antes de ejecutar una sola línea. Por eso un SyntaxError aparece aunque el error esté en la línea 200 de un archivo de 201 líneas —Python ya leyó todo. En cambio, un NameError o un ZeroDivisionError solo aparecen cuando la ejecución llega a esa línea, porque requieren valores que solo existen en tiempo de ejecución.

# Este archivo nunca ejecuta nada, ni siquiera el print de la línea 1
print("esta línea nunca corre")
def rota(:     # SyntaxError detectado en el parsing, antes de cualquier ejecución
    pass

La diferencia entre SyntaxError y errores en tiempo de ejecución no es trivial: te dice en qué etapa del viaje algo salió mal.

import ast
import tokenize
import io

# ── 1. Tokenización ─────────────────────────────────────────────────────────
source = "total = price * quantity + discount"

tokens = list(tokenize.generate_tokens(io.StringIO(source).readline))
for tok in tokens:
    # tok es una namedtuple: (tipo, string, inicio, fin, línea)
    if tok.string.strip():          # ignoramos tokens de espacio/newline vacíos
        print(f"{tokenize.tok_name[tok.type]:10}  →  {tok.string!r}")

print()

# ── 2. Parsing → AST ────────────────────────────────────────────────────────
tree = ast.parse(source)

# ast.dump muestra el árbol completo como texto
print(ast.dump(tree, indent=2))

print()

# ── 3. Recorrer el AST manualmente con un visitor ───────────────────────────
class NameCollector(ast.NodeVisitor):
    """Recolecta todos los identificadores que aparecen en el código."""

    def __init__(self):
        self.names: list[str] = []

    def visit_Name(self, node: ast.Name) -> None:
        self.names.append(node.id)
        self.generic_visit(node)    # seguir bajando por los nodos hijos

collector = NameCollector()
collector.visit(tree)
print("Identificadores encontrados:", collector.names)

print()

# ── 4. SyntaxError se detecta en parsing, antes de ejecutar ─────────────────
broken = "if x =  42:"   # = en vez de ==, sintaxis inválida

try:
    ast.parse(broken)
except SyntaxError as e:
    # El error incluye número de línea y columna exactos
    print(f"SyntaxError en línea {e.lineno}, columna {e.offset}: {e.msg}")

print()

# ── 5. RuntimeError solo ocurre al ejecutar ─────────────────────────────────
valid_syntax_bad_logic = "resultado = 1 / 0"

# Esto no lanza nada: la sintaxis es perfecta
tree2 = ast.parse(valid_syntax_bad_logic)
print("Parseó sin errores:", ast.dump(tree2))

try:
    exec(compile(tree2, filename="<string>", mode="exec"))
except ZeroDivisionError as e:
    # Solo explota cuando la VM intenta ejecutar el bytecode
    print(f"ZeroDivisionError en tiempo de ejecución: {e}")

Lo que hace cada parte del código

El bloque de tokenización usa tokenize.generate_tokens, que acepta un callable que devuelve líneas —de ahí el truco de envolver el string en io.StringIO y pasar su método .readline. Cada token te dice su tipo (NAME, OP, NUMBER, etc.), el string exacto que lo produjo, y su posición en el archivo. Fíjate que total, price, quantity y discount son todos tokens de tipo NAME: el tokenizador no sabe si son variables definidas o no. Eso no es su trabajo.

El ast.parse(source) hace tokenización y parsing en un solo paso desde la API pública. Lo que devuelve es un objeto ast.Module que es la raíz del árbol. Cada nodo del árbol es una instancia de alguna clase del módulo ast: ast.Assign, ast.BinOp, ast.Name, ast.Constant… La jerarquía refleja exactamente la gramática del lenguaje.

ast.NodeVisitor es el patrón estándar para recorrer el árbol. Defines métodos visit_<NombreDeNodo> y el visitor los llama automáticamente cuando encuentra un nodo de ese tipo. generic_visit es crucial: si no lo llamas, el visitor no desciende a los hijos del nodo actual. Olvidarlo es el error más común al escribir visitors propios.

El experimento con ast.parse(broken) demuestra concretamente que el SyntaxError viene del parser, no del intérprete: llamar a ast.parse no ejecuta nada, solo construye el árbol. Si falla, es porque la gramática está rota.

El último bloque es quizás el más revelador: "resultado = 1 / 0" tiene sintaxis perfecta. El árbol se construye sin problema. El error solo aparece cuando le pasas ese árbol a compile() para generar bytecode y luego exec() para ejecutarlo. La división entre SyntaxError y ZeroDivisionError no es arbitraria —refleja en qué etapa del pipeline Python detectó que algo iba mal.

Errores que debes conocer

Error: Asumir que si un archivo importa correctamente, todo el código en él es válido. Un SyntaxError en cualquier parte del archivo impide que el módulo cargue entero.

# ❌ Wrong — el módulo entero falla al importar
def greet(name):
    return f"Hola {name}"

def broken(   # SyntaxError aquí
    pass
# ✅ Right — sintaxis completa en todo el archivo
def greet(name):
    return f"Hola {name}"

def broken():
    pass

Python parsea el archivo completo al importarlo; un error en broken hace que greet tampoco esté disponible.


Error: Confundir un error de sintaxis con un error lógico porque ambos mencionan números de línea.

# ❌ Wrong — ZeroDivisionError NO es un SyntaxError, aunque Python
# indica exactamente en qué línea ocurrió
def calc(x):
    return x / 0   # línea 2: Python lo dice, pero solo al ejecutar

calc(5)
# ✅ Right — distingue en qué etapa falla: antes (syntax) o durante (runtime)
def calc(x):
    if x == 0:
        raise ValueError("divisor no puede ser cero")
    return 10 / x

calc(5)

Un SyntaxError siempre aparece antes de ejecutar; cualquier otro error con número de línea es de runtime y requiere que el código realmente corra hasta ese punto.


Error: Llamar a generic_visit incorrectamente al escribir un NodeVisitor, haciendo que el recorrido se detenga en el primer nivel.

# ❌ Wrong — nunca visita nodos más profundos que el primero que coincide
class BadVisitor(ast.NodeVisitor):
    def visit_Name(self, node):
        print(node.id)
        # sin generic_visit: los hijos de este nodo nunca se visitan
# ✅ Right
class GoodVisitor(ast.NodeVisitor):
    def visit_Name(self, node):
        print(node.id)
        self.generic_visit(node)  # delegar el descenso a los hijos

generic_visit es el mecanismo que propaga la visita hacia abajo; sin él, el árbol se recorre solo superficialmente.


El módulo ast también te deja modificar el árbol antes de compilarlo con ast.NodeTransformer —la misma idea que NodeVisitor pero con la capacidad de devolver nodos reemplazados—, lo que es la base de herramientas como los decoradores de pytest que reescriben assertions para mostrar mensajes de error más detallados.

17

Dejar un comentario

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

Scroll al inicio