How the Python Tutor visualizer can help students in your Python programming courses

Summary: This article is meant for instructors who teach Python, whether in university courses or K-12 classrooms. Python Tutor is a free web-based tool that draws step-by-step visualizations of code as it runs. Since 2010, over 25 million people in over 180 countries have used it to visualize over 500 million pieces of code. This article shows what it can illustrate at every level of Python teaching, from a student's first loop up through closures and generators. It will always remain free to use.

Python Tutor is a free tool that visualizes what your code does step-by-step as it runs. Since I created it in 2010, over 25 million people in over 180 countries have used it to visualize over 500 million pieces of code, and it's used for teaching in over 10,000 schools, including MIT, Harvard, Princeton, UC Berkeley, Tsinghua, and the IITs.

Here's an embarrassing admission, though: over the years I've written detailed articles for Java teachers and C/C++ teachers about how this tool can help their students... but I never wrote one for Python teachers. Given that the tool is literally named Python Tutor, it's about time. This article shows what it can illustrate across the full range of Python courses, from a student's very first loop up through closures and generators.

One thing to know up front: these visualizations aren't cartoons or animations of a simplified model. The backend runs your code in a real CPython interpreter (Python 3.11 by default, with 2.7 still available for legacy courses) and records what actually happened at every step, so the diagrams reflect Python's true semantics: names bind to objects, objects live on the heap, and frames hold local variables. If a visualization ever surprises you, it's because Python surprised you.

If you think this tool may be helpful for your students, please share this direct link in relevant course materials, chat groups, mailing lists, discussion forums, or social media:

This article starts with first-semester topics and works its way up. If you teach beginners, the first half may be all you need – feel free to stop whenever it goes past your course's level. And if you're a seasoned Pythonista, you may want to skim ahead to the "More Advanced Python" section near the end to see a few things (closures, generators) that few other tools can draw. All of the examples below are live: drag the slider under each one to step forward and back through execution, and click "Edit Code" under any example to open it in the full editor, where you and your students can modify and re-run it.

The Basics: Variables, Loops, and Conditionals

When students learn their first loops, teachers often have them trace code by hand, keeping a little table of each variable's value at each step. Python Tutor works like a self-updating trace table:

At the current step (Step 10), we're partway through the loop: day is 2 and total is 50. Students can step forward and backward to see every value this program ever computes, along with its printed output accumulating in the gray pane. There's nothing to install or set up: students go to the website, type code, and press Visualize.

Programs that read keyboard input work too. When execution reaches a call to input(), the visualizer prompts for a value, and entered values are saved so students can edit and re-run without retyping. Here's a loop that reads three test scores and averages them:

The "User inputs" panel below the code lists all three values that this run was given. At the current step (Step 7), the first two are crossed out in gray because the program has already consumed them: input() has run twice, and the second score is in s, about to be appended to the list. Step backward and forward to watch each call to input() cross off exactly one value at the moment it's read. Students who lose track of which typed value ended up in which variable – especially inside loops – can now simply look.

Functions and the Call Stack

Each function call gets its own frame that holds its local variables:

