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 ☕
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 ☕