Python’s symtable module builds the same symbol table that the compiler uses internally — exposing scope types, variable categories (local, global, free, cell), and closure relationships. import symtable. symtable: table = symtable.symtable(source, filename, compile_type) → root SymbolTable; compile_type is "exec" (module), "eval" (expression), or "single" (interactive). SymbolTable.get_name: scope name. get_type: "module", "class", "function". get_symbols: list of Symbol objects. get_children: nested SymbolTable objects. lookup: tbl.lookup("x") → Symbol. has_exec: True if scope uses exec. is_optimized: True if locals are accessed via LOAD_FAST (normal functions). Function (subclass): get_parameters() → tuple of parameter names; get_locals(), get_globals(), get_frees() (closed-over), get_cells() (captured by inner scopes). Symbol: get_name(), is_assigned(), is_referenced(), is_imported(), is_global(), is_local(), is_free() (captured from outer scope), is_cell() (captured by inner scope), is_parameter(), is_namespace(). Class (subclass): get_methods(). Iterate a SymbolTable tree with get_children() recursively. Claude Code generates scope analyzers, free-variable finders, closure auditors, and undefined-name checkers.
CLAUDE.md for symtable
## symtable Stack
- Stdlib: import symtable
- Build: table = symtable.symtable(source, "<string>", "exec")
- Scope: table.get_type() # "module" / "function" / "class"
- Syms: [s.get_name() for s in table.get_symbols()]
- Find: sym = table.lookup("x")
- Free: [s.get_name() for s in table.get_symbols() if s.is_free()]
- Walk: def walk(t): yield t; yield from (walk(c) for c in t.get_children())
symtable Scope Analysis Pipeline
# app/symtableutil.py — build, walk, classify symbols, free vars, closures, undefined
from __future__ import annotations
import ast
import symtable
from dataclasses import dataclass, field
from typing import Iterator
# ─────────────────────────────────────────────────────────────────────────────
# 1. Build helpers
# ─────────────────────────────────────────────────────────────────────────────
def build_table(
source: str,
filename: str = "<string>",
compile_type: str = "exec",
) -> symtable.SymbolTable:
"""
Parse source and return its root SymbolTable.
compile_type: "exec" (module), "eval" (expression), "single" (interactive).
Example:
table = build_table(Path("app/main.py").read_text(), "app/main.py")
"""
return symtable.symtable(source, filename, compile_type)
def build_table_from_file(path: str) -> symtable.SymbolTable:
"""
Build a SymbolTable from a .py file on disk.
Example:
table = build_table_from_file("app/main.py")
"""
from pathlib import Path
src = Path(path).read_text(encoding="utf-8")
return build_table(src, path)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Tree traversal
# ─────────────────────────────────────────────────────────────────────────────
def walk_tables(
table: symtable.SymbolTable,
) -> Iterator[symtable.SymbolTable]:
"""
Depth-first walk of all scopes in the symbol table tree.
Example:
for scope in walk_tables(table):
print(scope.get_name(), scope.get_type())
"""
yield table
for child in table.get_children():
yield from walk_tables(child)
def find_scope(
table: symtable.SymbolTable,
name: str,
scope_type: str | None = None,
) -> symtable.SymbolTable | None:
"""
Find the first scope by name (and optional type: "function", "class", "module").
Example:
fn_scope = find_scope(table, "my_function", scope_type="function")
"""
for scope in walk_tables(table):
if scope.get_name() == name:
if scope_type is None or scope.get_type() == scope_type:
return scope
return None
# ─────────────────────────────────────────────────────────────────────────────
# 3. Symbol classification
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class SymbolInfo:
name: str
scope_name: str
scope_type: str
is_local: bool
is_global: bool
is_free: bool # captured from outer scope (closure variable)
is_cell: bool # captured by inner scope
is_param: bool
is_assigned: bool
is_imported: bool
is_namespace: bool # refers to a nested scope
def category(self) -> str:
if self.is_param: return "parameter"
if self.is_free: return "free (closure)"
if self.is_cell: return "cell (captured)"
if self.is_global: return "global"
if self.is_local: return "local"
if self.is_imported: return "imported"
return "other"
def __str__(self) -> str:
return (f"{self.name:20s} {self.category():20s} "
f"scope={self.scope_name!r} ({self.scope_type})")
def classify_symbols(table: symtable.SymbolTable) -> list[SymbolInfo]:
"""
Return SymbolInfo for every symbol in a single scope (non-recursive).
Example:
for sym in classify_symbols(table):
print(sym)
"""
results: list[SymbolInfo] = []
scope_name = table.get_name()
scope_type = table.get_type()
for sym in table.get_symbols():
results.append(SymbolInfo(
name=sym.get_name(),
scope_name=scope_name,
scope_type=scope_type,
is_local=sym.is_local(),
is_global=sym.is_global(),
is_free=sym.is_free(),
is_cell=sym.is_cell(),
is_param=sym.is_parameter(),
is_assigned=sym.is_assigned(),
is_imported=sym.is_imported(),
is_namespace=sym.is_namespace(),
))
return results
def all_symbols(table: symtable.SymbolTable) -> list[SymbolInfo]:
"""
Recursively collect SymbolInfo for every symbol in every scope.
Example:
table = build_table(source, "app.py")
symbols = all_symbols(table)
free = [s for s in symbols if s.is_free]
"""
results: list[SymbolInfo] = []
for scope in walk_tables(table):
results.extend(classify_symbols(scope))
return results
# ─────────────────────────────────────────────────────────────────────────────
# 4. Specific queries
# ─────────────────────────────────────────────────────────────────────────────
def free_variables(table: symtable.SymbolTable) -> dict[str, list[str]]:
"""
Find all free variables (closure captures) in every function scope.
Returns {function_name: [free_var_names]}.
Example:
source = '''
def outer(x):
def inner():
return x # x is free in inner
return inner
'''
print(free_variables(build_table(source, "<s>", "exec")))
"""
result: dict[str, list[str]] = {}
for scope in walk_tables(table):
if scope.get_type() == "function":
frees = [s.get_name() for s in scope.get_symbols() if s.is_free()]
if frees:
result[scope.get_name()] = frees
return result
def cell_variables(table: symtable.SymbolTable) -> dict[str, list[str]]:
"""
Find cell variables (locals that are captured by nested scopes).
Returns {function_name: [cell_var_names]}.
"""
result: dict[str, list[str]] = {}
for scope in walk_tables(table):
if scope.get_type() == "function":
cells = [s.get_name() for s in scope.get_symbols() if s.is_cell()]
if cells:
result[scope.get_name()] = cells
return result
def global_names(table: symtable.SymbolTable) -> dict[str, list[str]]:
"""
Find `global` declarations in function scopes.
Returns {function_name: [global_decl_names]}.
"""
result: dict[str, list[str]] = {}
for scope in walk_tables(table):
if scope.get_type() == "function":
globs = [s.get_name() for s in scope.get_symbols() if s.is_global()]
if globs:
result[scope.get_name()] = globs
return result
# ─────────────────────────────────────────────────────────────────────────────
# 5. Scope summary report
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class ScopeSummary:
name: str
scope_type: str
depth: int
total: int
locals_: int
globals_: int
frees: int
cells: int
params: int
imported: int
children: int
def __str__(self) -> str:
indent = " " * self.depth
tag = f"[{self.scope_type}]"
return (
f"{indent}{tag:10s} {self.name:20s} "
f"syms={self.total} local={self.locals_} "
f"free={self.frees} cell={self.cells} "
f"param={self.params} import={self.imported} "
f"child_scopes={self.children}"
)
def _summarize_scope(
table: symtable.SymbolTable,
depth: int = 0,
) -> list[ScopeSummary]:
syms = table.get_symbols()
summary = ScopeSummary(
name=table.get_name(),
scope_type=table.get_type(),
depth=depth,
total=len(syms),
locals_=sum(1 for s in syms if s.is_local()),
globals_=sum(1 for s in syms if s.is_global()),
frees=sum(1 for s in syms if s.is_free()),
cells=sum(1 for s in syms if s.is_cell()),
params=sum(1 for s in syms if s.is_parameter()),
imported=sum(1 for s in syms if s.is_imported()),
children=len(table.get_children()),
)
results = [summary]
for child in table.get_children():
results.extend(_summarize_scope(child, depth + 1))
return results
def scope_report(source: str, filename: str = "<string>") -> list[ScopeSummary]:
"""
Build a full scope report for a source string.
Example:
for summary in scope_report(Path("app.py").read_text(), "app.py"):
print(summary)
"""
table = build_table(source, filename)
return _summarize_scope(table)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== symtable demo ===")
source = '''
import os
import sys
_CACHE: dict = {}
def outer(x: int, y: int = 0) -> int:
"""Outer function demonstrating closures."""
multiplier = 3
def inner(z: int) -> int:
# x and multiplier are free variables here
return x * multiplier + z
def another() -> int:
global _CACHE
_CACHE["key"] = x
return x
result = inner(y)
return result
class MyClass:
class_var = 42
def method(self, value: int) -> int:
return self.class_var + value
@staticmethod
def static_method(a: int, b: int) -> int:
return a + b
'''
table = build_table(source, "demo.py")
# ── scope_report ───────────────────────────────────────────────────────────
print("\n--- scope_report ---")
for summary in scope_report(source, "demo.py"):
print(summary)
# ── free_variables ─────────────────────────────────────────────────────────
print("\n--- free_variables ---")
frees = free_variables(table)
for fn, names in frees.items():
print(f" {fn}: {names}")
# ── cell_variables ─────────────────────────────────────────────────────────
print("\n--- cell_variables ---")
cells = cell_variables(table)
for fn, names in cells.items():
print(f" {fn}: {names}")
# ── global_names ───────────────────────────────────────────────────────────
print("\n--- global declarations ---")
globs = global_names(table)
for fn, names in globs.items():
print(f" {fn}: {names}")
# ── classify_symbols for 'outer' ───────────────────────────────────────────
print("\n--- classify_symbols in 'outer' ---")
outer_scope = find_scope(table, "outer", scope_type="function")
if outer_scope:
for sym in classify_symbols(outer_scope):
print(f" {sym}")
# ── Function-specific API ──────────────────────────────────────────────────
print("\n--- Function scope API for 'outer' ---")
if outer_scope and isinstance(outer_scope, symtable.Function):
print(f" parameters: {outer_scope.get_parameters()}")
print(f" locals: {outer_scope.get_locals()}")
print(f" globals: {outer_scope.get_globals()}")
print(f" frees: {outer_scope.get_frees()}")
print(f" cells: {outer_scope.get_cells()}")
# ── walk all scopes ────────────────────────────────────────────────────────
print("\n--- all scopes ---")
for scope in walk_tables(table):
children = len(scope.get_children())
print(f" {scope.get_type():8s} {scope.get_name()!r:20s} "
f"symbols={len(scope.get_symbols())} children={children}")
print("\n=== done ===")
For the ast alternative — ast.parse() builds an AST from source, and walking with ast.NodeVisitor or ast.walk() lets you inspect ast.Name, ast.Global, ast.Nonlocal, and scope-creating nodes (ast.FunctionDef, ast.ClassDef) manually — use ast for arbitrary structural source transformations, code generation, and lint rules that examine syntax tree shapes; use symtable when you specifically need the compiler’s resolved view of variable bindings (which names are free vs. local vs. global vs. cell after scope analysis) without implementing scope resolution yourself. For the dis / inspect alternative — dis.get_instructions() shows the bytecode including LOAD_FAST (local), LOAD_DEREF (free/cell), LOAD_GLOBAL (global), and LOAD_NAME operations that reveal how names are actually accessed at runtime; inspect.getclosurevars() returns the live values of free variables for a running closure — use dis when you want to audit the actual load opcodes after compilation; use inspect.getclosurevars() when you need the runtime cell contents; use symtable for static pre-compilation scope analysis before a function is ever called. The Claude Skills 360 bundle includes symtable skill sets covering build_table()/build_table_from_file() builders, walk_tables()/find_scope() tree traversal, SymbolInfo dataclass with classify_symbols()/all_symbols(), free_variables()/cell_variables()/global_names() targeted queries, and ScopeSummary with scope_report() full scope tree renderer. Start with the free tier to try scope analysis patterns and symtable pipeline code generation.