DEV Community

Mohith Dande
Mohith Dande

Posted on

TraceSieve: See What Your Program Actually Did

When I entered the Zero Dependency Hackathon 2026, I thought the hard part would be coming up with an interesting project concept.

It wasn't.

The hard part was realizing how often software engineering relies on external packages for problems that look simpleβ€”until you try to implement them from scratch.

For this hackathon, I built TraceSieve: an offline Python CLI tool that records what actually happens during program execution, turns that execution into reusable evidence records, and lets you query, reconstruct, and compare different execution scenarios.

The interesting part wasn't just building a tracer.

The interesting part was building the entire analytical machinery around the trace without reaching for any of the packages I would normally pip install.

No third-party runtime dependencies.
No external cloud service or agent.
No framework.
Just 100% Python standard library.
πŸ’‘ The Core Idea: Execution as an Artifact
When developers inspect a codebase, they usually look at its possibilities.

A static analysis tool or IDE tells us:

Which modules exist
Which functions and classes exist
Which imports are defined in source
text

app/
β”œβ”€β”€ auth.py
β”œβ”€β”€ payments.py
β”œβ”€β”€ cache.py
β”œβ”€β”€ legacy.py
└── plugins.py
Static tools treat all these files equally. But a running program tells a completely different story.

A user triggering a basic checkout request might only execute: $$\text{auth.py} \longrightarrow \text{cache.py} \longrightarrow \text{payments.py}$$

While another execution (e.g. an admin refund) takes a completely different path, leaving legacy.py untouched.

That led me to a simple question:

What if an execution run itself became a named artifact that we could query, inspect, and compare later?

That became TraceSieve. Instead of treating runtime tracing as temporary log output destined for stdout, TraceSieve captures Execution Evidence Records.

bash

1. Capture execution runs as named scenarios

tracesieve run --name login -- python app.py login
tracesieve run --name checkout -- python app.py checkout

2. Reconstruct caller evidence for specific execution decisions

tracesieve why checkout process_payment

3. Compute scenario behavioral deltas

tracesieve diff login checkout

4. Expose untested static code gaps for a scenario

tracesieve gaps checkout app/
βš–οΈ Why Not Just Use Existing Packages?
Python has a rich ecosystem for profiling (cProfile), line coverage (coverage.py), call graphs (pycallgraph), and CLI frameworks (Click, Typer).

In a standard commercial project, I would pull these packages down in seconds. But the Zero Dependency Hackathon rules require an empty requirements.txt and zero runtime pip packages.

Instead of asking: "Which package should I install?"
I had to ask: "What primitives does Python's standard library already provide?"

Here is how TraceSieve replaced standard third-party libraries with standard library implementations:

Domain Typical 3rd-Party Package TraceSieve Standard Library Primitive Key Implementation Highlight
CLI Engine Click / Typer argparse Robust subcommands (run, report, diff, why, gaps, export) with clean validation & ANSI coloring
Runtime Observation hunter / PySnooper sys.settrace & threading.settrace Zero-overhead frame hook filtering out stdlib & site-packages
Persistence Store SQLAlchemy / Peewee sqlite3 Local ACID database (.tracesieve/runs.db) with indexes on sequence, module, and functions
Static Code Parsing redbaron / parso ast (ast.NodeVisitor) Complete AST inventory generator tracking classes, functions, and imports
Graph Analysis NetworkX dict + set + BFS/DFS In-memory graph traversal algorithms for caller path reconstruction
Data Models Pydantic / attrs dataclasses Strongly-typed, memory-efficient data structures
Single Executable PyInstaller zipapp Compiles into tracesieve.pyz runnable on any Python 3.10+ installation
Test Suite pytest unittest Comprehensive unit & integration testing suite
πŸ› οΈ Deep Dive: The 5 Stdlib Replacements

  1. CLI Tooling: Crafting an Application CLI with argparse argparse is often seen as basic compared to Click or Typer. But argparse supports full subcommand hierarchies, argument validation, custom formatters, and error handling.

Building subcommands with argparse forced explicit control over:

Subcommand routing (run, report, diff, why, gaps)
Positional vs optional flag parsing
Standard error (stderr) vs standard output (stdout) separation
Custom help formatters and clean exit codes (0 for success, 1 for missing target, 2 for target runtime failure, 3 for analysis errors)
python

