<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Urjit UPADHYAY</title>
    <description>The latest articles on DEV Community by Urjit UPADHYAY (@urjit_upadhyay).</description>
    <link>https://dev.to/urjit_upadhyay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4022608%2Fadce196d-9348-43ba-88a2-eed7a55f3759.png</url>
      <title>DEV Community: Urjit UPADHYAY</title>
      <link>https://dev.to/urjit_upadhyay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/urjit_upadhyay"/>
    <language>en</language>
    <item>
      <title>Building an AST Code Verifier Without NetworkX, GitPython, or Any Dependencies</title>
      <dc:creator>Urjit UPADHYAY</dc:creator>
      <pubDate>Thu, 03 Sep 2026 03:00:40 +0000</pubDate>
      <link>https://dev.to/urjit_upadhyay/building-an-ast-code-verifier-without-networkx-gitpython-or-any-dependencies-20dd</link>
      <guid>https://dev.to/urjit_upadhyay/building-an-ast-code-verifier-without-networkx-gitpython-or-any-dependencies-20dd</guid>
      <description>&lt;p&gt;You end up learning why those packages exist in the first place.&lt;/p&gt;

&lt;p&gt;For the Zero Dependency Hackathon 2026, I built Proofline, a pure Python static analysis tool that works as a verification gate for code changes.&lt;/p&gt;

&lt;p&gt;The main rule for the project was simple:&lt;/p&gt;

&lt;p&gt;No third-party dependencies.&lt;/p&gt;

&lt;p&gt;So there was no networkx, no GitPython, no pre-commit, no fastapi, and no watchdog.&lt;/p&gt;

&lt;p&gt;Everything had to be built using Python's standard library.&lt;/p&gt;

&lt;p&gt;What is Proofline?&lt;/p&gt;

&lt;p&gt;The idea behind Proofline came from working with AI-generated code.&lt;/p&gt;

&lt;p&gt;Most linters are great at finding things like syntax issues, formatting problems, unused variables, and other common mistakes. But I wanted to look at something slightly different:&lt;/p&gt;

&lt;p&gt;What actually changed, and what could that change affect?&lt;/p&gt;

&lt;p&gt;Proofline parses Python code using the AST and builds information about functions, classes, callers, routes, and file changes.&lt;/p&gt;

&lt;p&gt;For example, it tries to detect things like:&lt;/p&gt;

&lt;p&gt;changed exception behavior&lt;br&gt;
orphaned routes&lt;br&gt;
broken caller relationships&lt;br&gt;
unexpected changes between scans&lt;br&gt;
dynamically registered routes that can't be completely verified statically&lt;/p&gt;

&lt;p&gt;It's not supposed to replace Ruff, Flake8, or similar tools.&lt;/p&gt;

&lt;p&gt;The question I'm trying to answer is more like:&lt;/p&gt;

&lt;p&gt;"What is the blast radius of this change, and how confident are we about it?"&lt;/p&gt;

&lt;p&gt;Building it without the usual libraries&lt;/p&gt;

&lt;p&gt;This was probably the most interesting part of the hackathon.&lt;/p&gt;

&lt;p&gt;If I were building this normally, I'd probably reach for GitPython for Git operations and NetworkX for the call graph.&lt;/p&gt;

&lt;p&gt;But I couldn't.&lt;/p&gt;

&lt;p&gt;So I had to build smaller versions of those pieces myself.&lt;/p&gt;

&lt;p&gt;Diff detection&lt;/p&gt;

&lt;p&gt;Instead of asking Git which files changed, Proofline walks the project using pathlib.rglob() and calculates SHA-256 hashes using hashlib.&lt;/p&gt;

&lt;p&gt;The basic idea is:&lt;/p&gt;

&lt;p&gt;find files&lt;br&gt;
   ↓&lt;br&gt;
read file&lt;br&gt;
   ↓&lt;br&gt;
calculate hash&lt;br&gt;
   ↓&lt;br&gt;
compare with previous hash&lt;br&gt;
   ↓&lt;br&gt;
analyze changed files&lt;/p&gt;

&lt;p&gt;It's obviously not as efficient as Git's own implementation, but it works without depending on GitPython.&lt;/p&gt;

&lt;p&gt;Building the symbol map&lt;/p&gt;

&lt;p&gt;For the Python analysis, I used the built-in ast module.&lt;/p&gt;

&lt;p&gt;ast.NodeVisitor lets me walk through the syntax tree and collect functions, classes, and other symbols.&lt;/p&gt;

&lt;p&gt;That gives Proofline a basic understanding of what's inside each file before trying to connect everything together.&lt;/p&gt;

&lt;p&gt;Building the caller graph&lt;/p&gt;

&lt;p&gt;This was another place where I initially thought, "I'll just use NetworkX."&lt;/p&gt;

&lt;p&gt;Obviously, I couldn't.&lt;/p&gt;

&lt;p&gt;So the first version was basically an adjacency list:&lt;/p&gt;

