Discover more from The Python Coding Stack • by Stephen Gruppetta
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
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:
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
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
Turtleinstances representing the balls
Define 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,
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
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
Turtleinstance and appends it to the list. You index the list using the
-1index in the function to access the last ball added to the list
Changes the shape and colour of the object using the methods
Reduces the size of the shape drawn to half its default size using
Raises the "pen" so that when the
Turtleobject moves, it doesn't draw a line
Sets the ball's position to the position of
reference, which is the spinning arrow. You use
.setposition()to change the position of a
.position()to return the current position of a
Sets the orientation of the ball to match the orientation of
.setheading()changes the orientation of a
.heading()returns the current heading of a
In the main animation loop, you add a couple of blocks:
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 the
You 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:
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
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
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.
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
pool_of_balls is empty, a new ball is created.
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:
You define two new functions:
You bind these functions to the left and right arrow keys using
window.onkeypress(). You also need to call
window.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.
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 to
Falseinitially. You also show the status of the edges in the window's title bar
toggle_bouncing()to change the value of the
window.bounce_onflag and update the window's title bar. This function is bound to the space bar using
Falsein 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
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.
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.
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
The Python Coding Stack • by Stephen Gruppetta is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.
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 example
From 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!