System subcommand initialization with standard library argparse

parser = argparse.ArgumentParser(prog="tracesieve", description="Runtime Execution Evidence Tool")
subparsers = parser.add_subparsers(dest="command", required=True)

Subcommand: why

why_parser = subparsers.add_parser("why", help="Explain why a function executed in a scenario")
why_parser.add_argument("scenario", help="Name of recorded scenario")
why_parser.add_argument("target", help="Function or qualified name to explain")

  1. Runtime Observation: sys.settrace & Thread Safety Collecting runtime evidence is fundamentally different from taking line coverage snapshots or measuring CPU clock cycles. I needed to capture:

Sequential event ordering (sequence_number)
Frame nesting depth (depth)
Parent/caller relationships (caller_func, caller_file)
Exception boundaries (event_type == "exception")
Multi-threaded execution isolation (thread_id)
Python's sys.settrace and threading.settrace hooks allow intercepting every frame event. The primary technical challenge was filtering out standard library internals so that tracing didn't slow down the interpreter or bloat the evidence log.

python

class ExecutionTracer:
"""Standard-library sys.settrace hook for capturing execution evidence."""
def init(self, collector: EventCollector, include_stdlib: bool = False):
self.collector = collector
self.include_stdlib = include_stdlib
self._thread_stacks: Dict[int, List[Tuple[str, str, Optional[str]]]] = {}
self._lock = threading.Lock()

    # Detect standard library paths to ignore runtime overhead
    self._stdlib_dirs: Set[str] = {
        os.path.abspath(prefix).lower()
        for prefix in (sys.prefix, sys.exec_prefix, getattr(sys, "base_prefix", sys.prefix))
        if prefix
    }
def _trace_dispatch(self, frame, event: str, arg):
    filename = frame.f_code.co_filename
    if not self._should_trace_file(filename):
        return self._trace_dispatch
    thread_id = threading.get_ident()
    func_name = frame.f_code.co_name
    module_name = frame.f_globals.get("__name__")
    line = frame.f_lineno
    with self._lock:
        stack = self._thread_stacks.setdefault(thread_id, [])
        caller_file, caller_func = (stack[-1][0], stack[-1][1]) if stack else (None, None)
        if event == "call":
            depth = len(stack) + 1
            self.collector.add_event(
                event_type="call",
                filename=filename,
                module=module_name,
                function=func_name,
                line=line,
                caller=caller_func,
                caller_file=caller_file,
                depth=depth,
                thread_id=thread_id,
            )
            stack.append((filename, func_name, module_name))
        elif event == "return":
            if stack:
                stack.pop()

    return self._trace_dispatch
Enter fullscreen mode Exit fullscreen mode
  1. Graphs Without NetworkX: Reconstructing Call Paths Once caller-callee pairs are captured, answering "Why did function X run?" becomes a graph path traversal problem.

Instead of introducing a dependency like NetworkX, a graph in TraceSieve is represented with pure Python dict and set primitives:

$$\text{Adjacency List}: \text{graph}[\text{caller}] = {\text{callee}_1, \text{callee}_2, \dots}$$

Reconstructing the shortest observed path from main() to authorize() is accomplished with a breadth-first search (BFS) over the execution event stream:

python

def extract_why_trail(events: List[Event], target_func: str) -> Optional[List[str]]:
"""Reconstruct observed caller path from entry point to target function."""
parent_map: Dict[str, str] = {}
visited: Set[str] = set()
queue = []
for event in events:
if event.event_type == "call" and event.caller:
if event.function not in parent_map:
parent_map[event.function] = event.caller
if not queue and event.depth == 1:
queue.append(event.function)
# Reconstruct path backwards from target
if target_func not in parent_map:
return None
path = [target_func]
curr = target_func
while curr in parent_map:
curr = parent_map[curr]
path.append(curr)
if curr == path[-1] and len(path) > 1: # Prevent cycle loops
break

return list(reversed(path))
Enter fullscreen mode Exit fullscreen mode
  1. Persistence with Zero-Config sqlite3 A runtime trace is only useful if it survives the process execution. TraceSieve uses Python's built-in sqlite3 engine to persist execution evidence in a local database file (.tracesieve/runs.db).

Using SQLite directly required writing clean schema migrations, indexes, and parameterized queries:

sql

