Python symtable: Analyze Name Scopes

Published on: August 27, 2026
Reading time: 9 minutes
Detailed view of programming code in a dark theme on a computer screen.

The symtable module exposes the symbol tables produced by Python’s compiler before bytecode generation. It reveals which names belong to each scope, which ones are parameters, imports, locals, globals, nonlocals, free variables, references, assignments, and nested namespaces. This layer is valuable for linters, refactoring tools, educational software, static analysis, and closure inspection.

A symbol table does not execute the program and cannot resolve every dynamic behavior. Assignments through globals(), setattr(), dynamic imports, decorators, metaclasses, and monkey patching remain beyond complete static understanding. Even so, the table reflects the compiler’s actual lexical-scope decisions and is more reliable than guessing bindings from source text alone.

Create a symbol table

symtable.symtable() accepts source code, a filename, and a compilation mode.

import symtable

source = """
x = 10

def add(y):
    z = x + y
    return z
"""

table = symtable.symtable(source, "example.py", "exec")
print(table.get_type())

The mode can be exec, eval, or single, matching the modes accepted by compile().

Handle syntax errors

Symbol-table construction uses the compiler and can raise SyntaxError.

try:
    table = symtable.symtable(source, path, "exec")
except SyntaxError as error:
    report(path, error.lineno, error.offset, error.msg)

When analyzing a project, record the error for one file and continue with the rest rather than discarding the whole batch.

The top-level SymbolTable

The root table represents the module, expression, or interactive input.

print(table.get_name())
print(table.get_type())
print(table.get_lineno())

The name and source line are useful for diagnostics, although their exact meaning depends on the table type and Python version.

List identifiers

get_identifiers() returns the names known to a scope.

for name in sorted(table.get_identifiers()):
    print(name)

The result includes bindings and references that matter to the compiler, not merely names that appear on the left side of an assignment.

Inspect a name with lookup

lookup(name) returns a Symbol object containing classification information.

symbol = table.lookup("x")
print(symbol.is_global())
print(symbol.is_assigned())
print(symbol.is_referenced())

Looking up an unknown name can raise an error, so check identifiers first or handle the exception.

Local variables

is_local() reports that a name belongs to the current local scope.

function_table = table.lookup("add").get_namespace()
for name in function_table.get_identifiers():
    symbol = function_table.lookup(name)
    if symbol.is_local():
        print("local", name)

Parameters are also local names, but they have the more specific is_parameter() classification.

Parameters

Function tables expose parameter names directly.

for name in function_table.get_parameters():
    print(name)

This is useful for detecting unused parameters, shadowing, naming-policy violations, and arguments required only by an interface.

Implicit and declared globals

A name can be global because no local binding exists or because the function contains an explicit global statement.

counter = 0

def increment():
    global counter
    counter += 1

Use is_global() and, where available, is_declared_global() to distinguish cases when the difference matters to a diagnostic.

Nonlocal names

nonlocal binds a name to an enclosing function scope rather than the module.

def outer():
    total = 0

    def inner():
        nonlocal total
        total += 1
        return total

    return inner

The inner symbol can be recognized with is_nonlocal().

Free variables

A free variable is read by a nested function but supplied by an enclosing scope.

inner_table = outer_table.get_children()[0]
print(inner_table.get_frees())

These names participate in closure construction and eventually appear in code-object metadata.

Cell variables

When a local variable is captured by a nested function, the compiler stores it in a cell. The symbol-table API can reveal the relationship by comparing locals in the outer table with free variables in children.

After compilation, code objects expose related details through co_cellvars and co_freevars. Use symbol tables for source-level scope analysis and code objects for post-compilation inspection.

Imported names

is_imported() reports names introduced by import statements.

import json as serializer
from pathlib import Path

The local alias, not necessarily the original module name, becomes the binding that appears in the table.

Assignments

is_assigned() identifies names that receive a binding in the current scope.

Assignments include more than the = operator: loop targets, imports, definitions, exception targets, comprehensions, and pattern-matching captures can all create bindings. Combine the table with AST nodes when you need to classify the source of the assignment.

References

is_referenced() indicates that the name is used in an expression or another relevant operation.

