Safeguard
Developer Tools•

How do you write a Python security check with the ast module?

Parse Python with ast.parse, subclass NodeVisitor, match one risky call, report file and line, then test on safe and unsafe samples. Short working example.

Safeguard Security Team
3 min read

To write a Python security check with the standard library, parse the source with ast.parse, subclass ast.NodeVisitor, add a visit method for the node type you want, and record the line number when it matches. No extra packages are needed, which makes it a good first custom check.

What the standard library gives you

The ast module processes trees of the Python abstract syntax grammar. Its NodeVisitor class walks the tree and calls a visitor function for every node it finds. The default implementation looks for a method named visit followed by the node class name, such as visit_Call, and falls back to generic_visit if none exists. You subclass it and add the methods you need.

A small check

This example flags calls to eval and exec with a non-constant argument:

import ast, sys

class RiskyCalls(ast.NodeVisitor):
    def __init__(self, path):
        self.path = path
        self.findings = []

    def visit_Call(self, node):
        name = getattr(node.func, "id", None)
        if name in ("eval", "exec") and node.args:
            if not isinstance(node.args[0], ast.Constant):
                self.findings.append((self.path, node.lineno, name))
        self.generic_visit(node)

for path in sys.argv[1:]:
    tree = ast.parse(open(path).read(), filename=path)
    v = RiskyCalls(path)
    v.visit(tree)
    for p, line, name in v.findings:
        print(f"{p}:{line}: {name} called with a non-constant argument")

Call generic_visit at the end of each method so the walk continues into child nodes. Skipping it is a common reason a check stops finding nested cases.

Steps

  1. Print a tree for a sample with ast.dump so you know the node names.
  2. Pick the node type, here Call.
  3. Decide what makes it unsafe. Here: a non-constant first argument.
  4. Report path and line so a developer can jump straight to it.
  5. Write sample files, one safe and one unsafe, and check the output.
  6. Run it on the whole repository and read the results before wiring it into CI.

Known limits of this check

  • It matches the bare names eval and exec. A call such as builtins.eval(...) is an attribute call and will be missed.
  • A variable that holds a constant string is reported even though it is safe.
  • It does not know where the value came from. It only knows the argument is not a literal.

Writing these limits next to the check is good practice. A reader should know what a clean result does and does not mean.

Keep it working across Python versions

The documentation notes that the abstract syntax may change with each Python release, and that node constructor behavior has changed in recent versions. Run your sample tests on every Python version you support, and re-run them when you add a new one.

Quick check

Should I use ast or regex for a quick check?

Use ast when the question is about code structure, such as whether an argument is a literal. Use regex for plain text with no structure, such as a token format.

Why call generic_visit?

Without it the visitor does not descend into the children of the node you handled, so nested calls inside arguments can be skipped.

Safeguard's scanning covers dependencies and SBOMs, and its reachability analysis helps show which findings your code can actually call. You can run a free scan on a repository.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.