Debugging with FlexLock
FlexLock provides powerful debugging tools to help you diagnose issues in your experiments quickly.
The @debug_on_fail Decorator
The debug_on_fail decorator is a specialized debugging tool that injects local variables into your interactive session when a function fails. This allows you to inspect the exact state when the error occurred.
Basic Usage
from flexlock import debug_on_fail
@debug_on_fail
def train_model(lr=0.01, epochs=10):
# Your training code
data = load_data()
model = create_model()
for epoch in range(epochs):
loss = train_epoch(model, data, lr)
# Something goes wrong here...
return model
When an exception occurs, the local variables (data, model, epoch, loss, etc.) are automatically injected into the caller’s scope for inspection.
Activation
Two control surfaces — they affect different things:
@flexcli/flexlock-run --debug:FLEXLOCK_DEBUG=1(or the--debugCLI flag) tells the runner to wrap the user function withdebug_on_failautomatically. With@flexcli, this is the easiest way to opt in:python train.py --debug # sets FLEXLOCK_DEBUG=true for you FLEXLOCK_DEBUG=1 python train.py
Manually applied
@debug_on_fail: once you put the decorator on a function, it is active by default.FLEXLOCK_DEBUG=0does not turn it off. To disable a manually applied decorator, setFLEXLOCK_NODEBUG=1— the decorator returns the original function unchanged.FLEXLOCK_NODEBUG=1 python train.py # @debug_on_fail becomes a no-op
Inside Jupyter / IPython
import os
os.environ['FLEXLOCK_DEBUG'] = '1'
# With @flexcli, this enables auto-wrapping on the next run.
Interactive Debugging Workflow
Step 1: Run with Debug Mode
# Using CLI flag (recommended)
python train.py --debug
# Or using environment variable
FLEXLOCK_DEBUG=1 python train.py
Step 2: Exception Occurs
Traceback (most recent call last):
File "train.py", line 45, in train_model
loss = train_epoch(model, data, lr)
ValueError: Invalid learning rate: -0.1
--- FLEXLOCK_DEBUG: An exception occurred in train_model: Invalid learning rate: -0.1 ---
--- FLEXLOCK_DEBUG: Locals in 'train_model' at time of error: ['lr', 'epochs', 'data', 'model', 'epoch', 'loss'] ---
--- FLEXLOCK_DEBUG: Injected locals into '__main__'. ---
Step 3: Inspect Variables in REPL
After the crash, if running in an interactive environment or with a debugger:
>>> # Variables are now available!
>>> print(lr)
-0.1
>>> print(epoch)
5
>>> print(model)
<Model object at 0x...>
>>> print(data.shape)
(1000, 10)
Example: Combined with @flexcli
from flexlock import flexcli, debug_on_fail
@flexcli
@debug_on_fail
def train(
data_path: str = "data/train.csv",
lr: float = 0.01,
epochs: int = 10
):
"""Train a model with debugging support."""
# Load data
data = pd.read_csv(data_path)
print(f"Loaded {len(data)} samples")
# Create model
model = LinearModel(input_dim=data.shape[1])
# Training loop
for epoch in range(epochs):
# Simulate training
predictions = model.forward(data)
loss = compute_loss(predictions, data['target'])
# Oops! Bug here
if epoch == 5:
raise ValueError(f"Training diverged at epoch {epoch}")
model.update(loss, lr)
return model
if __name__ == "__main__":
train()
Run with debugging:
FLEXLOCK_DEBUG=1 python train.py
# When it crashes:
# - data, model, epoch, predictions, loss are all available
# - You can inspect them in an interactive Python session or debugger
Debug Strategy Control
Control debugging behavior via environment variable:
# Auto-detect environment (default)
FLEXLOCK_DEBUG_STRATEGY=auto python train.py
# Force PDB post-mortem
FLEXLOCK_DEBUG_STRATEGY=pdb python train.py
# Force variable injection (for notebooks/interactive)
FLEXLOCK_DEBUG_STRATEGY=inject python train.py
Use Cases
1. Interactive Development
Develop functions interactively with automatic variable capture:
# In IPython or Jupyter
import os
os.environ['FLEXLOCK_DEBUG'] = '1'
from flexlock import debug_on_fail
@debug_on_fail
def experiment():
data = expensive_preprocessing() # Takes 10 minutes
results = run_experiment(data) # Crashes here!
return results
# Try running
experiment()
# After crash, 'data' is available!
# No need to rerun preprocessing
>>> data.shape
(10000, 100)
>>> # Fix the bug and continue...
2. Debugging Long-Running Jobs
When an experiment crashes after hours:
# Submit job with debug mode
sbatch --export=FLEXLOCK_DEBUG=1 long_job.sh
# Job crashes at step 500 of 1000
# Variables from step 500 are saved
# Resume from there without rerunning everything
3. Root Cause Analysis
Inspect the exact state when rare bugs occur:
@debug_on_fail
def training_loop(data, model, config):
for batch in data:
# ... complex logic ...
if some_rare_condition: # Happens once in 1000 batches
raise ValueError("Rare bug!")
# When it happens, all variables available for inspection
Important Warnings
⚠️ Advanced Feature: debug_on_fail modifies Python’s frame stack. Use with caution.
⚠️ Not for Production: This is a development tool. Never enable in production.
⚠️ Memory: Injected variables remain in memory. Be cautious with large objects.
⚠️ Side Effects: Only safe for pure functions. Don’t use with functions that manage resources (files, network connections).
Comparison with Other Debugging Tools
Tool |
When to Use |
Pros |
Cons |
|---|---|---|---|
|
Interactive development, preserving state |
No code changes needed, auto-capture |
Requires REPL |
|
Step-by-step debugging |
Full control |
Manual breakpoints |
|
Interactive debugging |
IPython features |
Requires installation |
Logging |
Production debugging |
Safe, permanent |
No interactivity |
Best Practices
✅ Do:
Use in development and debugging sessions
Enable only when actively debugging
Test both with and without debug mode
Use with interactive environments (IPython, Jupyter)
❌ Don’t:
Enable in production code
Rely on it for permanent error handling
Use with functions that manage resources
Forget to disable it (performance overhead)
Example: Full Debugging Session
# experiment.py
from flexlock import flexcli, debug_on_fail
import pandas as pd
@flexcli
@debug_on_fail
def analyze_data(
data_path: str = "data/results.csv",
threshold: float = 0.5,
save_dir: str = "results/analysis"
):
"""Analyze experiment results."""
# Load data
df = pd.read_csv(data_path)
print(f"Loaded {len(df)} results")
# Filter
filtered = df[df['accuracy'] > threshold]
print(f"Found {len(filtered)} results above threshold")
# Analyze
best_model = filtered.loc[filtered['accuracy'].idxmax()]
# Oops! Bug if filtered is empty
metrics = compute_metrics(best_model)
return metrics
if __name__ == "__main__":
analyze_data()
Debug session:
$ FLEXLOCK_DEBUG=1 python experiment.py -o threshold=0.99
Loaded 100 results
Found 0 results above threshold
--- FLEXLOCK_DEBUG: Exception in analyze_data: ... ---
--- FLEXLOCK_DEBUG: Locals: ['data_path', 'threshold', 'df', 'filtered', ...] ---
# Now in Python REPL or crash handler:
>>> filtered
Empty DataFrame
Columns: [accuracy, model_type, ...]
Index: []
>>> threshold
0.99
>>> # Ah! Threshold too high
>>> # Fix and rerun with lower threshold
Alternative: Logging-Based Debugging
For production-safe debugging, use FlexLock’s logging integration:
from flexlock import flexcli
from flexlock.utils import log_to_file
from loguru import logger
@flexcli
def train(lr=0.01, save_dir="results/train"):
with log_to_file(Path(save_dir) / "debug.log"):
logger.debug(f"Starting with lr={lr}")
data = load_data()
logger.debug(f"Loaded {len(data)} samples")
model = create_model()
logger.debug(f"Model: {model}")
# All debug info saved to debug.log
FlexLock uses Loguru for structured logging.
Summary
The @debug_on_fail decorator is a powerful tool for interactive experiment development:
✅ Automatic variable capture on failure
✅ No code changes needed
✅ Works with
@flexcliand other decorators✅ Controlled via environment variable
Use it during development to preserve expensive computation state and debug issues efficiently!