Live Coding in Python lets you run your Python code as you type it. For example, this code prints a greeting to my friend, Alice.
File Edit Options Buffers Tools Python Help
name = 'Alice' |name = 'Alice'
print('Hello, ' + name + '!') |print('Hello, Alice!')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-UU-:----F1 hello.py All L3 |-UUU:**--F1 *live-py-trace_hello.py_227
When I change the name to Bob, the display on the right immediately changes. I don’t even have to save the file.
File Edit Options Buffers Tools Python Help
name = 'Bob' |name = 'Bob'
print('Hello, ' + name + '!') |print('Hello, Bob!')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 hello.py All L1 |-UUU:**--F1 *live-py-trace_hello.py_227
In this tutorial, I’ll demonstrate two things: a live coding display that can be
used to show you what’s happening inside your code, and live unit tests. To try
it yourself, follow the Emacs installation instructions, then type some code,
as in the example above. Finally, launch the Live Python mode with
M-x live-py-mode
. You should see the display on the right. You can also watch
my demo video.
Live Coding Display
I’ll start with a trivial chunk of code where I assign a variable, and then modify it.
File Edit Options Buffers Tools Python Help
s = 'Hello'
s += ', World!'
-UU-:----F1 hello.py All L3 (Python) --------------------------------
That’s easy to step through in your head and see that s
is now
'Hello, World!'
, but most code you work on is more complicated. Still, you
either predict the result by running through the code in your head, or you run
it to see the result. One of this project’s main goals for live coding is to let
programmers’ brains focus on writing code instead of running code. If you can
see the code’s results laid out in front of you, you don’t have to hold it all
in your head.
I turn on live coding mode with M-x live-py-mode
, and it opens the live coding
display like the one on the right (below). The display shows me what’s in the
variable after each change.
File Edit Options Buffers Tools Python Help
s = 'Hello' |s = 'Hello'
s += ', World!' |s = 'Hello, World!'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-UU-:----F1 hello.py All L3 |-UUU:**--F1 *live-py-trace_hello.py_227
Live-Py mode enabled in current buffer
Let’s do something more interesting and write a library function that does binary search for a value in a sorted array. The live coding will show us what’s happening in our code so we don’t have to hold it all in our heads.
File Edit Options Buffers Tools Python Help
def search(n, a): |
return -1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 hello.py All L3 |-UUU:**--F1 *live-py-trace_hello.py_227
It’s a bad search function that never finds anything, but let’s see how it works when we call it.
File Edit Options Buffers Tools Python Help
def search(n, a): |n = 2 | a = [1, 2, 4]
return -1 |return -1
|
|
i = search(2, [1, 2, 4]) |i = -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 hello.py All L4 |-UUU:**--F1 *live-py-trace_hello.py_227
You can see the input parameters at the start of the function, and the return value at the end.
We’ll start looking for the value in the array, and the first place to look is the middle item.
File Edit Options Buffers Tools Python Help
def search(n, a): |n = 2 | a = [1, 2, 4]
low = 0 |low = 0
high = len(a) - 1 |high = 2
mid = low + high // 2 |mid = 1
if n == a[mid]: |
return mid |return 1
return -1 |
|
i = search(2, [1, 2, 4]) |i = 1
|
|
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 hello.py All L10 |-UUU:**--F1 *live-py-trace_hello.py_227
That was lucky! It was in the first place we looked, and you can see the
calculations as it goes. You see an abstract formula in the code, like
high = len(a) - 1
, and you see the concrete result in the live coding
display, like high = 2
. However, a search function usually won’t find the
item we’re searching for on the first try. Let’s ask for an item earlier in the
list and use a while loop to find it.
File Edit Options Buffers Tools Python Help
def search(n, a): |n = 1 | a = [1, 2, 4]
low = 0 |low = 0
high = len(a) - 1 |high = 2
while True: | |
mid = low + high // 2 |mid = 1 | mid = 0
v = a[mid] |v = 2 | v = 1
if n == v: | |
return mid | | return 0
if n < v: | |
high = mid - 1 |high = 0 |
return -1 |
|
i = search(1, [1, 2, 4]) |i = 0
|
|
|
|
|
|
|
|
-UU-:**--F1 hello.py All L1 |-UUU:**--F1 *live-py-trace_hello.py_227
The loop runs twice, and each run adds a column to the display showing the calculations. That’s a good example of how this tool differs from a debugger. With a debugger, you’re always looking at a single moment in time. Here, you can see the whole history of the search laid out on the screen, and you move back and forth through time just by moving your eye. It’s a lot like the difference that makes static visualizations of sorting algorithms easier to follow than animated sorting algorithms.
Now let’s look for an item later in the list.
File Edit Options Buffers Tools Python Help
def search(n, a): |n = 4 | a = [1, 2, 4]
low = 0 |low = 0
high = len(a) - 1 |high = 2
while True: | |
mid = low + high // 2 |mid = 1 | mid = 3
v = a[mid] |v = 2 | IndexError: list index out of$
if n == v: | |
return mid | |
if n < v: | |
high = mid - 1 | |
else: | |
low = mid + 1 |low = 2 |
return -1 |
|
i = search(4, [1, 2, 4]) |IndexError: list index out of range
|
|
|
|
|
|
-UU-:**--F1 hello.py All L1 |-UUU:**--F1 *live-py-trace_hello.py_227
Oops, I get an IndexError. Without the live coding display, I would just get a
traceback that shows where the error happened, but not how it happened. Now, I
can walk back from the error to see where things went wrong. mid
is the index
value, and it’s calculated at the top of the loop. The two values that go into
it are both 2, so they should average to 2. Oh, I need parentheses to calculate
the average.
File Edit Options Buffers Tools Python Help
def search(n, a): |n = 4 | a = [1, 2, 4]
low = 0 |low = 0
high = len(a) - 1 |high = 2
while True: | |
mid = (low + high) // 2 |mid = 1 | mid = 2
v = a[mid] |v = 2 | v = 4
if n == v: | |
return mid | | return 2
if n < v: | |
high = mid - 1 | |
else: | |
low = mid + 1 |low = 2 |
return -1 |
|
i = search(4, [1, 2, 4]) |i = 2
|
|
|
|
|
|
-UU-:**--F1 hello.py All L1 |-UUU:**--F1 *live-py-trace_hello.py_227
What happens if we try to find a value that’s not in the list?
File Edit Options Buffers Tools Python Help
def search(n, a): |n = 3 | a = [1, 2, 4]
low = 0 |low = 0
high = len(a) - 1 |high = 2
while True: | | | | $
mid = (low + high) // 2 |mid = 1 | mid = 2 | mid = 1 | mid = 1 $
v = a[mid] |v = 2 | v = 4 | v = 2 | v = 2 $
if n == v: | | | | $
return mid | | | | $
if n < v: | | | | $
high = mid - 1 | | high = 1 | | $
else: | | | | $
low = mid + 1 |low = 2 | | low = 2 | low = 2 $
return -1 |
|
i = search(3, [1, 2, 4]) |RuntimeError: live coding message limit$
|
|
|
|
|
|
-UU-:**--F1 hello.py All L1 |-UUU:**--F1 *live-py-trace_hello.py_227
I guess that while True wasn’t such a good idea, we’re stuck in an infinite loop. If you want to see some of the later loop runs, you can scroll over to the right.
From the third run on, the values in the loop don’t change, so we probably want to exit from the second or third run. If you look at the end of the second run, you can see that high is lower than low. That means that we’ve searched all the way from both ends to meet in the middle, and it’s time to give up.
File Edit Options Buffers Tools Python Help
def search(n, a): |n = 3 | a = [1, 2, 4]
low = 0 |low = 0
high = len(a) - 1 |high = 2
while low <= high: | |
mid = (low + high) // 2 |mid = 1 | mid = 2
v = a[mid] |v = 2 | v = 4
if n == v: | |
return mid | |
if n < v: | |
high = mid - 1 | | high = 1
else: | |
low = mid + 1 |low = 2 |
return -1 |return -1
|
i = search(3, [1, 2, 4]) |i = -1
|
|
|
|
|
|
-UU-:**--F1 hello.py All L1 |-UUU:**--F1 *live-py-trace_hello.py_227
At this point, I think I’m done. I can add a few entries and search for them to make sure everything is working. Also, if this were a real library module, I wouldn’t want to execute a call at the end of the file, so I only do it when I’m in live coding mode.
File Edit Options Buffers Tools Python Help
def search(n, a): |n = 3 | a = [1, 2, 4]
low = 0 |low = 0
high = len(a) - 1 |high = 2
while low <= high: | |
mid = (low + high) // 2 |mid = 1 | mid = 2
v = a[mid] |v = 2 | v = 4
if n == v: | |
return mid | |
if n < v: | |
high = mid - 1 | | high = 1
else: | |
low = mid + 1 |low = 2 |
return -1 |return -1
|
if __name__ == '__live_coding__': |
i = search(3, [1, 2, 4]) |i = -1
|
|
|
|
|
-UU-:**--F1 hello.py All L16 |-UUU:**--F1 *live-py-trace_hello.py_227
Live Unit Tests
In that example, I kept changing the parameters to search for different items in the list. Wouldn’t each set of search parameters make a nice unit test? I think unit tests help you write better code, so you can use the live coding display as you add each unit test and make it pass.
In this section, I’ll write a function that counts the number of unique words in a list. However, words with the same letters are counted as the same word. For example, the words “apple”, “lemon”, and “melon” would only count as two words, because “lemon” and “melon” have the same letters in different order.
To start, I turn off Live Coding mode with M-x live-py-mode
, then open a new
file test_anagrams.py
and write a simple unit test that doesn’t have any
duplicate words.
File Edit Options Buffers Tools Python Help
from unittest import TestCase
from anagrams import count_anagrams
class AnagramsTest(TestCase):
def test_words(self):
words = ['apple', 'melon']
n = count_anagrams(words)
self.assertEqual(2, n)
-UU-:**--F1 test_anagrams.py All L11 (Python) ----------------------------
I can run that test either by switching to another terminal window, or with the
M-x compile
command in Emacs. Either way, use the command
python -m unittest test_anagrams
Of course, that fails when I run it as a unit test, because I haven’t written
anagrams.py
and the count_anagrams()
method. I start by creating
anagrams.py
with a stupid version that always returns zero.
File Edit Options Buffers Tools Python Help
def count_anagrams(words):
return 0
-UUU:----F1 anagrams.py All L4 (Python) --------------------------------
The test now fails with a reasonable complaint.
File Edit Options Buffers Tools Compile Help
def count_anagrams(words):
return 0
-UUU:----F1 anagrams.py All L4 (Python) --------------------------------
======================================================================
FAIL: test_words (test_anagrams.AnagramsTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/don/workspace/scratch/test_anagrams.py", line 10, in test_words
self.assertEqual(2, n)
AssertionError: 2 != 0
----------------------------------------------------------------------
Ran 1 test in 0.000s
-UUU:%*--F1 *compilation* 22% L10 (Compilation:exit [1]) -----------------
I want to see what’s happening as I make the unit test pass, so I close the
compile buffer with C-x 1
, and launch Live Coding mode with M-x live-py-mode
.
Nothing happens at first, because nothing is calling my count_anagrams()
function. I need to set the driver script to be my unit test, with C-c M-d
and
then enter this driver script:
-m unittest test_anagrams
Now, I see the call that the unit test makes:
File Edit Options Buffers Tools Python Help
|---------------- |
|SystemExit: True |
|---------------- |
def count_anagrams(words): |words = ['apple', 'melon']
return 0 |return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-UU-:----F1 anagrams.py All L5 |-UUU:**--F1 *live-py-trace_anagrams.py_
I can see the input parameters and the return value, as well as the fact that the test failed. Next, I make that test pass with the simplest code that could possibly work.
File Edit Options Buffers Tools Python Help
|
|
|
def count_anagrams(words): |words = ['apple', 'melon']
return len(words) |return 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 anagrams.py All L5 |-UUU:**--F1 *live-py-trace_anagrams.py_
Once the test passes, I can add another test method with another scenario. This one includes two copies of ‘melon’, so the number of unique words is still two.
File Edit Options Buffers Tools Python Help
from unittest import TestCase
from anagrams import count_anagrams
class AnagramsTest(TestCase):
def test_words(self):
words = ['apple', 'melon']
n = count_anagrams(words)
self.assertEqual(2, n)
def test_duplicate_words(self):
words = ['apple', 'melon', 'melon']
n = count_anagrams(words)
self.assertEqual(2, n)
-UU-:----F1 test_anagrams.py All L18 (Python) ----------------------------
I could make the test pass now, but it’s a little confusing when both tests are being displayed.
File Edit Options Buffers Tools Python Help
|---------------- |
|SystemExit: True |
|---------------- |
def count_anagrams(words): |words = ['apple', 'melon', 'melon'] | w$
return len(words) |return 3 | r$
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 anagrams.py All L5 |-UUU:**--F1 *live-py-trace_anagrams.py_
Instead, I’ll convince Emacs to only run the new test method. That becomes
even more useful as we add more and more test methods. I open the test file, and
rename the new test method to plain test()
. Then switch back to the
anagrams.py
file, and use C-c M-d
to change the driver script to this:
-m unittest test_anagrams.AnagramsTest.test
The up arrow will cycle through previous driver scripts, so I don’t have to type
the whole thing again. From now on, I’ll add each test method as plain test()
so the driver will run it, then give it a full name when it’s passing. Now you
can see the failing test on its own.
To remove duplicates, just put all the words into a set before counting.
File Edit Options Buffers Tools Help
|$
|$
|$
def count_anagrams(words): |$'melon']
anagrams = set() |$
for word in words: |$ = 'melon' | word = 'me$
anagrams.add(word) |$rams = {'melon', 'apple'} |
return len(anagrams) |$
|
|
|
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 anagrams.py All L7 |-UUU:**--F1 *live-py-trace_anagrams.py_
When you get to the second copy of ‘melon’, the set doesn’t change. To see the
later iterations of the loop, you might have to use C-x o
to switch to the
other window, then scroll right.
Now we get to the interesting part: detecting anagrams.
File Edit Options Buffers Tools Python Help
n = count_anagrams(words)
self.assertEqual(2, n)
def test(self):
words = ['apple', 'melon', 'lemon']
n = count_anagrams(words)
self.assertEqual(2, n)
-UU-:----F1 test_anagrams.py Bot L20 (Python) ----------------------------
One way is to sort the letters in each word.
File Edit Options Buffers Tools Help
|$
|$
|$
def count_anagrams(words): |$lon', 'lemon']
anagrams = set() |$
for word in words: |$| word = 'melon' | word$
word = ''.join( |$| word = 'elmno' | word$
sorted(word)) |$| |
anagrams.add(word) |$| anagrams = {'aelpp', 'elmno'} |
return len(anagrams) |$ $
|
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 anagrams.py All L10 |-UUU:**--F1 *live-py-trace_anagrams.py_
You can see that the second and third iteration of the loop convert
‘melon’ and ‘lemon’ to ‘elmno’, and the set of anagrams
doesn’t
change in the third iteration.
The next feature I want to add is to treat upper case and lower case the same, so I add a new test case.
File Edit Options Buffers Tools Python Help
n = count_anagrams(words)
self.assertEqual(2, n)
def test_anagrams(self):
words = ['apple', 'melon', 'lemon']
n = count_anagrams(words)
self.assertEqual(2, n)
def test(self):
words = ['Melon', 'Lemon']
n = count_anagrams(words)
self.assertEqual(1, n)
-UU-:----F1 test_anagrams.py Bot L26 (Python) ----------------------------
Back in anagrams.py
, I see the test fail.
File Edit Options Buffers Tools Python Help
|$--- |
|$rue |
|$--- |
def count_anagrams(words): |$on', 'Lemon']
anagrams = set() |$t()
for word in words: |$' | word = 'Lemon'
word = ''.join( |$' | word = 'Lemno'
sorted(word)) |$ |
anagrams.add(word) |$Melno'} | anagrams = {'Melno', 'Lemno'$
return len(anagrams) |$
|
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 anagrams.py All L10 |-UUU:**--F1 *live-py-trace_anagrams.py_
You can see that ‘Melon’ and ‘Lemon’ get sorted into ‘Melno’ and ‘Lemno’, because upper-case letters sort before lower-case letters. We can fix that by switching all the words to lower case.
File Edit Options Buffers Tools Python Help
|$--- |
|$rue |
|$--- |
def count_anagrams(words): |$on', 'Lemon']
anagrams = set() |$t()
for word in words: |$' | word = 'Lemon'
word = ''.join( |$' | word = 'Lemno'
sorted(word)) |$ |
word = word.lower() |$' | word = 'lemno'
anagrams.add(word) |$melno'} | anagrams = {'lemno', 'melno'$
return len(anagrams) |$
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 anagrams.py All L11 |-UUU:**--F1 *live-py-trace_anagrams.py_
Oops, ‘Melon’ and ‘Lemon’ now get sorted into ‘melno’ and ‘lemno’. We fixed the case, but not the sort order. Switching to lower case before sorting the letters will fix it.
File Edit Options Buffers Tools Python Help
|$
|$
|$
def count_anagrams(words): |$on', 'Lemon']
anagrams = set() |$t()
for word in words: |$' | word = 'Lemon'
word = word.lower() |$' | word = 'lemon'
word = ''.join( |$' | word = 'elmno'
sorted(word)) |$ |
anagrams.add(word) |$elmno'} |
return len(anagrams) |$
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 anagrams.py All L11 |-UUU:**--F1 *live-py-trace_anagrams.py_
Finally, I want to handle foreign words correctly. For example, the German word for street can be written either as ‘Straße’ or ‘Strasse’. Python knows how to convert from one to the other, so I’ll add another test case.
File Edit Options Buffers Tools Python Help
self.assertEqual(2, n)
def test_upper(self):
words = ['Melon', 'Lemon']
n = count_anagrams(words)
self.assertEqual(1, n)
def test(self):
words = ['Straße', 'Strasse']
n = count_anagrams(words)
self.assertEqual(1, n)
-UUU:----F1 test_anagrams.py Bot L33 (Python) ----------------------------
When I run the new test case, the words are counted separately.
File Edit Options Buffers Tools Python Help
|---------------- |
|SystemExit: True |
|---------------- |
def count_anagrams(words): |words = ['Straße', 'Strasse']
anagrams = set() |anagrams = set()
for word in words: |word = 'Straße' | word = 'Strasse$
word = word.lower() |word = 'straße' | word = 'strasse$
word = ''.join( |word = 'aerstß' | word = 'aerssst$
sorted(word)) | |
anagrams.add(word) |anagrams = {'aerstß'} | anagrams = {'ae$
return len(anagrams) |return 2
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 anagrams.py All L7 |-UUU:**--F1 *live-py-trace_anagrams.py_
To fix it, I just switch lower()
to casefold()
.
File Edit Options Buffers Tools Python Help
|
|
|
def count_anagrams(words): |words = ['Straße', 'Strasse']
anagrams = set() |anagrams = set()
for word in words: |word = 'Straße' | word = 'Strass$
word = word.casefold() |word = 'strasse' | word = 'strass$
word = ''.join( |word = 'aerssst' | word = 'aersss$
sorted(word)) | |
anagrams.add(word) |anagrams = {'aerssst'} |
return len(anagrams) |return 1
|
|
|
|
|
|
|
|
|
|
-UU-:**--F1 anagrams.py All L7 |-UUU:**--F1 *live-py-trace_anagrams.py_
You can see that casefold()
converts ‘ß’ to ‘ss’, while still converting ‘S’
to ‘s’, and the test passes.
Now that I’ve made each test pass, I run the full test suite again to make sure
I didn’t break any of the other tests. The easiest way to run it is with
M-x compile
.
File Edit Options Buffers Tools Python Help
|-*- mode: compilation; default-director$
|Compilation started at Sat Nov 2 22:48$
|
def count_anagrams(words): |python -m unittest test_anagrams
anagrams = set() |.....
for word in words: |---------------------------------------$
word = word.casefold() |Ran 5 tests in 0.000s
word = ''.join( |
sorted(word)) |OK
anagrams.add(word) |
return len(anagrams) |Compilation finished at Sat Nov 2 22:4$
|
|
|
|
|
|
|
|
|
|
-UU-:----F1 anagrams.py All L7 |-UUU:%*--F1 *compilation* All L1
Compilation finished
It looks good, so I can publish my new library.
There are some extra features available if you install the space tracer library. Remember, you can find installation instructions and descriptions of all the other plugins and tools by visiting donkirkby.github.io. Help me test it, and report your bugs. I’d also love to hear about any other projects working on the same kind of tools.