7.3 Understanding What You Actually Need
You're convinced. You won't build custom instrumentation. But you're staring at a complex codebase, and you still need to understand what it does. Before reaching for any tool, you need to clarify exactly what question you're trying to answer. This isn't pedantic philosophy—it's practical strategy. Different questions require different tools, and using the wrong tool wastes time.
Distinguishing between tracing, profiling, and debugging
These three words get used interchangeably in conversation, but they represent distinct activities with distinct tools. Let's define them precisely:
Tracing answers: "What code runs and in what order?"
You want to see the execution path. You need to know: When I click this button, what functions get called? What's the sequence? Which middleware runs? What order do signal handlers fire in?
Tracing is about flow. You're building a map of the execution journey. Think of it like recording a video of your code as it runs—you want to see every frame in sequence.
Profiling answers: "Where does the time go?"
You know the code is slow. You need to identify bottlenecks. Which function takes 5 seconds? How many times is this database query executed? What percentage of CPU time is spent in this loop?
Profiling is about performance. You're measuring resource consumption—time, memory, CPU cycles. Think of it like a stopwatch on every function, showing you which ones dominate the runtime.
Debugging answers: "Why doesn't this work as expected?"
You have a bug. You need to understand why the code behaves incorrectly. What's the value of this variable at this point? Why did this conditional take the wrong branch? Why didn't this function get called?
Debugging is about state. You're inspecting values, watching variables change, testing hypotheses about what went wrong. Think of it like pausing time and looking around to see what's happening.
Let's see why the distinction matters:
Scenario 1: The form submission mystery
Sarah's problem from earlier: "I need to understand what happens when I submit this form."
-
Wrong approach: Profile the form submission. Profiling will tell her
form.save()takes 8 seconds, but not what code runs or why. -
Right approach: Trace the form submission. Set a breakpoint, step through execution, see the call stack.
-
Sarah's question is about FLOW, not PERFORMANCE. She needs tracing/debugging, not profiling.
Scenario 2: The slow API endpoint
Your API endpoint /api/products returns in 2 seconds. Your boss wants it under 500ms. You need to optimize.
-
Wrong approach: Set breakpoints and step through the code. You'll see what runs, but not where the time is spent. Maybe 95% of the time is in one database query—but you won't know that from tracing.
-
Right approach: Profile the endpoint. Use a profiler to see which operations consume the most time.
-
This question is about PERFORMANCE. You need profiling, not tracing.
Scenario 3: The incorrect calculation
A pricing calculation returns $42.50 when it should return $38.75. You need to find the bug.
-
Wrong approach: Profile the calculation. Profiling will show you timing, but not why the math is wrong.
-
Wrong approach: Just trace the execution. Tracing will show you what functions run, but not what's inside the variables.
-
Right approach: Debug interactively. Set a breakpoint, inspect variables at each step, find where the value diverges from expected.
-
This question is about CORRECTNESS. You need debugging (interactive inspection), not just tracing or profiling.
The Key Insight: These aren't just different words for the same thing. They're different lenses for looking at code execution:
-
Tracing gives you a sequence of events (function A called B called C)
-
Profiling gives you resource measurements (A took 100ms, B took 500ms)
-
Debugging gives you state snapshots (variable X was 5, then became 10)
Most tools do one of these well, and the others poorly or not at all. A profiler can show you function call counts, but it won't let you inspect variables. A debugger lets you step through code, but it doesn't automatically measure timing.
Here's the practical implication: Before you open any tool, ask yourself:
-
"Do I need to know WHAT runs?" → Tracing
-
"Do I need to know HOW LONG things take?" → Profiling
-
"Do I need to know WHY something produces wrong output?" → Debugging
Often, you need more than one. You might trace to understand the flow, then profile to find bottlenecks, then debug to fix a specific bug. But you do them sequentially with different tools, not all at once.
The three questions every codebase explorer asks
When you start working with unfamiliar code, you're really trying to answer three fundamental questions. Let's examine each one and understand what it's really asking.
"What code runs when I do X?"
This is the most common question when exploring a codebase. You perform some action—submit a form, click a button, send an API request—and you want to know what code executes as a result.
What you're really asking:
-
What's the entry point?
-
What functions get called?
-
What framework code runs implicitly?
-
What middleware/signal handlers/decorators are involved?
-
Where does my action "land" in the codebase?
Example situations:
-
"When I POST to
/api/login, what happens?" -
"When I click 'Delete Account', what code runs?"
-
"When this Celery task fires, what gets executed?"
-
"When I save this model, what signal handlers trigger?"
Why this matters:
You can't understand a system by reading class definitions and function signatures. You need to see the system in motion. Modern applications have execution paths determined by:
-
Routing/URL configuration
-
Class-based view inheritance
-
Decorator stacking
-
Middleware chains
-
Signal/event handlers
-
Framework conventions you might not know about
The only way to see the complete picture is to watch what actually runs.
Real example:
You inherit a Django admin interface. Users report: "When we delete a user via the admin panel, their comments disappear immediately, but their forum posts stay for 24 hours, then disappear. Why?"
Reading the code:
-
User model has
on_delete=CASCADEfor comments (explains immediate deletion) -
User model has no explicit relationship to forum posts
-
You search for "post" and "delete" but find nothing obvious
What actually happens (discovered by tracing):
-
Django admin calls
User.delete() -
A
pre_deletesignal handler you didn't find sets a flag on related forum posts -
A Celery periodic task (running hourly) checks for flagged posts older than 24 hours and deletes them
-
This behavior was implemented to comply with a legal requirement from 2 years ago
You cannot find this by reading. The signal handler is in a different app. The Celery task is in yet another file. The 24-hour delay is configured in settings. Only tracing the actual execution reveals the complete flow.
"In what order does it execute?"
This question often follows the first one. You've identified the pieces—now you need to understand their sequence and relationships.
What you're really asking:
-
What's the call stack?
-
Which happens first: middleware or view?
-
Do decorators run before or after the function?
-
When do signal handlers fire relative to database commits?
-
What's the nesting/hierarchy of execution?
Example situations:
-
"Does authentication run before or after rate limiting?"
-
"When does Django's transaction middleware commit?"
-
"In what order do my three decorators execute?"
-
"Does this signal handler run before or after the database save?"
Why order matters:
Order determines:
-
Correctness: If you check permissions after modifying data, you have a security bug
-
Behavior: If you query the database before a save commits, you get stale data
-
Performance: If you cache before transformation, you cache the wrong thing
Real example:
Your Django app has a view decorated with three custom decorators:
@require_login
@check_subscription
@rate_limit(max_calls=100)
def api_endpoint(request):
# implementation
A bug report: "Free users are hitting the rate limit even though they should be rejected at subscription check."
The order matters here:
-
If rate limiting runs first, free users burn through rate limit attempts before being rejected
-
If subscription check runs first, free users get rejected immediately without consuming rate limit
By tracing execution, you discover:
-
rate_limitruns first (decorators execute bottom-to-top) -
check_subscriptionruns second -
require_loginruns third -
The view runs last (if all decorators pass)
The fix: reorder the decorators so subscription check runs before rate limiting. You cannot determine the correct order by reading Python syntax alone—decorator execution order is a language feature you need to know, and tracing confirms your understanding.
"What data flows through the system?"
This is the most detailed question. You understand what runs and in what order—now you want to see the actual data being processed.
What you're really asking:
-
What values do variables have at each step?
-
How does data transform as it moves through functions?
-
Where does this value come from?
-
Why does this variable have an unexpected value?
-
What's in this object's attributes?
Example situations:
-
"This price calculation returns 42.50, but I expected 38.75—where does the difference come from?"
-
"This user object should have an email, but the field is None—why?"
-
"This API returns JSON with 20 fields, but I only set 15 in my serializer—where do the others come from?"
-
"This SQL query has a WHERE clause I didn't write—who added it?"
Why data matters:
Understanding execution flow isn't enough if you can't see the values. You might know that calculate_discount(user, price) runs, but if you can't see that user.discount_tier is unexpectedly None, you can't diagnose the bug.
Real example:
Your Django REST Framework API returns user data:
{
"id": 123,
"username": "alice",
"email": "alice@example.com",
"is_premium": true,
"last_login": "2025-10-11T10:30:00Z",
"subscription_expires": "2025-12-31T23:59:59Z"
}
You look at your serializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'is_premium']
Wait—your serializer only defines 4 fields, but the API returns 6. Where do last_login and subscription_expires come from?
Reading the code doesn't help. There's no obvious place where those fields are added. Possibilities:
-
A parent class?
-
A mixin?
-
A signal handler modifying the response?
-
Middleware adding fields?
-
A custom renderer?
By tracing execution and inspecting data at each step, you discover:
-
Your serializer inherits from
CustomModelSerializer(defined in a base app) -
CustomModelSerializer.to_representation()is overridden -
It checks if the model has
last_loginorsubscription_expiresfields -
If so, it automatically adds them to the output
The extra fields come from an implicit framework convention your team built years ago. You only discover this by watching the data transform through the serialization pipeline.
Matching tools to questions: A decision matrix
Now that you understand the three core questions, let's map tools to questions. This is your decision-making guide.
Question: "What code runs when I do X?"
| Tool | Best For | Limitations |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Framework DevTools (Django Debug Toolbar, React DevTools) | First line of investigation. Shows high-level execution in context of framework. | Framework-specific. Won't show code outside framework conventions. |
| Debugger with breakpoints | Precise control. Set breakpoint at entry point, see call stack. | Requires knowing where to set breakpoint. Can't see past executions. |
| Logging/Print statements | Quick checks. "Did this function run?" | Requires code changes. No call stack context. |
| sys.settrace() or similar | Comprehensive function call log without code changes. | Overwhelming output. Hard to filter. Performance impact. |
| APM tools (New Relic, DataDog) | Production environments. Historical data. | Expensive. Overkill for local exploration. Setup overhead. |
Recommended approach:
-
Start with framework DevTools (if available)
-
Use debugger breakpoints for precise investigation
-
Use
sys.settrace()only if you need comprehensive automated logging -
Avoid custom instrumentation
Question: "In what order does it execute?"
| Tool | Best For | Limitations |
| ------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------- |
| Debugger call stack | Shows current execution hierarchy at any breakpoint. | Point-in-time only. Can't see full flow history. |
| Debugger step-through | Watch execution step by step. See exact order. | Slow for large flows. Requires interactive stepping. |
| Framework DevTools timeline | Visualizes execution order (React Profiler, Chrome Performance) | Limited to framework-aware execution. |
| sys.settrace() output | Complete chronological log of all function calls. | Too much information. Hard to read. |
| Logging with timestamps | See order of specific checkpoints. | Requires adding log statements. Only shows what you log. |
Recommended approach:
-
Use debugger call stack to understand nesting
-
Step through execution with debugger for sequential flow
-
Use framework timelines for visual understanding
-
Add strategic log statements only for long-running flows you can't step through interactively
Question: "What data flows through the system?"
| Tool | Best For | Limitations |
| ------------------------------ | ------------------------------------------------------------- | ------------------------------------------------------ |
| Interactive debugger | Inspect any variable at any breakpoint. Evaluate expressions. | Requires stopping execution. Must know where to break. |
| Debugger watch expressions | Monitor specific variables as you step. | Manual setup. Only watches what you specify. |
| Logging data | Record values at specific points. | Requires code changes. Can't inspect on-the-fly. |
| Framework DevTools | See framework-specific data (props, state, SQL queries). | Only shows framework-aware data structures. |
| Memory profilers | Track object allocation and references. | For memory issues, not general data inspection. |
Recommended approach:
-
Always start with interactive debugger
-
Set breakpoints at key points in flow
-
Inspect variables, evaluate expressions, modify values to test hypotheses
-
Use framework DevTools for framework-specific data (database queries, component props)
-
Add logging only for data in production or long-running processes
Combined Questions Decision Tree:
START: I need to understand unfamiliar code
│
├─ Do I need to understand WHAT runs?
│ │
│ ├─ Is there a framework-specific DevTool?
│ │ └─ YES → Start there (Django Debug Toolbar, React DevTools, etc.)
│ │ └─ NO → Use debugger with breakpoint at entry point
│ │
│ └─ Do I also need to see ORDER?
│ └─ YES → Use debugger step-through + call stack view
│ └─ NO → Framework DevTools or single breakpoint is enough
│
├─ Do I need to understand WHERE TIME IS SPENT?
│ │
│ └─ Use a profiler
│ ├─ Python: cProfile, py-spy, line_profiler
│ ├─ JavaScript: Chrome Performance tab, clinic.js
│ └─ NOT a debugger (too slow), NOT a tracer (no timing info)
│
└─ Do I need to understand WHY BEHAVIOR IS WRONG?
│
└─ Use interactive debugger
├─ Set breakpoint where you suspect the bug
├─ Inspect variables
├─ Step through to find where behavior diverges from expected
└─ Use conditional breakpoints to catch specific cases
The Critical Principle: Start Simple, Escalate Only When Necessary
Notice the pattern in all the recommendations above:
-
Try the simplest tool first (framework DevTools, single breakpoint)
-
Escalate to more powerful tools only if simple ones don't answer the question
-
Build custom instrumentation only as a last resort (or never)
Common Mistake: Developers often skip straight to complex tools because they seem more powerful. They use sys.settrace() when a single breakpoint would suffice. They build custom instrumentation when Django Debug Toolbar would answer their question in 30 seconds.
The Efficiency Rule: The best tool is the one that answers your question in the least time with the least setup. Sometimes that's a single print statement. Sometimes it's a debugger. It's almost never custom AST transformation.
Example Decision-Making in Practice:
Scenario: New Django project. User registration seems to take 5 seconds. Need to understand why.
Bad approach:
-
Build custom timing decorator
-
Apply to all view functions
-
Parse output logs
-
Time spent: 3 hours
Good approach:
-
Install Django Debug Toolbar (3 minutes)
-
Submit registration form
-
Look at SQL panel: See 47 database queries
-
Look at timeline: 4.8 seconds in queries
-
Identify N+1 query problem
-
Total time: 5 minutes
The difference isn't skill—it's knowing which tool answers your question.
Your Tracing Toolkit Checklist:
âś“ Learn your debugger deeply (VS Code, PyCharm, Chrome DevTools)
-
Setting breakpoints
-
Stepping through code
-
Inspecting variables
-
Call stack navigation
-
Conditional breakpoints
âś“ Install framework DevTools
-
Django: Django Debug Toolbar
-
Flask: Flask-DebugToolbar
-
React: React DevTools
-
Vue: Vue DevTools
-
etc.
âś“ Know your profiler (for performance questions)
-
Python:
py-spy(install globally) -
JavaScript: Chrome Performance tab (already have it)
âś“ Keep it simple
-
Print statements are fine for quick checks
-
Logging is fine for production
-
Custom instrumentation is rarely justified
With these tools and this decision framework, you can trace execution in any codebase without building custom solutions. The tools already exist. They're more powerful than anything you'd build. Learn them deeply, and you'll understand codebases faster than developers who spend weeks building "elegant" custom instrumentation.