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.
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.
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.)
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.
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.
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:
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.
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.
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.
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.
In the interest of honesty, here's what it does not do, so you can plan around it:
math, random, collections,
itertools, functools, re, and json. (One exception is on the
way: reading file input, a natural extension of the keyboard-input
support shown earlier, is currently under active development and
testing – so it's coming soon.)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.)