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.
- 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)
- 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
- An easy way to save drawings
- 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 API is structured around two classes: A singleton
Screen
class that contains the Tkinter canvas and aTurtle
object that is responsible for drawing on the screen. - Most of the user-facing API lies on the
Turtle
class. However, not everything –Screen.bgcolor
being a notable exception. - The
Screen
object contains aScrolledCanvas
– a wrapper of theTKinter
canvas that adds some extra functionality. - Methods of
Turtle
instances are forwarded to theScreen
instance as drawing instructions and are immediately passed for Tkinter to be drawn during the next iteration of the GUI loop. - The code doesn’t follow the encapsulation principle: Whenever
Turtle
-instances do anything, they callScreen._incrementudc
(increment update counter) before checking if the screen should be updated. If so, then the command is passed to the screen. This introduces a bug shown below. - Multiple turtles can draw on the same screen, but they can only write to one screen at a time
- The tight coupling between the “logical drawing” and the GUI window means that we need a larger rewrite of the turtle module if we want to support other backends.
- Some methods are duplicated with the same name but have different behaviours for turtles and the screen. An example is
onscreenclick
, which records when the turtle shape is pressed for turtle instances and when the turtle canvas is clicked for the screen instance.
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:
- Draw straight lines (remember, circles are polygons in turtle)
- Undo drawing a line
- Draw filled polygons
- Draw polygonal vector “stamps”
- Set the background colour
The renderers can also do the following
- Draw custom “stamps” (e.g. raster images)
- Do live updates of the drawing
- Support interactivity such as onclick.
- Support interactive events for clicking the shape of a single turtle.
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.
- Potential bug: The colour of the screen is immediately updated as only turtle instances check if the screen is supposed to be updated
- Bug: Both
espen
andgard
have moved, but onlygard
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ês
and 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.