A value that is assigned but never referenced may deserve a warning, but tools should account for public APIs, required callback signatures, decorators, side effects, and naming conventions such as a leading underscore.

Nested namespaces

A symbol representing a function or class may own one or more child symbol tables.

symbol = table.lookup("add")
if symbol.is_namespace():
    namespace = symbol.get_namespace()

get_namespaces() is useful when one source name can correspond to multiple nested namespaces in supported language constructs.

Traverse child tables

get_children() returns nested tables.

def display(table, level=0):
    print("  " * level, table.get_type(), table.get_name())
    for child in table.get_children():
        display(child, level + 1)

This produces a tree of module, function, class, lambda, comprehension, and other compiler-created scopes.

Function-table helpers

Function symbol tables provide helpers such as get_parameters(), get_locals(), get_globals(), get_nonlocals(), and get_frees(), depending on the Python release.

Use feature detection when supporting multiple versions rather than assuming every helper exists everywhere.

Class scopes differ from function scopes

A class body executes in its own namespace, but methods do not automatically capture class attributes as lexical variables.

class Example:
    value = 10

    def method(self):
        return value

Inside the method, value does not automatically mean Example.value. Attribute access must normally be explicit.

The implicit __class__ cell

The compiler can create a special __class__ reference for features such as zero-argument super().

Analysis tools must be prepared for compiler-generated or implicit symbols that do not look like ordinary user assignments.

Lambda scopes

A lambda creates a function scope and appears as a child table.

double = lambda x: x * 2

The table name can reflect an internal compiler label rather than a user-chosen identifier.

Comprehension scopes

Modern Python comprehensions have their own scope, so the iteration variable does not leak into the enclosing block.

squares = [x * x for x in values]

The symbol table can expose a child namespace associated with the comprehension.

Generator expressions

Generator expressions also create internal scopes and can capture names from their surroundings.

Inspect child tables to find free variables and implicit parameters used by the generated code.

Async functions

Asynchronous functions follow ordinary lexical-scope rules even though their execution and suspension behavior differ.

A symbol table cannot determine whether a coroutine is awaited correctly; that requires AST and control-flow analysis.

Annotations

Type annotations can introduce references and bindings according to syntax and Python version. Deferred annotation behavior and newer annotation mechanisms affect when expressions are evaluated.

Do not infer complete runtime behavior from the presence of a symbol in an annotation-related scope.

Type aliases and newer table types

Recent language releases introduce constructs that can create additional symbol-table types, including scopes related to type parameters and aliases.

Use the enums and APIs of the running Python rather than comparing one hard-coded set of strings. Add version-specific tests for new syntax.

Pattern matching bindings

Structural pattern matching can bind local names.

match value:
    case {"id": identifier}:
        use(identifier)

The symbol table records the binding, while the AST explains that it originated from a pattern.

Exception targets

The name in except Exception as error is a local binding with special cleanup rules after the handler.

A symbol table does not simulate time, so checking whether a name is available after a particular statement requires control-flow analysis.

Deletion

del name affects a binding, but the table is not a timeline of values.

Detecting use before definition or use after deletion requires a control-flow graph and data-flow propagation.

Understanding UnboundLocalError

An assignment anywhere in a function can cause the compiler to classify a name as local, even when a read appears first.

x = 10

def example():
    print(x)
    x = 20

The symbol table explains why x is local and why execution raises UnboundLocalError.

Shadowing

A local variable can shadow an import, built-in, parameter, or enclosing name.

Not all shadowing is wrong. A linter should consider scope length, public conventions, readability, and whether the original symbol remains needed.

Built-in names

A name that is neither local nor explicitly global may be resolved from built-ins at runtime.

The symbol table cannot guarantee the original built-in will be used because globals and __builtins__ can be altered dynamically.

Safer renaming

For a reliable variable rename, combine tokens for precise source spans, AST for syntactic context, and symbol tables for binding classification.

Do not replace every textual occurrence. Attributes, dictionary keys, strings, comments, and names from other scopes are different entities.

Find unused parameters

Compare function parameters with referenced symbols in the same namespace.

