Antes de generar bytecode, el compilador de Python recorre el árbol sintáctico y decide el ámbito de cada identificador. Debe determinar si un nombre es local, global, nonlocal, parámetro, importado, variable libre o namespace. El módulo symtable en Python expone estas tablas de símbolos para análisis estático, herramientas educativas, linters, generadores de documentación y estudios sobre closures.
Esta guía muestra cómo crear una tabla con symtable(), recorrer funciones y clases, consultar símbolos y reconocer variables libres, globals, annotations, comprehensions y type parameters. Complementa nuestros artículos sobre bytecode con dis, introspección con inspect, descriptors, singledispatch y tracebacks.
El papel de una tabla de símbolos
Un árbol de sintaxis abstracta describe funciones, asignaciones y llamadas, pero el compilador todavía debe resolver el significado de cada nombre dentro de su bloque. Las tablas se generan después del AST y antes del bytecode.
Determinan, por ejemplo, que un nombre asignado dentro de una función es local, salvo que una declaración global o nonlocal cambie su clasificación. También identifican variables libres capturadas por closures.
Crear una tabla
symtable.symtable() recibe código fuente, un nombre de archivo utilizado en mensajes y un modo de compilación.
import symtable
codigo = """
tasa = 0.1
def total(valor):
return valor * (1 + tasa)
"""
tabla = symtable.symtable(
codigo,
"ejemplo.py",
"exec",
)
print(tabla.get_name())
print(tabla.get_type())Los modos siguen a compile(): exec para módulos y bloques, eval para expresiones y single para una instrucción interactiva.
Tipos de tabla
get_type() devuelve un miembro de SymbolTableType. Los tipos centrales representan módulos, funciones y clases. Python moderno también incluye ámbitos de annotations, aliases de tipo, parámetros de tipo y variables de tipo.
from symtable import SymbolTableType
if tabla.get_type() is SymbolTableType.MODULE:
print("Tabla del módulo")La documentación oficial de symtable recomienda utilizar el enum en lugar de strings fijas porque los valores textuales pueden cambiar.
Identificadores del módulo
get_identifiers() devuelve los nombres reconocidos en el bloque.
print(list(tabla.get_identifiers()))
# ['tasa', 'total']get_symbols() devuelve objetos Symbol con flags detalladas.
for simbolo in tabla.get_symbols():
print(
simbolo.get_name(),
simbolo.is_local(),
simbolo.is_global(),
simbolo.is_namespace(),
)En el nivel del módulo, los nombres asignados suelen ser locales al módulo y globales para el programa.
Consultar un nombre
lookup() recupera la entrada de un identificador.
simbolo = tabla.lookup("total")
print(simbolo.is_assigned())
print(simbolo.is_namespace())Un nombre introducido por una definición de función o clase es un namespace. El mismo nombre puede estar asociado con varios namespaces si el código lo vuelve a enlazar.
Recorrer tablas hijas
Funciones, clases y otros ámbitos anidados aparecen en get_children().
for hija in tabla.get_children():
print(
hija.get_name(),
hija.get_type(),
hija.get_lineno(),
)has_children() informa si existen namespaces internos, mientras is_nested() identifica una función o clase anidada.
Analizar una función
Las tablas de función ofrecen métodos para parámetros, locales, globals, nonlocals y variables libres.
funcion = tabla.get_children()[0]
print(funcion.get_parameters())
print(funcion.get_locals())
print(funcion.get_globals())
print(funcion.get_nonlocals())
print(funcion.get_frees())En el ejemplo, valor es parámetro y local. tasa es global porque la función la lee sin asignarla localmente.
Globals implícitos
Considera una lectura sin asignación local.
codigo = """
configuracion = {}
def obtener():
return configuracion
"""En la tabla de la función, configuracion es global. Esto no prueba que el nombre exista durante la ejecución; clasifica cómo se compila la búsqueda.
Declaraciones global explícitas
is_declared_global() diferencia una instrucción global de una búsqueda global implícita.
codigo = """
contador = 0
def incrementar():
global contador
contador += 1
"""
tabla = symtable.symtable(codigo, "contador.py", "exec")
funcion = tabla.get_children()[0]
simbolo = funcion.lookup("contador")
print(simbolo.is_global())
print(simbolo.is_declared_global())Un linter puede usar esta diferencia para señalar mutaciones de estado global.
nonlocal y closures
nonlocal permite modificar una variable de una función externa.
codigo = """
def crear_contador():
valor = 0
def incrementar():
nonlocal valor
valor += 1
return valor
return incrementar
"""La tabla interna clasifica valor como nonlocal y libre.
raiz = symtable.symtable(codigo, "closure.py", "exec")
externa = raiz.get_children()[0]
interna = externa.get_children()[0]
print(interna.get_nonlocals())
print(interna.get_frees())Esto explica por qué el compilador crea celdas accesibles mediante opcodes como LOAD_DEREF.
Flags de símbolos
Un Symbol expone varios predicados:
is_referenced(): usado en su bloque;is_assigned(): asignado en el bloque;is_parameter(): parámetro de función;is_imported(): creado por import;is_local(),is_global()eis_nonlocal();is_free(): resuelto en un ámbito externo;is_annotated(): posee annotation;is_namespace(): introduce un namespace.
Combina flags en lugar de asumir que todas son mutuamente excluyentes.
Imports
is_imported() identifica nombres creados por instrucciones import.
codigo = """
import json
from pathlib import Path
"""
tabla = symtable.symtable(codigo, "imports.py", "exec")
for nombre in tabla.get_identifiers():
simbolo = tabla.lookup(nombre)
print(nombre, simbolo.is_imported())Esto ayuda a analizadores de dependencias. Los imports dinámicos con importlib o __import__() no aparecen como enlaces estáticos normales.
Annotations
is_annotated() informa si un nombre posee annotation.
codigo = """
cantidad: int
precio: float = 10.0
"""
tabla = symtable.symtable(codigo, "tipos.py", "exec")
for nombre in tabla.get_identifiers():
print(nombre, tabla.lookup(nombre).is_annotated())Una annotation y un valor asignado son hechos diferentes. Un nombre puede estar anotado sin recibir un valor en esa línea.
Type parameters en Python moderno
Python 3.12 introdujo sintaxis de parámetros de tipo y ámbitos relacionados. Python 3.14 añade is_type_parameter().
codigo = """
def primero[T](elementos: list[T]) -> T:
return elementos[0]
"""
tabla = symtable.symtable(codigo, "generics.py", "exec")El parser debe soportar la sintaxis. Las herramientas deberían detectar la versión y usar SymbolTableType.TYPE_PARAMETERS.
Clases y métodos
Las tablas de clase heredan de SymbolTable. get_methods() lista funciones declaradas directamente en el cuerpo, pero fue deprecado en Python 3.14 y está previsto eliminarlo en Python 3.16.
Las herramientas nuevas deberían recorrer get_children(), seleccionar tablas de función y considerar ámbitos de parámetros de tipo que pueden aparecer entre la clase y sus métodos.
Nombres de clase y variables libres
Python 3.14 añade is_free_class() para un nombre de clase que es libre desde la perspectiva de un método.
codigo = """
def externa():
x = 1
class C:
x = 2
def metodo(self):
return x
"""El método devuelve el x de la función externa, no el atributo del cuerpo de la clase. Los bloques de clase no actúan como ámbitos léxicos de función.
Comprehensions
Python 3.14 añade is_comp_iter() para variables de iteración y is_comp_cell() para celdas de comprehensions inlined.
codigo = """
def cuadrados(valores):
return [valor * valor for valor in valores]
"""La forma exacta de la tabla depende de optimizaciones de la versión. Usa flags públicas en vez de nombres internos de bloques generados.
Almacenamiento optimizado
is_optimized() indica si los locales pueden usar almacenamiento optimizado. Las funciones suelen utilizar fast locals, mientras los módulos usan mappings de namespace.
Esta flag ayuda a explicar diferencias en el comportamiento de locals() dentro de módulos y funciones optimizadas.
IDs y líneas
get_id() devuelve un identificador de tabla y get_lineno() la primera línea del bloque.
for hija in tabla.get_children():
print(hija.get_id(), hija.get_lineno())No persistas get_id() como clave estable. Para reportes, combina archivo, tipo, nombre y línea.
Uso desde la terminal
Desde Python 3.13, el módulo puede ejecutarse como script.
python -m symtable programa.pySin archivos, lee la entrada estándar. La salida sirve para exploración; las aplicaciones deberían usar la API.
symtable frente a ast
La documentación oficial de AST describe nodos sintácticos y permite localizar asignaciones, llamadas y expresiones. symtable añade la clasificación de ámbito del compilador.
Un linter robusto suele usar ambos: AST para el contexto de la instrucción y la tabla para saber a qué namespace pertenece el nombre.
symtable frente a inspect
symtable trabaja con código fuente sin ejecutarlo. inspect examina objetos vivos después del import o la ejecución. El análisis estático reduce riesgos con código desconocido, aunque todavía deben existir límites para entradas enormes.
Ejemplo de reporte
def describir(tabla, ruta=()):
actual = ruta + (tabla.get_name(),)
for simbolo in tabla.get_symbols():
yield {
"ambito": ".".join(actual),
"nombre": simbolo.get_name(),
"local": simbolo.is_local(),
"global": simbolo.is_global(),
"nonlocal": simbolo.is_nonlocal(),
"libre": simbolo.is_free(),
"parametro": simbolo.is_parameter(),
"importado": simbolo.is_imported(),
}
for hija in tabla.get_children():
yield from describir(hija, actual)El resultado puede serializarse como JSON o mostrarse en una herramienta educativa.
Limitaciones
El módulo no revela tipos ni valores de runtime, branches ejecutados o imports dinámicos. Sigue las reglas del compilador de la versión actual. Código con sintaxis más reciente puede generar SyntaxError.
El análisis de proyectos completos también debe resolver archivos, paquetes, imports, stubs, código generado y configuración del type checker.
Errores frecuentes
- Asumir que un global existirá durante la ejecución.
- Tratar todas las flags como mutuamente excluyentes.
- Comparar strings de tipo en lugar del enum.
- Depender de
get_methods(), ya deprecado. - Ignorar ámbitos de annotations y type parameters.
- Suponer que una clase cierra nombres como una función.
- Ejecutar código cuando basta un análisis estático.
- Persistir IDs internos como identificadores estables.
Buenas prácticas
- Usa
SymbolTableType. - Recorre todas las tablas hijas.
- Combina symtable con AST.
- Registra versión y archivo.
- Prueba globals, nonlocals, closures y comprehensions.
- Considera ámbitos modernos de tipado.
- Evita helpers deprecados.
- Limita tamaño y complejidad de fuentes no confiables.
Conclusión
El módulo symtable en Python abre una ventana a la fase del compilador que resuelve identificadores y ámbitos. Distingue parámetros, locales, globals, nonlocals, imports, namespaces, annotations y variables libres antes de generar bytecode.
Esta información resulta valiosa para linters, herramientas educativas, documentación y análisis de closures. Combinando tablas de símbolos con AST y considerando nuevos ámbitos de tipado y comprehensions, una herramienta puede comprender el significado de los nombres sin ejecutar el programa ni reimplementar reglas complejas del compilador.