&lt;p&gt;dict[str, set[str]]&lt;/p&gt;

&lt;p&gt;A function becomes a node and calls between functions become edges.&lt;/p&gt;

&lt;p&gt;It's much simpler than a full graph library, but for what Proofline needs, it was enough to get started.&lt;/p&gt;

&lt;p&gt;Git hook&lt;/p&gt;

&lt;p&gt;I also wanted Proofline to work automatically before code gets committed.&lt;/p&gt;

&lt;p&gt;Instead of using the pre-commit package, the tool creates a small Bash/PowerShell wrapper inside:&lt;/p&gt;

&lt;p&gt;.git/hooks/pre-commit&lt;/p&gt;

&lt;p&gt;So the verification can happen as part of the normal commit flow.&lt;/p&gt;

&lt;p&gt;Dashboard&lt;/p&gt;

&lt;p&gt;For the dashboard, I used Python's built-in http.server.&lt;/p&gt;

&lt;p&gt;For live updates, I used Server-Sent Events (SSE).&lt;/p&gt;

&lt;p&gt;Again, no web framework.&lt;/p&gt;

&lt;p&gt;Then I ran into dynamic dispatch&lt;/p&gt;

&lt;p&gt;This was the part that took the most time.&lt;/p&gt;

&lt;p&gt;At first, the AST-based caller graph looked pretty good.&lt;/p&gt;

&lt;p&gt;The process was straightforward:&lt;/p&gt;

&lt;p&gt;parse AST&lt;br&gt;
   ↓&lt;br&gt;
find function calls&lt;br&gt;
   ↓&lt;br&gt;
connect callers and callees&lt;/p&gt;

&lt;p&gt;Then I started testing cases involving dynamic behavior.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;getattr(obj, "method_name")()&lt;/p&gt;

&lt;p&gt;Or routes that are registered dynamically by a framework.&lt;/p&gt;

&lt;p&gt;The problem is that the AST tells you what the source code looks like.&lt;/p&gt;

&lt;p&gt;It doesn't necessarily tell you what the program will do at runtime.&lt;/p&gt;

&lt;p&gt;I initially tried to solve this by writing another AST pass that would evaluate variables and follow dynamically registered routes.&lt;/p&gt;

&lt;p&gt;That quickly became messy.&lt;/p&gt;

&lt;p&gt;There were too many possible cases.&lt;/p&gt;

&lt;p&gt;Instead of spending the entire project trying to build a mini Python interpreter, I changed the approach.&lt;/p&gt;

&lt;p&gt;I added a route_detector.py that looks for common route/decorator patterns.&lt;/p&gt;

&lt;p&gt;But there's an important distinction:&lt;/p&gt;

&lt;p&gt;The results from this detector are marked INFERRED, not PROVEN.&lt;/p&gt;

&lt;p&gt;That was a design decision I was quite happy with.&lt;/p&gt;

&lt;p&gt;If static analysis isn't sure about something, I would rather show that uncertainty than pretend the result is guaranteed.&lt;/p&gt;

&lt;p&gt;The performance problem&lt;/p&gt;

&lt;p&gt;The other thing I didn't expect was how noticeable file hashing would be.&lt;/p&gt;

&lt;p&gt;Git is extremely fast at figuring out what changed because it has its own index and optimized internals.&lt;/p&gt;

&lt;p&gt;My implementation was doing something much simpler:&lt;/p&gt;

&lt;p&gt;walk files&lt;br&gt;
   ↓&lt;br&gt;
read files&lt;br&gt;
   ↓&lt;br&gt;
hash files&lt;br&gt;
   ↓&lt;br&gt;
compare hashes&lt;/p&gt;

&lt;p&gt;On a medium-sized repository, the initial scan took a little over a second on my tests.&lt;/p&gt;

&lt;p&gt;That's not terrible, but it was definitely slower than just asking Git.&lt;/p&gt;

&lt;p&gt;And honestly, this was a useful lesson.&lt;/p&gt;

&lt;p&gt;It made me understand why Git has an index instead of simply scanning the entire repository every time.&lt;/p&gt;

&lt;p&gt;Adding a cache&lt;/p&gt;

&lt;p&gt;The solution was to add a cache:&lt;/p&gt;

&lt;p&gt;.proofline/cache/&lt;/p&gt;

&lt;p&gt;Each file's SHA-256 hash is used as the key.&lt;/p&gt;

&lt;p&gt;The first scan still has to do the expensive work:&lt;/p&gt;

&lt;p&gt;File&lt;br&gt;
 ↓&lt;br&gt;
Hash&lt;br&gt;
 ↓&lt;br&gt;
Parse AST&lt;br&gt;
 ↓&lt;br&gt;
Build symbols&lt;br&gt;
 ↓&lt;br&gt;
Store result&lt;/p&gt;

&lt;p&gt;But after that:&lt;/p&gt;

&lt;p&gt;File&lt;br&gt;
 ↓&lt;br&gt;