CREATE TABLE IF NOT EXISTS runs (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
target_cmd TEXT NOT NULL,
exit_code INTEGER NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
duration_ms REAL NOT NULL,
total_events INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
seq INTEGER NOT NULL,
event_type TEXT NOT NULL,
filename TEXT NOT NULL,
module TEXT,
function TEXT NOT NULL,
line INTEGER NOT NULL,
caller TEXT,
depth INTEGER NOT NULL,
thread_id INTEGER NOT NULL,
FOREIGN KEY(run_id) REFERENCES runs(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_events_run_func ON events(run_id, function);

  1. Static Inventory vs. Runtime Reality (ast) Static analysis tells us what exists in code. Runtime evidence tells us what actually executed.

Using Python's built-in ast module, TraceSieve walks local source trees (ast.NodeVisitor) to index every defined class, function, and import, then overlays that static inventory against the recorded SQLite evidence.

python

class ASTVisitor(ast.NodeVisitor):
"""Parses static functions and classes from AST."""
def init(self, filename: str):
self.filename = filename
self.functions = []
def visit_FunctionDef(self, node: ast.FunctionDef):
self.functions.append(node.name)
self.generic_visit(node)
This contrast powers the tracesieve gaps command:

text

EXECUTION GAP (STATIC <-> RUNTIME MATRIX)

Module / File Static Runtime

auth [+] [+]

database [+] [+]

payment [+] [+]

legacy [+] [-]

UNOBSERVED FUNCTIONS (Not executed in scenario)
refund() [legacy.py:14]
chargeback() [legacy.py:32]
⚠️ Important Design Distinction: TraceSieve explicitly avoids calling unobserved code "dead code". An unobserved function in checkout doesn't mean it can't execute; it simply means this scenario never touched it.

πŸ”₯ Features & Practical Terminal Output

  1. Reconstructing Execution Evidence (tracesieve why) bash

python tracesieve.pyz why checkout authorize
text

WHY WAS THIS EXECUTED?

Target:
authorize()
Observed path:
main()
|
checkout()
|
process_payment()
|
authorize()
Observed in:
scenario = checkout
First observed event:
#27
Call count:
1

  1. Scenario Differential Analysis (tracesieve diff) bash

python tracesieve.pyz diff login checkout
text

SCENARIO DIFFERENCE

LOGIN ONLY
- login
- session_create
CHECKOUT ONLY
+ authorize
+ load_cart
+ process_payment
SHARED
connect
main
query
validate
NEW EXECUTION BRANCH
authorize
+-- check
EXECUTION DELTA (COUNT CHANGES)
query 1 -> 9 (+800.0%)
Execution events
login: 24
checkout: 48
πŸ”’ Security & Privacy by Design
Tracing tools often inadvertently capture sensitive production data when logging frame variables (frame.f_locals).

TraceSieve guarantees privacy by adhering to a strict design boundary:

Structure over Payload: TraceSieve captures only execution structure (file names, function names, line numbers, caller relationships, event depths, execution times).
Zero Data Leakage: No variable contents, arguments, or return payloads are ever read, serialized, or stored in SQLite.
πŸ“¦ Single-File Zero-Dependency Executable (zipapp)
Python includes zipapp in the standard library, which bundles Python source trees into a standalone executable .pyz file:

bash

python -m zipapp src -o tracesieve.pyz -m "tracesieve.cli:main"
The resulting tracesieve.pyz file is a complete, self-contained executable that runs on any machine with Python 3.10+ without pip install.

πŸ’‘ Lessons Learned from Zero Dependencies
Dependencies are often convenience layers over STDLIB primitives: Click wraps argparse, NetworkX wraps dict/set, and Pydantic wraps dataclasses. Building these layers manually yields deep appreciation for stdlib design.
Constraints drive focused architecture: Removing dependencies eliminates supply-chain risks, package version conflicts, and heavy container images.
Execution is data: Treating program execution as an inspectable, persistent artifact fundamentally changes how we debug, audit, and verify complex software.
πŸ› οΈ Try TraceSieve
TraceSieve is open-source, zero-dependency, and ready to use offline.

⭐️ GitHub Repository: Mohith1-stack/TraceSieve
πŸ“– Built for: Zero Dependency Hackathon 2026
What standard library primitives do you often rely on when avoiding third-party packages? Let's discuss in the comments!

Top comments (0)