Ignore conventional placeholders such as _ and parameters required by protocols, callbacks, decorators, framework hooks, or overridden methods.

Find unused imports

An imported symbol that is never referenced can be a cleanup opportunity, but imports may intentionally register plugins or trigger side effects.

Support exceptions, re-exports, and __all__ before applying automatic deletion.

Global mutable state

The table can identify global access, but it does not determine mutability, thread safety, or whether an operation modifies a referenced object.

Use AST and data-flow analysis to distinguish reading a global object from mutating shared state.

Combine with tokenize

tokenize provides comments, exact spelling, and locations. Symbol tables provide binding and scope.

See Python tokenize for the lexical layer.

Combine with AST

The AST shows where a name appears and what construct contains it. The symbol table shows how the compiler classifies the name.

See Python ast for structural analysis.

Combine with dis

After symbol analysis, the compiler selects appropriate load and store instructions. Comparing the table with bytecode reveals the result of those decisions.

See Python dis.

Project-wide analysis

Each file has its own module table. Resolving imports, re-exports, package aliases, and public APIs requires a project index.

Do not execute imports during static analysis because imported modules may perform arbitrary side effects.

Read source with the right encoding

Use tokenize.open() to read Python source before passing text to symtable().

Preserve the real filename so syntax errors and diagnostics point to the correct location.

Limit large inputs

When analyzing uploads or untrusted repositories, limit bytes, lines, nesting, files, and total processing time.

Public services should run compiler-based analysis in an isolated worker process with CPU and memory boundaries.

Python-version differences

Table types, helper methods, and classifications evolve as new syntax is added.

Test every supported Python version and use capability checks instead of assuming one runtime’s behavior.

symtable is not a type checker

The module does not resolve types, overloads, protocols, generics, or inference. It describes lexical name binding.

Integrate a dedicated type checker when type semantics matter.

symtable is not control-flow analysis

The table does not report execution order, reachability, or whether a variable is defined on every path.

Build a control-flow graph or use an existing analysis framework for temporal rules.

Security boundaries

A clean symbol table does not make code safe to execute. Imports, decorators, descriptors, and runtime calls can perform arbitrary actions.

Never use a symbol-name allowlist as a sandbox.

Testing

Cover modules, functions, nested functions, closures, classes, lambdas, comprehensions, generators, async functions, global, nonlocal, annotations, pattern matching, and syntax errors.

Only compare static results with execution for trusted fixtures.

Common mistakes

Common failures include inferring bindings from text alone, confusing implicit and declared globals, ignoring comprehension scopes, treating class bodies like functions, renaming without namespace awareness, using symtable as a type checker, executing imports to resolve names, and depending on APIs from one Python version only.

Conclusion

symtable reveals how Python’s compiler organizes names and scopes before bytecode generation. Use lookup() and Symbol properties to identify locals, globals, imports, parameters, nonlocals, and free variables, and traverse get_children() to build the namespace tree.

Combine symbol tables with tokens, AST, and control-flow analysis for precise linting and refactoring. Consult the official symtable documentation.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python dis: Understand Bytecode

    Learn Python dis to inspect bytecode instructions, jumps, stack effects, adaptive caches, and optimizations without relying on unstable internals.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python tokenize: Read Source Code Tokens

    Learn Python tokenize to read tokens, comments, encodings, indentation, and positions, then transform and rebuild source safely.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python zipapp: Build Executable .pyz Files

    Learn Python zipapp to build .pyz files, define entry points, bundle pure dependencies, handle resources, and distribute secure CLI tools.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python sysconfig: Paths and Build Info

    Learn Python sysconfig to inspect paths, schemes, headers, compiler flags, ABI details, native extensions, and virtual environments.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    A person in a hoodie coding on dual monitors, depicting cybersecurity and hacking themes.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python marshal: Internal Binary Format

    Learn Python marshal for internal objects and bytecode, including versions, allow_code, disposable caches, limits, and untrusted-input risks.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    A close-up view of fresh, green cucumbers ready for pickling and preservation in Estonia.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python copyreg: Customize Pickle Types

    Learn Python copyreg to customize pickle reducers, version serialized state, avoid global conflicts, and test safely across processes.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026