Hash&lt;br&gt;
 ↓&lt;br&gt;
Cache hit&lt;br&gt;
 ↓&lt;br&gt;
Reuse analysis&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc1wqlfmk93dr2fac2l6l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc1wqlfmk93dr2fac2l6l.png" alt="AST vs Dynamic Dispatch Technical Diagram" width="623" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So unchanged files don't need to be parsed again.&lt;/p&gt;

&lt;p&gt;On subsequent scans, the difference is significant, with unchanged repositories getting down to millisecond-level analysis in my tests.&lt;/p&gt;

&lt;p&gt;Where Proofline makes sense&lt;/p&gt;

&lt;p&gt;I don't think this is a tool that every Python project needs.&lt;/p&gt;

&lt;p&gt;It makes more sense for projects where:&lt;/p&gt;

&lt;p&gt;AI-generated code is being reviewed regularly&lt;br&gt;
you want an additional verification step before merging&lt;br&gt;
you want to understand the impact of a code change&lt;br&gt;
you don't want to add a large dependency tree for a small internal tool&lt;br&gt;
you want a local dashboard without setting up another service&lt;/p&gt;

&lt;p&gt;There are also obvious cases where it isn't a great fit.&lt;/p&gt;

&lt;p&gt;If a project relies heavily on metaprogramming or runtime-generated behavior, static analysis can only go so far.&lt;/p&gt;

&lt;p&gt;That's one of the limitations I'm deliberately keeping visible.&lt;/p&gt;

&lt;p&gt;What's next?&lt;/p&gt;

&lt;p&gt;There are a few things I'd like to work on next:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fctrl5z0tinjlsruur0ox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fctrl5z0tinjlsruur0ox.png" alt="Proofline System Architecture" width="710" height="563"&gt;&lt;/a&gt;1&lt;br&gt;
GitHub PR comments without depending on requests&lt;br&gt;
better Django route detection&lt;br&gt;
more verification rules for AI-generated changes&lt;br&gt;
improving the caching system&lt;br&gt;
handling more cases where static analysis currently reports UNKNOWN or INFERRED&lt;br&gt;
What I learned from building this&lt;/p&gt;

&lt;p&gt;The biggest takeaway from this project wasn't actually AST parsing.&lt;/p&gt;

&lt;p&gt;It was what happened when I couldn't install a package to solve a problem.&lt;/p&gt;

&lt;p&gt;Normally, if I need a graph, I use a graph library.&lt;/p&gt;

&lt;p&gt;If I need Git integration, I use a Git library.&lt;/p&gt;

&lt;p&gt;If I need a web server, I use a framework.&lt;/p&gt;

&lt;p&gt;This project forced me to stop and ask:&lt;/p&gt;

&lt;p&gt;What is the library actually doing for me?&lt;/p&gt;

&lt;p&gt;That turned out to be one of the most useful parts of the hackathon.&lt;/p&gt;

&lt;p&gt;I learned more about AST traversal, graph representation, file hashing, Git's index, hooks, caching, and the limits of static analysis than I probably would have by simply installing the packages.&lt;/p&gt;

&lt;p&gt;And the dynamic dispatch problem taught me something else:&lt;/p&gt;

&lt;p&gt;A good static analysis tool should know when it doesn't know.&lt;/p&gt;

&lt;p&gt;That's one of the ideas I want Proofline to keep as it grows.&lt;/p&gt;

&lt;p&gt;Not pretending to have perfect certainty.&lt;/p&gt;

&lt;p&gt;Just being clear about what is PROVEN, what is INFERRED, and what is UNKNOWN.&lt;/p&gt;

&lt;p&gt;Try it / Get involved&lt;/p&gt;

&lt;p&gt;Proofline is open source&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxtlhatee33wydjx5sxm5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxtlhatee33wydjx5sxm5.png" alt="Proofline AI Block Illustration" width="589" height="579"&gt;&lt;/a&gt;e:&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/ShreyaKaushikdev/zerodep-analyzer" rel="noopener noreferrer"&gt;https://github.com/ShreyaKaushikdev/zerodep-analyzer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you work with AI-generated code, I'd be interested to know:&lt;/p&gt;

&lt;p&gt;What would you want a tool like this to verify before an AI-generated PR gets merged?&lt;/p&gt;

&lt;p&gt;And if you think something like Proofline is unnecessary compared with tools like Ruff, Flake8, or Sonar, I'd also genuinely like to know why.&lt;/p&gt;

&lt;p&gt;When you remove the packages, you don't just end up writing more code.&lt;/p&gt;

&lt;p&gt;You start understanding what those packages were doing for you in the first place.&lt;/p&gt;

&lt;p&gt;cc: @Hackathon Raptors&lt;/p&gt;

&lt;h1&gt;
  
  
  ZeroDependency #Python #StaticAnalysis #AST
&lt;/h1&gt;

</description>
      <category>codequality</category>
      <category>programming</category>
      <category>python</category>
      <category>software</category>
    </item>
  </channel>
</rss>
