Improving the Turtle library (original) (raw)

The Idea

I’m a big fan of the turtle module (to the point where MarieRoald and I created a library to make embroidery patterns with turtle commands: https://turtlethread.com/), and I have used turtle.py several times in introductory Python courses. However, the more I’ve used it, the more I notice features that I wish were present.

Here is a list of the main features I’m missing and that I’ve found other educators miss as well.

  1. Context managers for begin/end-functions
    • begin_fill/end_fill
    • begin_poly/end_poly
    • turtle.tracer(0)/turtle.update(); turtle.tracer(1) (An easy way to disable automatic canvas updates)
  2. Context managers for other utilities that are frequently changed back and forth in loops
    • penup / pendown (might be fixed with the new teleport method)
    • color, pencolor, fillcolor
    • pensize
  3. An easy way to save drawings
  4. Decoupling turtle.py from Tkinter (big change)

At the end of this post, I have also listed some oddities and (potential) bugs that Marie and I have found working on this.

The Utility

Context managers for begin/end-functions

I’ve typically used Turtle to introduce variables, loops and functions. However, when we move on to context managers, I can no longer use turtle. So I’ll have to teach learners to close their files after opening them and also what a context manager is at the same time. It would be extremely useful to demonstrate context managers with Turtle so I only have to explain one concept at a time.

Filling polygons are particularly well suited for this! You always need to call begin_fill and end_fill, which would be the perfect visual demonstration of what context managers do.

Unfortunately, Turtle doesn’t work this way, so I’ll have to resolve to hand-waving about context managers while also explaining file-opening – and whenever someone asks, “Why don’t we have this in Turtle?” (I’ve been asked on more than one occasion), I don’t have any good answer.

As an addendum to this, I would love a context manager for disabling auto-update that resets and updates the screen once exited.

Example code:

import turtle

with turtle.disable_autoupdate():
    with turtle.fill():
        for side in range(4):
            turtle.forward(50)
            turtle.right(90)

Context managers for other utilities

Really the same as above. However, for the three cases above, we already follow the setup-teardown pattern manually. For penup/pendown, color, pencolor, fillcolor and pensize, we don’t explicitly follow this pattern in the same way. However, we still do often want to change colour, pen size, etc temporarilly.

An easy way to save drawings

The turtle library supports saving the drawings as postscript files by calling the cryptic command turtle.getscreen().getcanvas().postscript(file=filename). Whenever a learner asks “How can I print what I made?”, I’ll have to show them that extremely scary command with method chaining - not the best snippet to show someone who’s still struggling with functions…

If we instead had a turtle.save(filename, format="postscript"), or maybe a turtle.getscreen().save(filename), then I could tell the learners to use that function and an online postscript to pdf converter instead.

An easy way to disable auto-updating the canvas

The Turtle library quickly becomes slow if you want to draw complex drawings, which leads some learners to find the cryptic turtle.tracer(0) command. This command disables automatically updating the canvas, and lets the learner draw many lines at once by calling turtle.update(). This is a must-have for anyone that uses turtle for anything a bit complicated.

Unfortunately, explaining why you write turtle.tracer(0) is not something I have managed in a good way, and I think it would be much easier to tell my learners to use something like “turtle.disable_autoupdate()” instead of turtle.tracer(0).

Decoupling turtle.py from Tkinter

turtle.py is tightly coupled with tkinter. In fact, you cannot even import the turtle module if you have a Python version built without Tkinter. In an ideal world, we would have a general Turtle API in Python where anyone can implement their own backend on top of it. This could enable fully compatible turtle interfaces across various platforms that currently don’t support Tkinter (like PyScript), saving the canvas as something other than a postscript file and more.

There are also several reasons for why I would like to import Turtle on systems without Tkinter both in TurtleThread and elsewhere. In TurtleThread, we inherit from the TNavigator class from the Turtle module. However, we also want it to be possible to run TurtleThread on a web server without Tkinter installed. To facilitate this, we had to copy the TNavigator class into a separate file. We also have some visualisation code that we use Xvfb to test, and it would be great if we could switch out the turtle-type with something that doesn’t draw on a Tkinter canvas.

Marie Roald and I spent some time on PyCon US 2024 trying to figure out how to do this without breaking backwards compatibility and we have since then tried to assemble our thoughts.

Thoughts on how to decouple turtle.py from Tkinter

Some observations about the current API (can be skipped)

The proposed API

We propose to define an official Turtle interface in a backend-agnostic way. Implementors of new Turtle backends (e.g. for WASM-builds of CPython, Jupyter notebooks or image exporters) can then all agree on how to implement Turtle renderers and what they should include (outside the minimal requirements).

Specifically, we propose to define three main classes: Turtle, Canvas and Renderer. Each turtle draws on exactly one canvas (stored as an immutable variable), and the canvas has a list of turtles that have drawn on it. Whenever a command is passed to a turtle, it forwards it to the Canvas, which stores a programmatic list of them. Then, if the canvas’ current update counter is zero, it forwards the commands to the renderers.

The renderers must be able to do the following:

The renderers can also do the following

However, this architecture is not compatible with the current state of affairs. Therefore, to ensure backward compatibility, we can also include a Screen class, which is a wrapper of both a canvas and a renderer (maybe the renderer type can be controlled with an environment variable?).

Oddities, bugs and potential bugs

The turtle.tracer-bug

Run the following code:

import turtle

espen = turtle.Turtle()
gard = turtle.Turtle()
turtle.tracer(3)  # Only update every third command
turtle.bgcolor("pink")
espen.forward(100)
gard.right(90)
gard.forward(100)  # The third command, will trigger screen update

There are two oddities here, one of which is definitely a bug.

  1. Potential bug: The colour of the screen is immediately updated as only turtle instances check if the screen is supposed to be updated
  2. Bug: Both espen and gard have moved, but only gard made a line on the screen.

The Terminator exception

Run the following code

import turtle

turtle.forward(50)
turtle.done()

turtle.forward(100)
turtle.done()

By reading this code, you’d think that you first get one turtle drawing and once you close it, you’ll get a second turtle drawing.
However, this is not the case. You’ll get a turtle.Terminator exception instead as the singleton Screen-object is closed.
I’ve seen people mistakenly saying that calling turtle.bye will fix this, but that is not the case – it will only trigger the turtle.Terminator exception again.
I have two solutions:


import turtle

turtle.forward(50)
turtle.done()

try:
    turtle.bye()
except Exception:
    pass

turtle.forward(100)
turtle.done()

or by manually modifying “private” variables:

import turtle

turtle.forward(50)
turtle.done()

turtle.Turtle._screen = None  # force recreation of singleton Screen object
turtle.TurtleScreen._RUNNING = True  # only set upon TurtleScreen() definition

turtle.forward(100)
turtle.done()

neither are good solutions in an intro to Python class.

This bug has been a big headache during teaching since this bug will trigger for anyone that use an interactive coding environment like Spyder.

Undo quircks

Turtle-specific undo buffers

Each turtle object has an undo-buffer, and calling turtle.undo() will undo the latest command by the global Turtle-instance, so if you have multiple turtles (e.g. inêsand the global turtle-instance), then turtle.undo() might behave in surprising ways. Try, for example:

import turtle

inês = turtle.Turtle()
inês.forward(100)
turtle.undo()

Confusing clone behaviour

We can also clone turtles, which is even more confusing!

import turtle

inês = turtle.Turtle()
inês.forward(100)
inês2 = inês.clone()
inês2.undo()

Here, inês2 moves back to to (0, 0) while inês stays put at (100, 0). However, the line that inês drew disappeared!

Custom deque?

The undo-function implements its own circular buffer instead of using a deque. I cannot understand why, especially since (as far as I can tell) the deque was added before undo was added to turtle.py?

Prior Discussion

I haven’t found any discussion on this online, but I mentioned it to some core team members and educators at PyCon US 2023 and PyCon US 2024 and received positive feedback – educators, in particular, seemed excited about the proposed changes. The main question was how functional changes to the turtle module would affect existing teaching materials and schools and universities that may be on older versions of Python.

While it could be confusing for learners in the beginning to see that Python code they find online doesn’t work on a school computer with an old Python version, I don’t think that alone is worth skipping these changes. Teaching materials change all the time, and I believe that these changes will bring more positive changes than negative.

Who will make these changes

Marie and I would be happy to attempt to make these changes. However, our free time is unfortunately very limited these days, so progress on the main rewrite will be slow if we lead the work on it.