Trimming Context: Dependency Graphs Instead of Full Repos
For code tasks, the fastest way to bloat a prompt is to paste an entire repository when the model only needs a handful of files.
A dependency graph lets you send structure instead of bulk: a map of what imports what, plus the specific snippets that actually matter.
Summary
A dependency graph is a small data structure that records which files or modules import, call, or otherwise depend on which others.
Instead of pasting a whole codebase into a prompt, you walk this graph from the file being changed and pull in only the files it's structurally connected to.
This keeps the prompt small and focused, which lowers cost and avoids diluting the model's attention with unrelated code.
It also scales better than manually guessing which files are relevant, since the graph is built once and reused across many prompts.
This page walks through building a minimal dependency graph in Python, using it to select relevant files for a change, and sending that trimmed context to Claude.
Recipe
Quick-reference recipe card - copy-paste ready.
import ast
import os
def get_imports(filepath: str) -> set[str]:
with open(filepath) as f:
tree = ast.parse(f.read(), filename=filepath)
imports = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imports.update(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom) and node.module:
imports.add(node.module)
return importsWhen to reach for this:
- The task is about one file or function, but that file lives inside a much larger repo.
- You want Claude to understand how a change ripples to callers, without sending every caller's full source.
- The repo is too large to fit in a single prompt even at a generous context window.
- You're running the same kind of code-review or refactor prompt repeatedly and want a reusable selection step.
Working Example
import ast
import os
import anthropic
client = anthropic.Anthropic()
def get_imports(filepath: str) -> set[str]:
"""Return the set of module names a Python file imports."""
with open(filepath) as f:
tree = ast.parse(f.read(), filename=filepath)
imports = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imports.update(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom) and node.module:
imports.add(node.module)
return imports
def build_dependency_graph(repo_root: str) -> dict[str, set[str]]:
"""Map each file path to the module names it imports."""
graph = {}
for dirpath, _, filenames in os.walk(repo_root):
for name in filenames:
if name.endswith(".py"):
path = os.path.join(dirpath, name)
graph[path] = get_imports(path)
return graph
def module_name_to_path(repo_root: str, module: str) -> str | None:
"""Best-effort mapping from a module name back to a file path."""
candidate = os.path.join(repo_root, *module.split(".")) + ".py"
return candidate if os.path.exists(candidate) else None
def relevant_files(repo_root: str, target_file: str, graph: dict[str, set[str]]) -> set[str]:
"""Files the target imports, plus files that import the target."""
related = {target_file}
for module in graph.get(target_file, set()):
path = module_name_to_path(repo_root, module)
if path:
related.add(path)
for path, imports in graph.items():
target_module = os.path.splitext(os.path.relpath(target_file, repo_root))[0].replace(os.sep, ".")
if target_module in imports:
related.add(path)
return related
def prompt_with_trimmed_context(repo_root: str, target_file: str, question: str) -> str:
graph = build_dependency_graph(repo_root)
files = relevant_files(repo_root, target_file, graph)
context_blocks = []
for path in sorted(files):
with open(path) as f:
context_blocks.append(f"# {os.path.relpath(path, repo_root)}\n{f.read()}")
prompt = f"{question}\n\n" + "\n\n".join(context_blocks)
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=500,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
answer = prompt_with_trimmed_context(
repo_root="./billing_service",
target_file="./billing_service/retry_queue.py",
question="If I add a max_attempts parameter to retry_queue.py, what callers break?",
)
print(answer)What this demonstrates:
build_dependency_graphwalks a repo once and records each file's imports, which is cheap to compute and reusable across many prompts.relevant_fileswalks one hop in both directions: what the target file imports, and what imports the target file.- Only the files in that one-hop neighborhood are read and included in the prompt, not the entire repo.
- The graph-building step is separated from the prompt-building step, so the same graph can answer many different questions about different files.
Deep Dive
How It Works
ast.parseturns Python source into a syntax tree without executing it, which is the safe, standard way to inspect imports.- Walking the tree with
ast.walkfinds everyImportandImportFromnode regardless of how deep it's nested in the file. - The graph itself is just a dictionary mapping file paths to the module names they import, which is enough to answer "what does X depend on" and, by inverting the lookup, "what depends on X."
- Selecting a one-hop neighborhood (direct dependencies and direct dependents) is usually enough context for most single-file changes; deeper traversal pulls in more files at the cost of a larger prompt.
Choosing Graph Depth
| Depth | What's Included | Best Fit |
|---|---|---|
| 0 hops | Only the target file | Isolated bug fixes, style questions |
| 1 hop | Target file's direct imports and direct importers | Most refactors and "what breaks" questions |
| 2+ hops | Transitive dependencies | Cross-cutting changes to shared utilities or core types |
Going beyond one or two hops usually reintroduces the problem this technique is meant to solve: the prompt grows back toward "the whole repo," just with extra steps.
Python Notes
# Guard against files that fail to parse (syntax errors, non-UTF8, etc.)
def get_imports_safe(filepath: str) -> set[str]:
try:
return get_imports(filepath)
except (SyntaxError, UnicodeDecodeError):
return set()Real repos have generated files, vendored code, or occasional syntax errors from in-progress edits.
Wrapping the parse step so one bad file doesn't crash the whole graph build keeps this practical to run on a real codebase.
Gotchas
- Relative imports resolve incorrectly.
astreports the literal module string, so a relative import likefrom . import utilsneeds package-aware resolution, not simple path joining. Fix: resolve relative imports against the importing file's package path, or restrict the graph to absolute imports for a simpler first version. - Dynamic imports are invisible to
ast. Code that imports viaimportlib.import_module(computed_name)won't show up as a staticImportnode. Fix: treat dynamically-loaded modules as a known gap, and manually add known dynamic dependencies to the graph. - The graph goes stale if it's built once and cached forever. A codebase that adds new imports won't be reflected in an old graph. Fix: rebuild the graph on each run for small repos, or invalidate the cache on file changes for larger ones.
- Circular dependencies can cause unbounded traversal at higher hop counts. Two files that import each other, chained through several more files, can make a "2 hop" traversal pull in far more than expected. Fix: track visited files during traversal and stop revisiting the same file.
- A one-hop neighborhood can still miss the actually relevant file. Some architectural changes affect a file with no direct import relationship, such as a config value read by convention rather than import. Fix: treat the graph as a starting point, not a guarantee, and let a human or a cheap relevance-scoring pass add files the graph misses.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Manual snippet selection | The task touches one or two files you already know | The repo is large enough that manual selection is slow or error-prone |
| Full-repo context | The repo is small enough to fit comfortably and cost isn't a concern | Cost matters, or the repo is large enough to risk context rot |
| Retrieval-augmented search (embedding-based) | You need semantic relevance, not just import relationships (e.g. "find code related to refunds") | The relationship you care about is structural (what calls what), which a dependency graph answers more precisely |
| A dedicated code-analysis tool (e.g. a language server's call graph) | You need cross-language or highly accurate call-graph data at scale | A lightweight, single-language script is sufficient for the task at hand |
FAQs
What's the difference between a dependency graph and just grepping for related files?
Grep finds text matches, which can both miss relevant files (no shared keyword) and include irrelevant ones (a coincidental string match).
A dependency graph is built from actual import relationships, so it's more precise for the specific question of "what does this file depend on."
Do I need a real static-analysis library, or is a simple `ast` script enough?
For a single-language, moderately sized repo, a simple ast-based script like the one above is often enough.
- For very large or multi-language repos, a dedicated tool (a language server,
pydeps, or similar) will be more robust. - Start simple and only reach for heavier tooling once the simple version demonstrably misses cases you care about.
How many hops should I traverse in the graph?
One hop (direct imports and direct importers) is a reasonable default for most single-file changes.
- Increase to two hops for changes to widely-shared utility code.
- Going much beyond that usually reintroduces the "whole repo" problem this technique exists to avoid.
Does this replace summarization or model tiering?
No, they solve different problems and typically get combined.
- Dependency-graph trimming decides which files to include.
- Summarization condenses a file that's still too long even after it's selected.
- Model tiering decides which model handles the resulting, now-smaller prompt.
What happens if the graph misses a relevant file?
The prompt will lack context the model needed, which can produce an incomplete or wrong answer.
Treat the graph as a starting point: for high-stakes changes, have a human review which files were selected before sending the prompt.
Can this approach work for languages other than Python?
Yes, the concept is language-agnostic: any language with a static import or require/use statement can be parsed similarly.
The specific parsing step (ast here) is Python-specific; other languages need their own parser or a general-purpose static-analysis tool.
How do I handle circular imports when building the graph?
Circular imports are fine for graph construction since you're just recording import statements, not executing them.
They matter for traversal: track visited files so a cycle doesn't cause the same file to be added, or worse, cause unbounded recursion in a naive traversal implementation.
Is building the graph itself expensive?
Building it is a local, offline operation, it parses source files on disk and costs no API tokens.
It only needs to be rebuilt when the codebase changes meaningfully, so the cost is amortized across every prompt that reuses it.
Should I include the graph structure itself in the prompt, or just the selected files?
Both can help, depending on the question.
- For "what breaks if I change this," listing which files import the target (the graph edges) gives Claude useful structure even before it reads the code.
- For most tasks, the selected file contents matter more than the graph shape itself.
Why not just send file names and let Claude ask for the ones it needs?
That's a valid alternative, typically implemented as a tool-use loop where Claude requests specific files on demand.
It trades one extra round trip (and the token cost of that round trip) for potentially better relevance than a pre-computed graph, and it pairs well with the tool-call caching described elsewhere in this section.
Does a bigger context window make dependency-graph trimming unnecessary?
No, a larger window changes what you can fit, not what you should send.
Unnecessary files still cost tokens, and irrelevant code can still dilute the model's focus on the part of the codebase that actually matters, which is the context rot problem covered elsewhere in this section.
What's a good sign that the one-hop neighborhood is too small?
If Claude's answers repeatedly reference a file or a concept it wasn't given, that's a signal the traversal depth or the resolution logic (relative imports, dynamic imports) is missing something.
Widen the hop count or patch the specific resolution gap, rather than defaulting to sending the whole repo.
Related
- How Context Engineering Reduces Token Spend - the broader mental model this technique fits into.
- Prompt and Context Engineering Basics - hands-on examples of trimming prompts, including a simpler relevance-scoring approach.
- Summarization Before the Call - what to do when a selected file is still too long.
- Avoiding Redundant Tool Calls in Multi-Turn Agent Loops - a tool-use alternative to pre-computing a graph.
- Context Rot: Why More Tokens Doesn't Mean Better Answers - why including unrelated files can hurt answer quality, not just cost.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, pricing, and SDK versions move quickly - verify current specifics at platform.claude.com/docs before relying on them.