Tracking Memory Access Patterns in Python Using sys.settrace and Custom Hooks

Python doesn’t expose raw memory the way lower-level languages do, but you can still track how and when your variables change. In this tutorial, we’ll build a lightweight memory tracing tool using Python’s built-in sys.settrace function, allowing us to monitor variable assignments and get insights into runtime behavior — no C extensions, no third-party tools. Setup This method works in pure Python 3. No dependencies needed. You’ll just need a working Python environment and a target function you want to introspect. It's particularly useful for debugging, reverse engineering, or building your own lightweight instrumentation tools. Full Working Code import sys def trace_vars(frame, event, arg): if event != "line": return trace_vars code = frame.f_code lineno = frame.f_lineno locals_now = frame.f_locals.copy() global last_locals if code.co_name not in last_locals: last_locals[code.co_name] = locals_now return trace_vars old_locals = last_locals[code.co_name] for var, new_val in locals_now.items(): if var not in old_locals: print(f"[{code.co_name}:{lineno}] NEW {var} = {new_val}") elif old_locals[var] != new_val: print(f"[{code.co_name}:{lineno}] MODIFIED {var}: {old_locals[var]} → {new_val}") for var in old_locals: if var not in locals_now: print(f"[{code.co_name}:{lineno}] DELETED {var}") last_locals[code.co_name] = locals_now return trace_vars def monitor(func): def wrapper(*args, **kwargs): global last_locals last_locals = {} sys.settrace(trace_vars) try: return func(*args, **kwargs) finally: sys.settrace(None) return wrapper Example usage @monitor def run_example(): a = 10 b = a + 5 b = b * 2 del a return b run_example() Explanation By using sys.settrace, we register a line-by-line callback that can introspect the current local variables at each step. We store a snapshot of the locals for each function, then compare it on the next invocation to detect changes — additions, updates, deletions. It’s a powerful (and often overlooked) way to understand the control and data flow of any Python function at runtime. Pros & Cons ✅ Pros Works with unmodified Python Fully userland — no C or unsafe operations Easy to wrap around any function Valuable for introspection, testing, and education ⚠️ Cons Significant performance overhead Doesn’t work well with async or multithreaded code Can’t access native extensions or memory directly Not suitable for production monitoring Summary This approach gives you a window into the lifecycle of variables in Python, letting you trace changes without any third-party packages or tooling. While it's not a true memory inspector, it's a powerful pattern for anyone debugging tricky code or trying to understand execution in fine-grained detail. If this was useful, you can Buy Me a Coffee ☕

Apr 22, 2025 - 23:27
 0
Tracking Memory Access Patterns in Python Using sys.settrace and Custom Hooks

Python doesn’t expose raw memory the way lower-level languages do, but you can still track how and when your variables change. In this tutorial, we’ll build a lightweight memory tracing tool using Python’s built-in sys.settrace function, allowing us to monitor variable assignments and get insights into runtime behavior — no C extensions, no third-party tools.

Setup


This method works in pure Python 3. No dependencies needed. You’ll just need a working Python environment and a target function you want to introspect. It's particularly useful for debugging, reverse engineering, or building your own lightweight instrumentation tools.

Full Working Code


import sys

def trace_vars(frame, event, arg):
if event != "line":
return trace_vars

code = frame.f_code
lineno = frame.f_lineno
locals_now = frame.f_locals.copy()
global last_locals

if code.co_name not in last_locals:
    last_locals[code.co_name] = locals_now
    return trace_vars

old_locals = last_locals[code.co_name]

for var, new_val in locals_now.items():
    if var not in old_locals:
        print(f"[{code.co_name}:{lineno}] NEW {var} = {new_val}")
    elif old_locals[var] != new_val:
        print(f"[{code.co_name}:{lineno}] MODIFIED {var}: {old_locals[var]} → {new_val}")

for var in old_locals:
    if var not in locals_now:
        print(f"[{code.co_name}:{lineno}] DELETED {var}")

last_locals[code.co_name] = locals_now
return trace_vars

def monitor(func):
def wrapper(*args, **kwargs):
global last_locals
last_locals = {}
sys.settrace(trace_vars)
try:
return func(*args, **kwargs)
finally:
sys.settrace(None)
return wrapper

Example usage

@monitor
def run_example():
a = 10
b = a + 5
b = b * 2
del a
return b

run_example()

Explanation


By using sys.settrace, we register a line-by-line callback that can introspect the current local variables at each step. We store a snapshot of the locals for each function, then compare it on the next invocation to detect changes — additions, updates, deletions. It’s a powerful (and often overlooked) way to understand the control and data flow of any Python function at runtime.

Pros & Cons

✅ Pros


  • Works with unmodified Python
  • Fully userland — no C or unsafe operations
  • Easy to wrap around any function
  • Valuable for introspection, testing, and education

⚠️ Cons


  • Significant performance overhead
  • Doesn’t work well with async or multithreaded code
  • Can’t access native extensions or memory directly
  • Not suitable for production monitoring

Summary


This approach gives you a window into the lifecycle of variables in Python, letting you trace changes without any third-party packages or tooling. While it's not a true memory inspector, it's a powerful pattern for anyone debugging tricky code or trying to understand execution in fine-grained detail.

If this was useful, you can Buy Me a Coffee