Zen and The Art of Python `turtle` Animations • A Step-by-Step Guide
Python's `turtle` is not just for drawing simple shapes
We all need something calming from time to time. You have a choice. You can watch this animation video over and over again. Or you can go through the step-by-step tutorial to write the code. Then, you can watch your own animation over and over again.
This article differs from the ones I published on The Stack so far. Instead of digging underneath the surface of a specific programming topic, we'll have a gentle stroll through the steps needed to create a fun and calming animation. But there’s more to this project than meets the eye.
We'll start with a blank screen and build this animation one element at a time, controlling all the moving parts, the user interaction, and the speed of the animation. Visual projects such as this give us a unique insight into the progression of writing a program.
I've been known to use the turtle
module from time to time. But if you never used this module before, there's nothing to worry about. As we progress through this article, I'll outline everything you need to know.
0 • The Ingredients of The Animation
Look at the video of the animation at the top of this article. You'll see:
An arrow spinning in the centre of the screen
Balls spawn from the centre at regular time intervals. The direction of travel of each ball is the same as the direction in which the arrow is pointing when it's spawned
The rotation speed of the arrow can be altered by the user. One key increases the clockwise rotation speed. Another key increases the anticlockwise rotation speed
The balls leave the screen when they move past the edges of the window. However, the user can turn on bouncing off the edges by pressing a key. Balls will then bounce back into the window until the user toggles the bouncing off again
Let's start building this animation.
1 • The Spinning Arrow
We'll start with an arrow spinning at a constant speed. But first, we'll need to figure out how to use the turtle
module to draw on the screen. Luckily, getting to grips with this module is not too difficult.
You can start by creating an instance of the Turtle
class:
You create a Turtle
object. When you run the code, the Turtle
is shown as an arrow pointing towards the right in the middle of the window. The final line, turtle.done()
, keeps the program running until the window is closed:
Next, you can use the .left()
method to rotate the Turtle
object. Since you want the arrow to keep rotating continuously, you can add a while
loop and rotate the arrow within the loop:
The turtle
module automatically creates the window when you create a Turtle
object. However, you can also create this explicitly. This will enable you to have more control over when things are drawn on the screen:
The arrow spins faster in the latest version compared to the previous one. This faster animation speed is due to adding window.tracer(0)
when you create the screen and window.update()
in the while
loop. The first of this pair of method calls, window.tracer(0)
, prevents every change in the Turtle
object's position or orientation from being drawn on the screen. The drawing on the screen is updated when you call window.update()
.
Since drawing images on the screen is time-consuming, choosing to update the display on the screen only once within each loop makes the drawing more efficient. Each iteration of the while
loop represents one frame of the animation. Therefore, the window.update()
call in the while
loop updates the display once in every frame of the animation.
You've got a spinning arrow. Next, you can create a stream of balls that emerges from the arrow at the centre.
2 • The Stream of Balls
You'll use more Turtle
objects for the balls, which spawn at the centre of the screen at regular time intervals. When a ball is spawned, its direction of travel matches the direction the arrow is pointing.
Let's look at the next steps you'll need:
Create a list you'll use to store all the
Turtle
instances representing the ballsDefine a function to:
spawn a new ball
change its colour
set its position and direction to match the spinning arrow
append the instance to the list
Start a timer and spawn a new ball once a time interval elapses
Move all the balls forward in each frame
Let's translate these steps to code:
You define two more variable names, spawn_interval
and ball_speed
. You choose a random colour by creating a tuple of random red, green, and blue values. random.random()
returns a random float between 0
and 1
.
The spawn_ball()
function accepts another Turtle
instance as an argument. The new ball's position and orientation will mirror that in reference
. You'll use the spinning arrow as an argument for this function.
The function performs the following actions:
Creates a new
Turtle
instance and appends it to the list. You index the list using the-1
index in the function to access the last ball added to the listChanges the shape and colour of the object using the methods
.shape()
and.color()
Reduces the size of the shape drawn to half its default size using
.turtlesize()
Raises the "pen" so that when the
Turtle
object moves, it doesn't draw a lineSets the ball's position to the position of
reference
, which is the spinning arrow. You use.setposition()
to change the position of aTurtle
and.position()
to return the current position of aTurtle
Sets the orientation of the ball to match the orientation of
reference
..setheading()
changes the orientation of aTurtle
and.heading()
returns the current heading of aTurtle
In the main animation loop, you add a couple of blocks:
You call
spawn_ball(spinner)
when the time interval elapses. You reset the timer each time a new ball is set. The timer is set initially before thewhile
loopYou loop through the list of balls and move them all forward
When you run this animation, you'll see a stream of balls shooting out from the centre:
Cleaning up
As you let the animation run, you'll notice it will start slowing down. You're creating lots of balls and adding them to the list. The code still moves all the balls, even when they leave the screen! Therefore, you need to get rid of the balls when they leave the screen to avoid accumulating too many Turtle
objects:
In the for
loop block that iterates through the list of balls, you add an if
statement to check whether the ball has left the screen. The built-in abs()
function returns the positive value of its argument, whether it's positive or negative. You pass ball.xcor()
or ball.ycor()
to abs()
. These methods return the current x- and y-coordinates of the ball. You compare these values to the edges using the width and height of the window.
If the ball leaves the screen, you remove the image of the turtle using .hideturtle()
and remove the ball from the list of all balls. This makes a noticeable difference to the animation, which runs faster now without the slowing down you saw earlier if you let the animation run for longer.
You may have noticed another change in the code above. You're no longer looping through balls
in the for
loop but through a copy of it. This option is preferred now since you're removing items from the list within the loop, and we shouldn't mutate a list we're looping through to avoid missing any of the objects during iteration.
There's a bit more tidying up we could do. Even though you remove the balls from the list of balls, the objects are still present in the program. Over time, this takes up memory. Time to recycle.
Recycling Turtle
objects
Instead of removing Turtle
objects when they leave the screen and creating new Turtle
instances each time you need a new ball, you can store the balls that leave the screen in a pool of balls and then use those balls when you need a new one.
By recycling the Turtle
instances, you ensure you don't have excess Turtle
objects. This method can sometimes increase efficiency further if creating new instances of an object is expensive. Let's implement this recycling regime:
You add a pool_of_balls
list to hold inactive balls when they leave the screen. The spawn_ball()
function now checks whether pool_of_balls
is non-empty. If there are balls in pool_of_balls
, one of them is removed from pool_of_balls
and added to balls
. If pool_of_balls
is empty, a new ball is created.
In the while
loop, when a ball leaves the screen, you append it to pool_of_balls
. There's also a call to print()
to output the length of three lists. Two of these lists are ones you've defined in your code. The third one is returned by turtle.turtles()
. This list is maintained by the turtle
module, and it's a list that contains all the turtles in the program. The number of Turtle
objects in the third of these lists soon reaches a steady state, showing that the program doesn't create more Turtle
objects than it needs!
The next step is to add user interaction to this animation to control the spinning arrow's speed of rotation.
3 • The Speed of Rotation
You can add user interaction by binding a function to a key using window.onkeypress()
. You'll need to define a function which increases or decreases the spinning arrow's rotation speed:
Let's look at the changes to the code in this step:
You add a new variable:
increase_rotation_step
You define two new functions:
increase_anticlockwise_rotation()
anddecrease_anticlockwise_rotation()
You bind these functions to the left and right arrow keys using
window.onkeypress()
. You also need to callwindow.listen()
to be able to record keypresses during the animation
There's one other change you had to do. To modify the rotation speed within the functions, you couldn't use the global variable rotation_speed
you had before since variables within the functions are local. Instead, you "convert" the global variable into a data attribute (sometimes called instance variable) for spinner
, which is a Turtle
object. Therefore, you use spinner.rotation_speed
everywhere you need to refer to the speed of rotation.
The user can now control the speed of rotation using the arrow keys. This, in turn, affects the trajectory of the stream of balls:
Before adding more user interaction, you can change the colour of the balls after a time interval. You can add another timer to change the ball colours:
You also change the screen's background colour and the spinning arrow's colour—dark mode looks better!
In the next section, you'll add more user interaction in the animation by toggling the screen edges' behaviour.
I have a beginner’s textbook, written in the same relaxed and friendly manner as these articles. Available in paperback and ebook.
4 • The Bouncing Off The Edges
In the last section, you dealt with what happens when the balls leave the screen. Let's also add the option to turn the edges into "hard edges" so the balls bounce back into the screen when they hit an edge.
You'll need a flag to toggle between turning the hard edges on or off and a function to change the value of the flag when a key is pressed:
Let's look at the changes to the code:
Create a data attribute (instance variable)
window.bounce_on
, which you set toFalse
initially. You also show the status of the edges in the window's title barDefine
toggle_bouncing()
to change the value of thewindow.bounce_on
flag and update the window's title bar. This function is bound to the space bar usingwindow.onkeypress()
Check whether
window.bounce_on
isTrue
orFalse
in each frame of the animation. If the flag is turned on, you change the ball's heading when it hits one of the edges. The arithmetic is different depending on whether the ball hits the top or bottom edges or the left or right edges
When you run the animation, you can now use the left and right arrow keys to control the speed of rotation and the space bar to toggle bouncing off the edges of the window:
You'll notice the speed of the animation changing depending on how many balls there are flying around. When you turn on bouncing off the edges, the balls are not recycled and remain within the list balls
. This slows down the animation since there are more balls to move in each frame. When you open the edges again, balls start to escape from the window, and the animation speeds up again as the number of balls that need to move in each frame decreases.
In the final section of this article, we'll work on this issue to control the animation's speed.
5 • The Speed of The Animation
Each iteration of the while
loop represents a frame of the iteration. However, at the moment, we have no control over how long it takes for the program to run one iteration of the while
loop. Therefore, the time it takes for the animation to draw one frame is variable. One of the major bottlenecks is moving all the balls forward in each frame of the game. So, when you turn on the "hard edges", and the balls bounce back into the window, the animation slows down as there are more balls to draw in each frame.
You can time how long each frame of the animation takes by adding another timer. You could display the time of each frame. However, you'll get a better sense of what's happening if you average a number of frames. In this version, you'll show the average frame time from the last 100 frames of the animation:
You use a deque to keep the times of the last 100 frames since this allows you to efficiently add new values to the end of the deque and also remove values from the beginning. You set the deque's maximum length to 100, which means that old values are automatically removed from the deque when you add a new value if the deque is full.
When I run this animation on my computer, the average frame settles to 0.0084 seconds when the 'gates are open', and all the balls leave the screen. The animation starts to slow down when I press the space bar to turn on bouncing at the edges. On my computer, the average frame time was about 0.045 seconds after a minute and 0.080 after two minutes—that's about 10 times slower than at the beginning of the animation. However, once I pressed the space bar again, the balls could leave the screen, and the time per frame went down again.
Let's set a frame rate so every frame takes the same amount of time to render. The time per frame I got after two minutes of letting the animation run with the balls bouncing off the edges was around 0.045. This is just a bit faster than 20 frames per second (fps). So, I'll set the frame rate to 20 fps. At the end of each iteration of the while
loop, you can pause the loop until the desired frame rate is reached. This approach means that the animation will run at a slower speed from the start, but the speed will not change. You can adjust the initial parameters spawn_interval
, ball_speed
, increase_rotation_speed
, and spinner.rotation_speed
to adjust the perceived speed of the animation:
The frame time is now steady and constant at 0.05 seconds right from the beginning. When you turn on bouncing off the edges, you won't see any slowing down of the animation. Note that, if you let the animation run long enough with bouncing turned on, the animation will slow down since the frame rate can slow down below your required 20 frames per second. We won't look at more complex techniques to deal with displaying the animation in this article.
This code produces the animation you saw at the beginning of this article.
Final Words
This brings us to the end of this step-by-step guide to creating the animation. We've explored how to deal with several moving parts—literally. Working on visual projects such as this animation gives us a unique perspective into the process of gradually building a computer program since we can clearly see the steps build up towards the final state.
The turtle
module may be one of the simplest in the standard library. But this doesn't mean all projects that use this module are trivial. In this project, we've discussed frame rates in animations, reusing instances, using multiple timers, and we even used the deque data structure to efficiently add and remove items from both ends.
Still, I'll return to "normal service" from the next article, digging underneath the surface of particular Python topic. But I may revisit the turtle
module at some point, too!
Code in this article uses Python 3.11
Stop Stack
Today's article is a different style to the ones published so far. This is a good opportunity to write about the style of articles I tend to write for those who are new here. Broadly-speaking, articles will fall into one of three categories:
Most articles will look at some general aspect or branch of Python programming but do so from a different perspective to the norm. These articles reflect my way of thinking and learning about programming. The traditional articles you find elsewhere on the web and in conventional textbooks are fine. But sometimes, we need to look at topics from a different viewpoint to trigger different thought processes in our brains. To see examples of these types of articles you can see the Harry Potter-themed series about OOP or the articles about data structure categories I've been publishing since the start of The Stack, or the last article from a few days ago about taxi drivers and mappings
Or they'll be a deep-dive into a specific Python topic. Often, this will focus on a specific function or class from the standard module or another key library such as NumPy and Matplotlib. See the link to the article about
functools.partial()
below as an exampleFrom time to time, I'll write step-by-step tutorials, such as the one in this article.
Recently published articles on The Stack:
The One About The Taxi Driver, Mappings, and Sequences • A Short Trip to 42 Python Street. How can London cabbies help us learn about mappings and sequences in Python?
Deconstructing Ideas And Constructing Code • Using the Store-Repeat-Decide-Reuse Concept. Starting to code on a blank page • How do you convert your ideas into code?
There's A Method To The Madness • Defining Methods In Python Classes. Year 3 at Hogwarts School of Codecraft and Algorithmancy • Defining Methods
Finding Your Way To The Right Value • Python's Mappings. Part 3 of the Data Structure Categories Series
Python's functools.partial() Lets You Pre-Fill A Function. An exploration of partial() and partialmethod() in functools
Most articles will be published in full on the free subscription. However, a lot of effort and time goes into crafting and preparing these articles. If you enjoy the content and find it useful, and if you're in a position to do so, you can become a paid subscriber. In addition to supporting this work, you'll get access to the full archive of articles and some paid-only articles. Thank you!