At the current step, the frames for both checkout and add_tax are on screen at once. Stepping through shows values flowing into parameters and back out through return values (each function's return value is displayed just before its frame disappears). Students who think of functions as "mini-programs that jump around" can finally see the underlying discipline: calls push frames, returns pop them, and each frame keeps its own variables.

(You may also notice that the functions themselves appear as objects in the visualization. That's not a rendering quirk – functions really are objects in Python, a fact that becomes useful later when your students meet higher-order functions.)

Lists and the Famous Aliasing Surprise

If I had to pick the one moment where Python Tutor earns its keep in an intro course, it's this one:

Why does a print as [1, 2, 3, 4] when the code only ever appended to b? Because b = a doesn't copy anything: both names now refer to the same list object, and the diagram shows exactly that – two arrows, one list. By contrast, list(a) creates a genuine copy, which appears as a second list on the heap. You can explain aliasing with a paragraph of prose and students will nod along; showing them two arrows pointing at one list tends to stick better.

The same idea explains what happens when lists are passed to functions:

In the current step, replace_all has just rebound its local name lst to a brand-new list, and there are now two separate lists on the heap: groceries still points at the original one (which add_item successfully mutated earlier), while the new ['coffee'] list is about to vanish when the function returns. This one picture settles the perennial "is Python pass-by-value or pass-by-reference?" argument by just showing what happens: the parameter name binds to the same object that the caller passed in, so mutating the object is visible to the caller, but rebinding the name is not.

Dictionaries, Tuples, and Sets

Here is the classic counting pattern, with the dictionary growing key by key:

At the current step, counts already holds entries for 'cat' and 'dog', and students can watch each loop iteration either add a new key or bump an existing count – the exact distinction that the if animal in counts test exists to make. Tuples and sets render similarly (you'll see a tuple in the next example), as do types from the collections module such as Counter and namedtuple, for courses that use them.

Nested Data Structures

Realistic data is nested: lists of dictionaries, dictionaries of lists, tuples inside both. This is where diagrams beat squinting at printed output:

The loop variable s points at whichever student record is currently being processed – at the current step it's Bob's dictionary, and his 75.0 average is about to lose to Ada's 92.5. Once students can see the arrows, an expression like s['scores'][0] stops being a mysterious incantation and becomes directions for following arrows.

For list-of-lists data there's also an optional grid display (the "show list-of-lists as 2D array" checkbox below the code editor), which is handy for nested-loop units and matrix-style problems:

Objects and Classes

When your course reaches object-oriented programming, the visualizer draws classes, instances, and methods:

At the current step, alice.deposit(25) is executing: the deposit frame's self points at Alice's account object – and only hers. This is the picture that makes self finally click for many students: it's just another name bound to a particular object. The two accounts hold separate state; alice.balance has already changed to 125 while bob.balance remains 50.

Inheritance and method overriding display too:

The class objects themselves appear on the heap, with Dog marked as extending Animal. At the current step, the loop is calling speak() on Rex, so self points at the Dog instance and Python dispatches to Dog's overriding method. Step backward one iteration to watch the very same call site dispatch to Animal's version instead.

Recursion

Since every recursive call gets its own frame with its own copy of the parameters, the call stack builds up and unwinds right before students' eyes:

At the current step, four frames of factorial are stacked up, each holding its own n, and the base case is about to return 1. Stepping forward shows each frame handing its result down to its caller until the original call returns 24.

One related option worth knowing about: "show frames of exited functions" (under advanced options) keeps every finished frame on screen in a faded style, so after the program ends you can review the complete history of calls. Some instructors like teaching recursion that way – it turns the call stack into a call record.

Exceptions

Exception handling is control flow, so it benefits from the same step-through treatment:

Step through the second call to watch control jump from the failing division directly into the except block. Uncaught exceptions display too: the visualizer marks the exact step where the exception occurred, with every variable still on screen for the postmortem, which beats handing beginners a raw traceback.

More Advanced Python

Everything below this point is for courses that go beyond the intro level (or for your own curiosity). If you teach beginners, you can stop here without missing anything your students need.

First, closures – a feature that almost no other tool draws correctly:

make_counter returned long ago, yet its frame lives on because the nested increment function still refers to its count variable. The visualizer draws increment's frame with an explicit link to that parent frame, where you can watch count tick up to 3 across three separate calls. nonlocal, lexical scoping, and the whole idea that "functions carry their birth environment with them" – all in one picture. (If you teach decorators, this diagram is also nine-tenths of how those work.)

Generators visualize too:

Each time the for loop asks for another value, the countdown frame comes back onto the stack, resumes where it left off, and shows the value it's about to yield. Watching a generator's frame suspend and resume demystifies yield faster than any metaphor I've found.

And for the functional corners of the language:

At the current step, sorted is repeatedly calling the lambda, which runs in its own little frame with its own n – anonymous functions get real frames too. Step backward and you'll see that the list comprehension on the second line also ran in its own <listcomp> frame, which is exactly how Python 3 actually scopes comprehension variables. Default arguments, *args and **kwargs, and even circular references (lst.append(lst) renders as a list pointing to itself) all display correctly – cases like these live in the regression test suite that guards this tool.

What the visualizer does not cover

In the interest of honesty, here's what it does not do, so you can plan around it:

Please Help Spread The Word!

Python Tutor can help your students see what their code actually does, from their first for loop to their first closure. It's free, it runs in the browser with nothing to install, and it's been battle-tested by tens of millions of learners over the past 16 years.

Feel free to share this direct link in relevant course materials, chat groups, mailing lists, discussion forums, social media, or anywhere else:

(And if you also teach Java or C/C++, check out my companion articles on what the Java visualizer can do and what the C/C++ visualizer can do.)