Find Dead Code in a Python Project Using AST
Walk a project tree, parse every Python file with ast, and list defined functions that are never called anywhere.
Python code
36 linesimport ast
import os
import sys
def find_dead_code(project_path):
defined_functions = {}
called_functions = set()
for root, dirs, files in os.walk(project_path):
for file in files:
if file.endswith('.py'):
filepath = os.path.join(root, file)
with open(filepath, 'r') as f:
try:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
defined_functions[node.name] = filepath
elif isinstance(node, ast.Call):
if isinstance(node.func, ast.Name):
called_functions.add(node.func.id)
except SyntaxError:
print(f"Syntax error in {filepath}")
dead_functions = [name for name in defined_functions if name not in called_functions]
return dead_functions
if __name__ == "__main__":
path = sys.argv[1] if len(sys.argv) > 1 else "."
dead = find_dead_code(path)
if dead:
print("Potentially dead functions:")
for func in dead:
print(f" {func}")
else:
print("No dead functions found.")
Output
Potentially dead functions:
unused_helper
legacy_cleanup
How it works
The script uses os.walk to traverse all .py files in the project directory. For each file it parses the AST with ast.parse and walks the tree collecting function definitions (ast.FunctionDef) into a dict and direct function calls (ast.Call with a simple name) into a set. After processing all files, any function name in the definition dict that never appears in the call set is considered potentially dead. This approach is static and may produce false positives for dynamically accessed functions, but it works well for local function names without decorator wrapping or imports.
Common mistakes
- Forgetting to filter out calls through object attributes (e.g. `obj.method`) — the script only catches plain name calls.
- Assuming the AST parser can handle invalid syntax — the try/except on SyntaxError prevents a crash on malformed files.
- Missing functions defined inside classes or nested scopes because `ast.walk` only visits `FunctionDef` nodes at the top level.
- Overlooking functions imported from other modules — the script only finds calls by the same simple name.
Variations
- Use `ast.NodeVisitor` instead of `ast.walk` for more granular control over node types.
- Add support for method calls by checking `ast.Attribute` nodes and resolving the method name.
Real-world use cases
- Sweep a legacy repository for unused private functions before a major refactor.
- Integrate into a CI pipeline to flag newly added dead code on every pull request.
- Combine with code coverage reports to identify functions that are defined but never exercised.
Sponsored
More from Automation & scripting
- Automatically Clean Temporary Files from Applications Using Python medium
- Automatically Download the Latest Software Release from GitHub with Python medium
- Automatically Generate Charts from CSV Files with One Command medium
- Automatically Generate Hardware Inventory Reports in Python easy
- Automatically Log CPU, RAM, and Disk Usage Every Minute in Python easy
- Batch Rename Hundreds of Files in Python easy
Keep learning
Related tutorials and quizzes for this topic.