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
- Print a tree for a sample with ast.dump so you know the node names.
- Pick the node type, here Call.
- Decide what makes it unsafe. Here: a non-constant first argument.
- Report path and line so a developer can jump straight to it.
- Write sample files, one safe and one unsafe, and check the output.
- 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.