You may think the Python turtle
module isn’t useful for anything. “It’s just for kids”, is what many think of it. However, this module allows you to explore and experiment with many areas of Python. In this article, you’ll write a Python turtle
animation of a sunrise scene using named tuples:
As you work your way through this article, you’ll explore:
- How to use Python named tuples
- How to make gradual changes in colour and position in an animation
Don’t worry if you’ve never used Python’s turtle
module. I’ll explain what you need to write this Python turtle
animation.
I’ll assume you’re familiar with defining functions and Python’s built-in data structures. If you need to learn more about these topics or just refresh your memory, you can read Power-up Your Coding: Create Your Own Functions and Data, Data Types and Data Structures in The Python Coding Book.
Planning The Sunrise Python Turtle Animation
Let’s look at the steps needed to write this Python turtle
animation:
- You’ll need to write functions to draw the stars, the flower, and the sun. The colours will need to change. And in the case of the sun, its position will need to change, too
- You’ll need to work out how much to change the colours of the items in each frame. You’ll also need to work out how much to move the sun in each frame
- Finally, you’ll need to write the loop to run the animation
You’ll deal with lots of colours throughout the animation. You need to define the start and end colours for each item in your drawing. Each colour is made up of three components: red, green, and a blue.
This means you’ll start with six values for each item in your drawing: three to represent the starting colour and three to represent the final colour. You also need to change each item’s red, green, and blue components in each frame of the animation.
In the next section, you’ll look at what data structures you can use to deal with these requirements.
Using Python Named Tuples
Let’s start by looking at colours. You often see colours represented by three values showing how much red, green, and blue the colour is made up of. This is the RGB colour model. You’ll usually see these as numbers in the range from 0
to 255
as this represents 8
-bit colour. If each component is represented by 8
bits, then there are 2^8 values, which is 256
.
Therefore, white is represented by (255, 255, 255)
as it consists of the maximum amount of red, green, and blue. Black is represented by (0, 0, 0)
. Other combinations of RGB values represent other colours.
You could represent a colour in Python by storing the three RGB values in a list. However, you’ll often see colours defined as tuples instead. Tuples are more suited for this as they’re immutable. A colour will always have three values, and the RGB values will not change for that colour:
>>> sky_colour = 0, 191, 255 >>> type(sky_colour) <class 'tuple'>
The variable sky_colour
is a tuple containing RGB values. This represents a light blue colour which you’ll use as the sky colour in the sunrise animation.
You’ll need to store two colours for each part of your drawing:
- The first colour corresponds to the start of the animation
- The second colour corresponds to the end of the animation
You can create another tuple for this:
>>> sky_colour = ( ... (0, 0, 0), ... (0, 191, 255) ... ) >>> type(sky_colour) <class 'tuple'>>>> sky_colour[0] # Start colour (0, 0, 0) >>> sky_colour[1][2] # Blue value of end colour 255
The variable sky_colour
is a 2
-tuple. That’s a tuple that has two items. Each one of those items is a 3
-tuple with the RGB values.
You can access the colour at the start of the animation using indexing: sky_colour[0]
. You can also go deeper into the nesting, for example sky_colour[1][2]
is the blue value of the colour at the end of the animation.
This is perfectly fine. However, using indexes can be confusing and lead to errors in the code since you’ll need to keep track of what each index represents.
Named Tuples
Instead, you can use named tuples. You can find Python’s named tuples in the collections
module. Let’s see how you can create a named tuple to represent colour:
>>> from collections import namedtuple >>> Colour = namedtuple("Colour", "red, green, blue") >>> issubclass(Colour, tuple) True >>> Colour._fields ('red', 'green', 'blue')
You create a class called Colour
, which is a subclass of tuple
. Colour
has three fields which you set in the second argument when you call namedtuple()
.
You can now define sky_colour
as you did earlier. You’ll start by defining only the end colour, for now. However, this time, you can use the named tuple class Colour
rather than a standard tuple:
>>> sky_colour = Colour(0, 191, 255) >>> sky_colour[1] 191 >>> sky_colour.green 191
sky_colour
can still be accessed using indexes just as a standard tuple. But you can also access items using the field name, such as sky_colour.green
. This can make your code more readable.
You can create another named tuple to define the start and end points of a range:
>>> RangeLimits = namedtuple("RangeLimits", "start, end") >>> sky_colour = RangeLimits( ... Colour(0, 0, 0), ... Colour(0, 191, 255), ... ) >>> sky_colour[1][2] 255 >>> sky_colour.end.blue 255
This time, you define sky_colour
as a RangeLimits
named tuple. This named tuple contains two Colour
named tuples.
Now, you can access the end colour’s blue value either by using sky_colour[1][2]
or sky_colour.end.blue
. Accessing values using field names is more readable in this case and less likely to lead to errors.
You may be thinking that a named tuple is similar to a dictionary. However, a named tuple is immutable, just like standard tuples. However, dictionaries are mutable.
You’ll read more about the differences between named tuples and dictionaries later in this article.
To learn more about named tuples, you can read the Real Python article Write Pythonic and Clean Code With namedtuple.
Setting Up The Sunrise Python Turtle Animation
You can start setting up the turtle
animation by creating a window. You can create a script called sunrise.py
:
# sunrise.py import turtle width = 1200 height = 800 # Set up the window for the animation sky = turtle.Screen() sky.setup(width, height) sky.tracer(0) sky.title("Good morning…") turtle.done()
The variable sky
represents the screen object with the width and height you choose. You can also add a title to the window.
You also call sky.tracer(0)
, allowing you to control when things are displayed in your animation. When you set the tracer to 0
, you’ll need to call sky.update()
to update the drawing on the window. There’s nothing being drawn at the moment, so there’s no call to sky.update()
yet.
When you run this code, you’ll see a blank window displayed. The turtle.done()
call keeps the program from terminating.
Note: If you’re using a web-based editor to code in Python, you may find that sky.setup()
and sky.title()
raise errors. These methods are not present in the turtle
version used by most online editors.
Adding The Named Tuple Classes
In the earlier section introducing named tuples, you created the Colour
and the RangeLimits
named tuple classes. You can use these in the animation code:
# sunrise.py import turtle from collections import namedtuple width = 1200 height = 800 # Create Named Tuple Classes RangeLimits = namedtuple("RangeLimits", "start, end") Colour = namedtuple("Colour", "red, green, blue") sky_colour = RangeLimits( Colour(0 / 255, 0 / 255, 0 / 255), Colour(0 / 255, 191 / 255, 255 / 255), ) # Set up the window for the animation sky = turtle.Screen() sky.setup(width, height) sky.tracer(0) sky.title("Good morning…") sky.bgcolor(sky_colour.start) turtle.done()
There is one difference in this script. The colour values are divided by 255
. The default colour mode in the turtle
module represents colours as floats in the range from 0
to 1
rather than integers in the range 0
to 255
.
You also call sky.bgcolor()
which changes the background colour. The output is now a window with a black background since sky_colour.start
is black. This is the first colour in the sky_colour
named tuple.
Note: If you’re using a web-based editor to code in Python, you should add sky.colormode(1)
to set this as the default mode.
Calculating The Colour Change Needed In Each Animation Frame
To create an animation, you’ll need a loop. Each iteration of the loop represents a frame. You could control your animation based on time. However, I’ll choose a simpler route for this article and set the length of the iteration based on the number of frames rather than time.
You can create a variable called number_of_steps
to determine how many frames you want your animation to have. You can also define a function to calculate how much you’ll need to change a colour in each animation frame:
# sunrise.py import turtle from collections import namedtuple number_of_steps = 500 width = 1200 height = 800 # Create Named Tuple Classes RangeLimits = namedtuple("RangeLimits", "start, end") Colour = namedtuple("Colour", "red, green, blue") sky_colour = RangeLimits( Colour(0 / 255, 0 / 255, 0 / 255), Colour(0 / 255, 191 / 255, 255 / 255), ) def calculate_colour_change( start: Colour, end: Colour, n_steps: int, ): red_step = (end.red - start.red) / n_steps green_step = (end.green - start.green) / n_steps blue_step = (end.blue - start.blue) / n_steps return Colour(red_step, green_step, blue_step) # Set up the window for the animation sky = turtle.Screen() sky.setup(width, height) sky.tracer(0) sky.title("Good morning…") sky.bgcolor(sky_colour.start) # Calculate step sizes needed for colour changes sky_colour_steps = calculate_colour_change( sky_colour.start, sky_colour.end, number_of_steps ) turtle.done()
The function calculate_colour_change()
has three parameters:
start
is the colour at the start of the animationend
is the colour at the end of the animationn_steps
is the number of steps in the animation
You use type hinting to make the code more readable and make it clear that start
and stop
are objects of the class Colour
. This is the named tuple class you created.
You need to work out the step sizes required for the red, green, and blue components separately to determine how to change the colours one frame at a time.
The function calculate_colour_change()
returns an object of type Colour
. Technically, the return value is not a colour but the steps needed for the red, green, and blue components to make a change to the colour. However, the Colour
class works perfectly fine for this, too.
You call this function to work out the steps needed to change the sky colour from black to sky blue in 500
frames.
Adding The Animation Loop
Now, you can add the animation loop. Since you’re determining the animation’s length by choosing the number of frames, you can use a for
loop as the main loop:
# sunrise.py import turtle from collections import namedtuple number_of_steps = 500 width = 1200 height = 800 # Create Named Tuple Classes RangeLimits = namedtuple("RangeLimits", "start, end") Colour = namedtuple("Colour", "red, green, blue") sky_colour = RangeLimits( Colour(0 / 255, 0 / 255, 0 / 255), Colour(0 / 255, 191 / 255, 255 / 255), ) def calculate_colour_change( start: Colour, end: Colour, n_steps: int, ): red_step = (end.red - start.red) / n_steps green_step = (end.green - start.green) / n_steps blue_step = (end.blue - start.blue) / n_steps return Colour(red_step, green_step, blue_step) # Set up the window for the animation sky = turtle.Screen() sky.setup(width, height) sky.tracer(0) sky.title("Good morning…") sky.bgcolor(sky_colour.start) # Calculate step sizes needed for colour changes sky_colour_steps = calculate_colour_change( sky_colour.start, sky_colour.end, number_of_steps ) current_sky_colour = sky_colour.start for _ in range(number_of_steps): current_sky_colour = Colour( current_sky_colour.red + sky_colour_steps.red, current_sky_colour.green + sky_colour_steps.green, current_sky_colour.blue + sky_colour_steps.blue, ) # Change the background to use the new colour sky.bgcolor(current_sky_colour) sky.update() turtle.done()
You create a variable called current_sky_colour
which is initially equal to the start colour. In the for
loop, you modify this colour’s red, green, and blue components by adding the steps you calculated. Finally, you update the screen’s background colour.
You also call sky.update()
since you want to update the display once per frame.
This code creates the following animation:
The sky colour changes gradually from black to sky blue. You can control the speed of the animation by changing the value of number_of_steps
. The more steps you use, the slower the sky’s change of colour will be.
Updating A Named Tuple?!
You may recall that you used a named tuple to represent the start and end colours because these do not change. It made sense to use an immutable data type.
However, current_sky_colour
needs to be updated every frame. Since a Colour
object is a named tuple, and therefore immutable, you have to overwrite the named tuple each time.
For the time being, I’ll keep using the named tuple class Colour
for current_sky_colour
and for similar variables linked to other parts of the drawings, which you’ll add soon.
You’ll revisit this point towards the end of this article. We’ll discuss whether this is the best approach for the colours that are changing in every frame.
Adding A Flower To The Scene
The hard work is done. Now, if you want to add more items to the scene, you can follow the same pattern as you’ve done with the background colour.
You can start by adding a flower with a stem. There are three components of the flower which have different colours:
- The petals
- The centre of the flower. This is called the pistil, but I’ll just refer to it as the centre of the flower!
- The stem
You can add colours for all of these and work out the steps needed in each frame. Next, you create Turtle
objects for the flower and the stem and use Turtle
methods to hide the “pen” that’s doing the drawing, turn it, and change the size of the lines it draws:
# sunrise.py import turtle from collections import namedtuple number_of_steps = 500 width = 1200 height = 800 # Create Named Tuple Classes RangeLimits = namedtuple("RangeLimits", "start, end") Colour = namedtuple("Colour", "red, green, blue") sky_colour = RangeLimits( Colour(0 / 255, 0 / 255, 0 / 255), Colour(0 / 255, 191 / 255, 255 / 255), ) petal_colour = RangeLimits( Colour(50 / 255, 50 / 255, 50 / 255), Colour(138 / 255, 43 / 255, 226 / 255), ) flower_centre_colour = RangeLimits( Colour(30 / 255, 30 / 255, 30 / 255), Colour(255 / 255, 165 / 255, 0 / 255), ) stem_colour = RangeLimits( Colour(15 / 255, 15 / 255, 15 / 255), Colour(34 / 255, 139 / 255, 34 / 255), ) def calculate_colour_change( start: Colour, end: Colour, n_steps: int, ): red_step = (end.red - start.red) / n_steps green_step = (end.green - start.green) / n_steps blue_step = (end.blue - start.blue) / n_steps return Colour(red_step, green_step, blue_step) # Set up the window for the animation # Sky sky = turtle.Screen() sky.setup(width, height) sky.tracer(0) sky.title("Good morning…") sky.bgcolor(sky_colour.start) # Flower and Stem flower = turtle.Turtle() flower.hideturtle() stem = turtle.Turtle() stem.hideturtle() stem.right(90) stem.pensize(10) def draw_flower(petal_col, flower_centre_col, stem_col): stem.clear() stem.color(stem_col) stem.forward(height / 2) stem.forward(-height / 2) flower.clear() flower.color(petal_col) # Draw petals for _ in range(6): flower.forward(100) flower.dot(75) flower.forward(-100) flower.left(360 / 6) # Draw centre of flower flower.color(flower_centre_col) flower.dot(175) # Draw the initial flower using the starting colours draw_flower( petal_colour.start, flower_centre_colour.start, stem_colour.start, ) #### # Calculate step sizes needed for colour changes sky_colour_steps = calculate_colour_change( sky_colour.start, sky_colour.end, number_of_steps ) petal_colour_steps = calculate_colour_change( petal_colour.start, petal_colour.end, number_of_steps ) flower_centre_colour_steps = calculate_colour_change( flower_centre_colour.start, flower_centre_colour.end, number_of_steps, ) stem_colour_steps = calculate_colour_change( stem_colour.start, stem_colour.end, number_of_steps ) #### # Start animation current_sky_colour = sky_colour.start current_petal_colour = petal_colour.start current_flower_centre_colour = flower_centre_colour.start current_stem_colour = stem_colour.start for _ in range(number_of_steps): # Sky current_sky_colour = Colour( current_sky_colour.red + sky_colour_steps.red, current_sky_colour.green + sky_colour_steps.green, current_sky_colour.blue + sky_colour_steps.blue, ) # Change the background to use the new colour sky.bgcolor(current_sky_colour) # Flower and Stem current_petal_colour = Colour( current_petal_colour.red + petal_colour_steps.red, current_petal_colour.green + petal_colour_steps.green, current_petal_colour.blue + petal_colour_steps.blue, ) current_flower_centre_colour = Colour( current_flower_centre_colour.red + flower_centre_colour_steps.red, current_flower_centre_colour.green + flower_centre_colour_steps.green, current_flower_centre_colour.blue + flower_centre_colour_steps.blue, ) current_stem_colour = Colour( current_stem_colour.red + stem_colour_steps.red, current_stem_colour.green + stem_colour_steps.green, current_stem_colour.blue + stem_colour_steps.blue, ) # Draw the flower again with the new colours draw_flower( current_petal_colour, current_flower_centre_colour, current_stem_colour, ) sky.update() turtle.done()
You define draw_flower()
which draws the stem and flower by turning and moving the turtles and changing their colours. The function also clears the drawing from the previous frame when you call stem.clear()
and flower.clear()
.
You calculate the steps needed to change the colours in every frame and set the initial colours like you did for the sky. You change the current colours in the animation loop and re-draw the flower in every frame.
The animation now looks like this:
Adding Stars To The Scene
Next, you can add stars in random positions on the screen. Since you’ll need to re-draw the stars in every frame, you can generate random star positions and sizes and store them so that you can use the same values each time you need to draw the stars. This is preferable to creating a new turtle for each star which can slow down the animation.
Dealing with the stars’ colour change follows the same pattern as with the sky and flower:
# sunrise.py import random import turtle from collections import namedtuple number_of_steps = 500 number_of_stars = 200 width = 1200 height = 800 # Create Named Tuple Classes RangeLimits = namedtuple("RangeLimits", "start, end") Colour = namedtuple("Colour", "red, green, blue") sky_colour = RangeLimits( Colour(0 / 255, 0 / 255, 0 / 255), Colour(0 / 255, 191 / 255, 255 / 255), ) petal_colour = RangeLimits( Colour(50 / 255, 50 / 255, 50 / 255), Colour(138 / 255, 43 / 255, 226 / 255), ) flower_centre_colour = RangeLimits( Colour(30 / 255, 30 / 255, 30 / 255), Colour(255 / 255, 165 / 255, 0 / 255), ) stem_colour = RangeLimits( Colour(15 / 255, 15 / 255, 15 / 255), Colour(34 / 255, 139 / 255, 34 / 255), ) star_colour = RangeLimits( Colour(1, 1, 1), sky_colour.end, ) def calculate_colour_change( start: Colour, end: Colour, n_steps: int, ): red_step = (end.red - start.red) / n_steps green_step = (end.green - start.green) / n_steps blue_step = (end.blue - start.blue) / n_steps return Colour(red_step, green_step, blue_step) # Set up the window for the animation # Sky sky = turtle.Screen() sky.setup(width, height) sky.tracer(0) sky.title("Good morning…") sky.bgcolor(sky_colour.start) # Flower and Stem flower = turtle.Turtle() flower.hideturtle() stem = turtle.Turtle() stem.hideturtle() stem.right(90) stem.pensize(10) def draw_flower(petal_col, flower_centre_col, stem_col): stem.clear() stem.color(stem_col) stem.forward(height / 2) stem.forward(-height / 2) flower.clear() flower.color(petal_col) # Draw petals for _ in range(6): flower.forward(100) flower.dot(75) flower.forward(-100) flower.left(360 / 6) # Draw centre of flower flower.color(flower_centre_col) flower.dot(175) # Draw the initial flower using the starting colours draw_flower( petal_colour.start, flower_centre_colour.start, stem_colour.start, ) # Stars stars = turtle.Turtle() stars.hideturtle() stars.penup() # Generate pairs of coordinates for the star positions star_positions = tuple( ( random.randint(-width // 2, width // 2), random.randint(-width // 2, width // 2), ) for _ in range(number_of_stars) ) # …and size for the stars star_sizes = tuple( random.randint(2, 8) for _ in range(number_of_stars) ) def draw_stars(colour): stars.clear() stars.color(colour) for position, size in zip(star_positions, star_sizes): stars.setposition(position) stars.dot(size) # Draw the initial stars using the starting colour draw_stars(star_colour.start) #### # Calculate step sizes needed for colour changes sky_colour_steps = calculate_colour_change( sky_colour.start, sky_colour.end, number_of_steps ) petal_colour_steps = calculate_colour_change( petal_colour.start, petal_colour.end, number_of_steps ) flower_centre_colour_steps = calculate_colour_change( flower_centre_colour.start, flower_centre_colour.end, number_of_steps, ) stem_colour_steps = calculate_colour_change( stem_colour.start, stem_colour.end, number_of_steps ) star_colour_steps = calculate_colour_change( star_colour.start, star_colour.end, number_of_steps ) #### # Start animation current_sky_colour = sky_colour.start current_petal_colour = petal_colour.start current_flower_centre_colour = flower_centre_colour.start current_stem_colour = stem_colour.start current_star_colour = star_colour.start for _ in range(number_of_steps): # Sky current_sky_colour = Colour( current_sky_colour.red + sky_colour_steps.red, current_sky_colour.green + sky_colour_steps.green, current_sky_colour.blue + sky_colour_steps.blue, ) # Change the background to use the new colour sky.bgcolor(current_sky_colour) # Stars current_star_colour = Colour( current_star_colour.red + star_colour_steps.red, current_star_colour.green + star_colour_steps.green, current_star_colour.blue + star_colour_steps.blue, ) draw_stars(current_star_colour) # Flower and Stem current_petal_colour = Colour( current_petal_colour.red + petal_colour_steps.red, current_petal_colour.green + petal_colour_steps.green, current_petal_colour.blue + petal_colour_steps.blue, ) current_flower_centre_colour = Colour( current_flower_centre_colour.red + flower_centre_colour_steps.red, current_flower_centre_colour.green + flower_centre_colour_steps.green, current_flower_centre_colour.blue + flower_centre_colour_steps.blue, ) current_stem_colour = Colour( current_stem_colour.red + stem_colour_steps.red, current_stem_colour.green + stem_colour_steps.green, current_stem_colour.blue + stem_colour_steps.blue, ) # Draw the flower again with the new colours draw_flower( current_petal_colour, current_flower_centre_colour, current_stem_colour, ) sky.update() turtle.done()
The starting colour of the stars is white. However, you need to match the stars’ end colour to the sky’s end colour so that the stars ‘blend’ into the sky background when the sunrise is complete.
You use generator expressions to create the pairs of star coordinates and the star sizes and then convert these to tuples. If you’re familiar with list comprehensions, you may wonder why you couldn’t use the same syntax but replace the square brackets [ ]
with parentheses ( )
? Here’s the reason why:
>>> # List comprehension >>> numbers = [x ** 2 for x in range(10)] >>> type(numbers) <class 'list'>>>> numbers [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] >>> # But this is _not_ a tuple comprehension: >>> numbers = (x ** 2 for x in range(10)) >>> type(numbers) <class 'generator'>>>> numbers <generator object <genexpr> at 0x7fdf4f853ba0>>>> # You need to use `tuple(...)`: >>> numbers = tuple(x ** 2 for x in range(10)) >>> type(numbers) <class 'tuple'>>>> numbers (0, 1, 4, 9, 16, 25, 36, 49, 64, 81)
In draw_stars()
, you need to loop through both star_positions
and star_sizes
. Therefore, you use Python’s zip()
function to ‘zip’ these two tuples together and loop through them simultaneously.
In the animation loop, you draw the stars before you draw the flower to make sure the stars are ‘behind’ the flower in the drawing.
The animation now has stars, the flower, and the sky all changing their nighttime colours into daytime ones.
Adding The Sun To The Scene
When you add the sun, there’s a new challenge. Setting the sun’s colour transformation should not be too difficult as this follows the same pattern as everything else. However, you’ll also need the sun to rise in the sky.
The sun’s x-coordinate is constant throughout the animation. However, the y-coordinate changes. As with colour, you also have a start and end value for the y-coordinate. Therefore, you can use the RangeLimits
named tuple class for the y-coordinate range. The values within it are floats instead of Colour
objects.
You also need to define calculate_movement_change()
which performs a similar task to calculate_colour_change()
. Its input arguments are the start and end y-coordinates and the number of steps in the animation:
# sunrise.py import random import turtle from collections import namedtuple number_of_steps = 500 number_of_stars = 200 sun_size = 150 width = 1200 height = 800 # Create Named Tuple Classes RangeLimits = namedtuple("RangeLimits", "start, end") Colour = namedtuple("Colour", "red, green, blue") sky_colour = RangeLimits( Colour(0 / 255, 0 / 255, 0 / 255), Colour(0 / 255, 191 / 255, 255 / 255), ) petal_colour = RangeLimits( Colour(50 / 255, 50 / 255, 50 / 255), Colour(138 / 255, 43 / 255, 226 / 255), ) flower_centre_colour = RangeLimits( Colour(30 / 255, 30 / 255, 30 / 255), Colour(255 / 255, 165 / 255, 0 / 255), ) stem_colour = RangeLimits( Colour(15 / 255, 15 / 255, 15 / 255), Colour(34 / 255, 139 / 255, 34 / 255), ) star_colour = RangeLimits( Colour(1, 1, 1), sky_colour.end, ) sun_colour = RangeLimits( Colour(10 / 255, 10 / 255, 10 / 255), Colour(249 / 255, 215 / 255, 28 / 255), ) sun_x_coordinate = -width / 3 sun_y_position = RangeLimits( -height / 2 - sun_size / 2, height / 2 - height / 8, ) def calculate_colour_change( start: Colour, end: Colour, n_steps: int, ): red_step = (end.red - start.red) / n_steps green_step = (end.green - start.green) / n_steps blue_step = (end.blue - start.blue) / n_steps return Colour(red_step, green_step, blue_step) def calculate_movement_change(start, end, n_steps): return (end - start) / n_steps # Set up the window for the animation # Sky sky = turtle.Screen() sky.setup(width, height) sky.tracer(0) sky.title("Good morning…") sky.bgcolor(sky_colour.start) # Flower and Stem flower = turtle.Turtle() flower.hideturtle() stem = turtle.Turtle() stem.hideturtle() stem.right(90) stem.pensize(10) def draw_flower(petal_col, flower_centre_col, stem_col): stem.clear() stem.color(stem_col) stem.forward(height / 2) stem.forward(-height / 2) flower.clear() flower.color(petal_col) # Draw petals for _ in range(6): flower.forward(100) flower.dot(75) flower.forward(-100) flower.left(360 / 6) # Draw centre of flower flower.color(flower_centre_col) flower.dot(175) # Draw the initial flower using the starting colours draw_flower( petal_colour.start, flower_centre_colour.start, stem_colour.start, ) # Stars stars = turtle.Turtle() stars.hideturtle() stars.penup() # Generate pairs of coordinates for the star positions star_positions = tuple( ( random.randint(-width // 2, width // 2), random.randint(-width // 2, width // 2), ) for _ in range(number_of_stars) ) # …and size for the stars star_sizes = tuple( random.randint(2, 8) for _ in range(number_of_stars) ) def draw_stars(colour): stars.clear() stars.color(colour) for position, size in zip(star_positions, star_sizes): stars.setposition(position) stars.dot(size) # Draw the initial stars using the starting colour draw_stars(star_colour.start) # Sun sun = turtle.Turtle() sun.hideturtle() sun.setposition(sun_x_coordinate, sun_y_position.start) def draw_sun(sun_col, sun_height): sun.clear() sun.color(sun_col) sun.sety(sun_height) sun.dot(sun_size) draw_sun(sun_colour.start, sun_y_position.start) #### # Calculate step sizes needed for colour changes sky_colour_steps = calculate_colour_change( sky_colour.start, sky_colour.end, number_of_steps ) petal_colour_steps = calculate_colour_change( petal_colour.start, petal_colour.end, number_of_steps ) flower_centre_colour_steps = calculate_colour_change( flower_centre_colour.start, flower_centre_colour.end, number_of_steps, ) stem_colour_steps = calculate_colour_change( stem_colour.start, stem_colour.end, number_of_steps ) star_colour_steps = calculate_colour_change( star_colour.start, star_colour.end, number_of_steps ) sun_colour_steps = calculate_colour_change( sun_colour.start, sun_colour.end, number_of_steps ) sun_movement_steps = calculate_movement_change( sun_y_position.start, sun_y_position.end, number_of_steps ) #### # Start animation current_sky_colour = sky_colour.start current_petal_colour = petal_colour.start current_flower_centre_colour = flower_centre_colour.start current_stem_colour = stem_colour.start current_star_colour = star_colour.start current_sun_colour = sun_colour.start current_sun_y_position = sun_y_position.start for _ in range(number_of_steps): # Sky current_sky_colour = Colour( current_sky_colour.red + sky_colour_steps.red, current_sky_colour.green + sky_colour_steps.green, current_sky_colour.blue + sky_colour_steps.blue, ) # Change the background to use the new colour sky.bgcolor(current_sky_colour) # Stars current_star_colour = Colour( current_star_colour.red + star_colour_steps.red, current_star_colour.green + star_colour_steps.green, current_star_colour.blue + star_colour_steps.blue, ) draw_stars(current_star_colour) # Flower and Stem current_petal_colour = Colour( current_petal_colour.red + petal_colour_steps.red, current_petal_colour.green + petal_colour_steps.green, current_petal_colour.blue + petal_colour_steps.blue, ) current_flower_centre_colour = Colour( current_flower_centre_colour.red + flower_centre_colour_steps.red, current_flower_centre_colour.green + flower_centre_colour_steps.green, current_flower_centre_colour.blue + flower_centre_colour_steps.blue, ) current_stem_colour = Colour( current_stem_colour.red + stem_colour_steps.red, current_stem_colour.green + stem_colour_steps.green, current_stem_colour.blue + stem_colour_steps.blue, ) # Draw the flower again with the new colours draw_flower( current_petal_colour, current_flower_centre_colour, current_stem_colour, ) # Sun current_sun_colour = Colour( current_sun_colour.red + sun_colour_steps.red, current_sun_colour.green + sun_colour_steps.green, current_sun_colour.blue + sun_colour_steps.blue, ) current_sun_y_position += sun_movement_steps draw_sun(current_sun_colour, current_sun_y_position) sky.update() turtle.done()
The function draw_sun()
needs the sun’s colour and y-position. You use Turtle
‘s setposition()
initially to set both the x- and y-positions of the sun. However, in draw_sun()
you can use sety()
since the x-coordinate no longer changes.
Incrementing current_sun_y_position
in the animation loop is simpler than with the colours since the value is a single float rather than a named tuple.
The animation is now complete:
Should You Use Named Tuples In The Animation Loop?
Earlier in the article, we discussed how tuples are ideal for colours and other values that do not change. However, in the current version of the code, you’re using named tuples to store the colours during the animation loop, too. These are the colours named current_<...>_colour
.
Because tuples are immutable objects, you have to create new Colour
named tuples in the for
loop and reassign them to the same variable names. Tuples are not ideal for this.
Instead, you can convert the named tuples to dictionaries before the for
loop. Dictionaries are mutable types and more suited for values that need to change frequently.
You can refactor the code using named tuple’s _asdict()
method which converts the named tuple to a dictionary:
# sunrise.py import random import turtle from collections import namedtuple number_of_steps = 500 number_of_stars = 200 sun_size = 150 width = 1200 height = 800 # Create Named Tuple Classes RangeLimits = namedtuple("RangeLimits", "start, end") Colour = namedtuple("Colour", "red, green, blue") sky_colour = RangeLimits( Colour(0 / 255, 0 / 255, 0 / 255), Colour(0 / 255, 191 / 255, 255 / 255), ) petal_colour = RangeLimits( Colour(50 / 255, 50 / 255, 50 / 255), Colour(138 / 255, 43 / 255, 226 / 255), ) flower_centre_colour = RangeLimits( Colour(30 / 255, 30 / 255, 30 / 255), Colour(255 / 255, 165 / 255, 0 / 255), ) stem_colour = RangeLimits( Colour(15 / 255, 15 / 255, 15 / 255), Colour(34 / 255, 139 / 255, 34 / 255), ) star_colour = RangeLimits( Colour(1, 1, 1), sky_colour.end, ) sun_colour = RangeLimits( Colour(10 / 255, 10 / 255, 10 / 255), Colour(249 / 255, 215 / 255, 28 / 255), ) sun_x_coordinate = -width / 3 sun_y_position = RangeLimits( -height / 2 - sun_size / 2, height / 2 - height / 8, ) def calculate_colour_change( start: Colour, end: Colour, n_steps: int, ): red_step = (end.red - start.red) / n_steps green_step = (end.green - start.green) / n_steps blue_step = (end.blue - start.blue) / n_steps return Colour(red_step, green_step, blue_step) def calculate_movement_change(start, end, n_steps): return (end - start) / n_steps # Set up the window for the animation # Sky sky = turtle.Screen() sky.setup(width, height) sky.tracer(0) sky.title("Good morning…") sky.bgcolor(sky_colour.start) # Flower and Stem flower = turtle.Turtle() flower.hideturtle() stem = turtle.Turtle() stem.hideturtle() stem.right(90) stem.pensize(10) def draw_flower(petal_col, flower_centre_col, stem_col): stem.clear() stem.color(stem_col) stem.forward(height / 2) stem.forward(-height / 2) flower.clear() flower.color(petal_col) # Draw petals for _ in range(6): flower.forward(100) flower.dot(75) flower.forward(-100) flower.left(360 / 6) # Draw centre of flower flower.color(flower_centre_col) flower.dot(175) # Draw the initial flower using the starting colours draw_flower( petal_colour.start, flower_centre_colour.start, stem_colour.start, ) # Stars stars = turtle.Turtle() stars.hideturtle() stars.penup() # Generate pairs of coordinates for the star positions star_positions = tuple( ( random.randint(-width // 2, width // 2), random.randint(-width // 2, width // 2), ) for _ in range(number_of_stars) ) # …and size for the stars star_sizes = tuple( random.randint(2, 8) for _ in range(number_of_stars) ) def draw_stars(colour): stars.clear() stars.color(colour) for position, size in zip(star_positions, star_sizes): stars.setposition(position) stars.dot(size) # Draw the initial stars using the starting colour draw_stars(star_colour.start) # Sun sun = turtle.Turtle() sun.hideturtle() sun.setposition(sun_x_coordinate, sun_y_position.start) def draw_sun(sun_col, sun_height): sun.clear() sun.color(sun_col) sun.sety(sun_height) sun.dot(sun_size) draw_sun(sun_colour.start, sun_y_position.start) #### # Calculate step sizes needed for colour changes sky_colour_steps = calculate_colour_change( sky_colour.start, sky_colour.end, number_of_steps ) petal_colour_steps = calculate_colour_change( petal_colour.start, petal_colour.end, number_of_steps ) flower_centre_colour_steps = calculate_colour_change( flower_centre_colour.start, flower_centre_colour.end, number_of_steps, ) stem_colour_steps = calculate_colour_change( stem_colour.start, stem_colour.end, number_of_steps ) star_colour_steps = calculate_colour_change( star_colour.start, star_colour.end, number_of_steps ) sun_colour_steps = calculate_colour_change( sun_colour.start, sun_colour.end, number_of_steps ) sun_movement_steps = calculate_movement_change( sun_y_position.start, sun_y_position.end, number_of_steps ) #### # Start animation current_sky_colour = sky_colour.start._asdict() current_petal_colour = petal_colour.start._asdict() current_flower_centre_colour = flower_centre_colour.start._asdict() current_stem_colour = stem_colour.start._asdict() current_star_colour = star_colour.start._asdict() current_sun_colour = sun_colour.start._asdict() current_sun_y_position = sun_y_position.start for _ in range(number_of_steps): # Sky current_sky_colour["red"] += sky_colour_steps.red current_sky_colour["green"] += sky_colour_steps.green current_sky_colour["blue"] += sky_colour_steps.blue # Change the background to use the new colour sky.bgcolor(current_sky_colour.values()) # Stars current_star_colour["red"] += star_colour_steps.red current_star_colour["green"] += star_colour_steps.green current_star_colour["blue"] + star_colour_steps.blue draw_stars(current_star_colour.values()) # Flower and Stem current_petal_colour["red"] += petal_colour_steps.red current_petal_colour["green"] += petal_colour_steps.green current_petal_colour["blue"] += petal_colour_steps.blue current_flower_centre_colour["red"] += flower_centre_colour_steps.red current_flower_centre_colour["green"] += flower_centre_colour_steps.green current_flower_centre_colour["blue"] += flower_centre_colour_steps.blue current_stem_colour["red"] += stem_colour_steps.red current_stem_colour["green"] += stem_colour_steps.green current_stem_colour["blue"] += stem_colour_steps.blue # Draw the flower again with the new colours draw_flower( current_petal_colour.values(), current_flower_centre_colour.values(), current_stem_colour.values(), ) # Sun current_sun_colour["red"] += sun_colour_steps.red current_sun_colour["green"] += sun_colour_steps.green current_sun_colour["blue"] += sun_colour_steps.blue current_sun_y_position += sun_movement_steps draw_sun(current_sun_colour.values(), current_sun_y_position) sky.update() turtle.done()
There are changes in the for
loop, too. You’re no longer creating new Colour
named tuples. Instead, you’re changing the colour values within the dictionaries using the increment operator +=
.
Then, you pass the values of the dictionary as arguments for sky.bgcolor()
, draw_stars()
, draw_flower()
, and draw_sun()
. You can use dictionary’s value()
method to create an iterable that can be used in all those functions.
You won’t be able to notice any changes between the animation from this version and the one you completed earlier which didn’t use dictionaries.
So, Why Bother?
If the animation looks the same, why bother making this change?
In this project, it doesn’t matter. However, this is a good example to get us to think about which data types to use. When using named tuples in the for
loop, the program must create several new named tuples in every frame. Creating new objects takes time.
But updating a dictionary is efficient. In the refactored version, you’re not creating new objects in the for
loop but updating existing ones.
You can compare the two versions by timing them. However, most of the time is dedicated to displaying graphics on the screen in an animation such as this one.
You can compare the efficiency of the two versions by stripping out the drawing in the animation and just comparing the code that updates the colours.
Let’s use the timeit
module to time the for
loops in both versions of the code. Start with timing the original version which uses named tuples throughout, including in the animation for
loop:
# sunrise_performance_version1.py import timeit setup_first_version = """ import random from collections import namedtuple number_of_steps = 500 number_of_stars = 200 sun_size = 150 width = 1200 height = 800 # Create Named Tuple Classes RangeLimits = namedtuple("RangeLimits", "start, end") Colour = namedtuple("Colour", "red, green, blue") sky_colour = RangeLimits( Colour(0 / 255, 0 / 255, 0 / 255), Colour(0 / 255, 191 / 255, 255 / 255), ) petal_colour = RangeLimits( Colour(50 / 255, 50 / 255, 50 / 255), Colour(138 / 255, 43 / 255, 226 / 255), ) flower_centre_colour = RangeLimits( Colour(30 / 255, 30 / 255, 30 / 255), Colour(255 / 255, 165 / 255, 0 / 255), ) stem_colour = RangeLimits( Colour(15 / 255, 15 / 255, 15 / 255), Colour(34 / 255, 139 / 255, 34 / 255), ) star_colour = RangeLimits( Colour(1, 1, 1), sky_colour.end, ) sun_colour = RangeLimits( Colour(10 / 255, 10 / 255, 10 / 255), Colour(249 / 255, 215 / 255, 28 / 255), ) sun_x_coordinate = -width / 3 sun_y_position = RangeLimits( -height / 2 - sun_size / 2, height / 2 - height / 8, ) def calculate_colour_change( start: Colour, end: Colour, n_steps: int, ): red_step = (end.red - start.red) / n_steps green_step = (end.green - start.green) / n_steps blue_step = (end.blue - start.blue) / n_steps return Colour(red_step, green_step, blue_step) def calculate_movement_change(start, end, n_steps): return (end - start) / n_steps # Generate pairs of coordinates for the star positions star_positions = tuple( ( random.randint(-width // 2, width // 2), random.randint(-width // 2, width // 2), ) for _ in range(number_of_stars) ) # …and size for the stars star_sizes = tuple( random.randint(2, 8) for _ in range(number_of_stars) ) # Calculate step sizes needed for colour changes sky_colour_steps = calculate_colour_change( sky_colour.start, sky_colour.end, number_of_steps ) petal_colour_steps = calculate_colour_change( petal_colour.start, petal_colour.end, number_of_steps ) flower_centre_colour_steps = calculate_colour_change( flower_centre_colour.start, flower_centre_colour.end, number_of_steps, ) stem_colour_steps = calculate_colour_change( stem_colour.start, stem_colour.end, number_of_steps ) star_colour_steps = calculate_colour_change( star_colour.start, star_colour.end, number_of_steps ) sun_colour_steps = calculate_colour_change( sun_colour.start, sun_colour.end, number_of_steps ) sun_movement_steps = calculate_movement_change( sun_y_position.start, sun_y_position.end, number_of_steps ) #### # Start animation current_sky_colour = sky_colour.start current_petal_colour = petal_colour.start current_flower_centre_colour = flower_centre_colour.start current_stem_colour = stem_colour.start current_star_colour = star_colour.start current_sun_colour = sun_colour.start current_sun_y_position = sun_y_position.start """ animation_loop_first_version = """ for _ in range(number_of_steps): # Sky current_sky_colour = Colour( current_sky_colour.red + sky_colour_steps.red, current_sky_colour.green + sky_colour_steps.green, current_sky_colour.blue + sky_colour_steps.blue, ) # Stars current_star_colour = Colour( current_star_colour.red + star_colour_steps.red, current_star_colour.green + star_colour_steps.green, current_star_colour.blue + star_colour_steps.blue, ) # Flower and Stem current_petal_colour = Colour( current_petal_colour.red + petal_colour_steps.red, current_petal_colour.green + petal_colour_steps.green, current_petal_colour.blue + petal_colour_steps.blue, ) current_flower_centre_colour = Colour( current_flower_centre_colour.red + flower_centre_colour_steps.red, current_flower_centre_colour.green + flower_centre_colour_steps.green, current_flower_centre_colour.blue + flower_centre_colour_steps.blue, ) current_stem_colour = Colour( current_stem_colour.red + stem_colour_steps.red, current_stem_colour.green + stem_colour_steps.green, current_stem_colour.blue + stem_colour_steps.blue, ) # Sun current_sun_colour = Colour( current_sun_colour.red + sun_colour_steps.red, current_sun_colour.green + sun_colour_steps.green, current_sun_colour.blue + sun_colour_steps.blue, ) current_sun_y_position += sun_movement_steps """ print( timeit.timeit( animation_loop_first_version, setup=setup_first_version, number=1_000, ) )
Start from the bottom of this script. You’re running timeit.timeit()
with three arguments:
animation_loop_first_version
is the code you want to time. The code is passed intotimeit()
as a string. You define this variable as a triple-quoted string just above thetimeit()
call. This includes the code in the animationfor
loop, excluding those lines responsible for drawing on the screen.setup=setup_first_version
includes the code you want to run before you start timing the main code. This is another triple-quoted string which includes the code before the animationfor
loop, excluding those lines responsible for drawing on the screen.number=1_000
is the argument that sets how many timestimeit()
should run the code you want to time. Therefore, you’re timing1,000
runs of the animation without displaying it.
When you run this script, you get the following output:
1.631227905
The code took around 1.6
seconds to run 1,000
times on my setup.
You can create a similar script for the second version where dictionaries replaced named tuples in the for loop:
# sunrise_performance_version2.py import timeit setup_first_version = """ import random from collections import namedtuple number_of_steps = 500 number_of_stars = 200 sun_size = 150 width = 1200 height = 800 # Create Named Tuple Classes RangeLimits = namedtuple("RangeLimits", "start, end") Colour = namedtuple("Colour", "red, green, blue") sky_colour = RangeLimits( Colour(0 / 255, 0 / 255, 0 / 255), Colour(0 / 255, 191 / 255, 255 / 255), ) petal_colour = RangeLimits( Colour(50 / 255, 50 / 255, 50 / 255), Colour(138 / 255, 43 / 255, 226 / 255), ) flower_centre_colour = RangeLimits( Colour(30 / 255, 30 / 255, 30 / 255), Colour(255 / 255, 165 / 255, 0 / 255), ) stem_colour = RangeLimits( Colour(15 / 255, 15 / 255, 15 / 255), Colour(34 / 255, 139 / 255, 34 / 255), ) star_colour = RangeLimits( Colour(1, 1, 1), sky_colour.end, ) sun_colour = RangeLimits( Colour(10 / 255, 10 / 255, 10 / 255), Colour(249 / 255, 215 / 255, 28 / 255), ) sun_x_coordinate = -width / 3 sun_y_position = RangeLimits( -height / 2 - sun_size / 2, height / 2 - height / 8, ) def calculate_colour_change( start: Colour, end: Colour, n_steps: int, ): red_step = (end.red - start.red) / n_steps green_step = (end.green - start.green) / n_steps blue_step = (end.blue - start.blue) / n_steps return Colour(red_step, green_step, blue_step) def calculate_movement_change(start, end, n_steps): return (end - start) / n_steps # Generate pairs of coordinates for the star positions star_positions = tuple( ( random.randint(-width // 2, width // 2), random.randint(-width // 2, width // 2), ) for _ in range(number_of_stars) ) # …and size for the stars star_sizes = tuple( random.randint(2, 8) for _ in range(number_of_stars) ) # Calculate step sizes needed for colour changes sky_colour_steps = calculate_colour_change( sky_colour.start, sky_colour.end, number_of_steps ) petal_colour_steps = calculate_colour_change( petal_colour.start, petal_colour.end, number_of_steps ) flower_centre_colour_steps = calculate_colour_change( flower_centre_colour.start, flower_centre_colour.end, number_of_steps, ) stem_colour_steps = calculate_colour_change( stem_colour.start, stem_colour.end, number_of_steps ) star_colour_steps = calculate_colour_change( star_colour.start, star_colour.end, number_of_steps ) sun_colour_steps = calculate_colour_change( sun_colour.start, sun_colour.end, number_of_steps ) sun_movement_steps = calculate_movement_change( sun_y_position.start, sun_y_position.end, number_of_steps ) #### # Start animation current_sky_colour = sky_colour.start._asdict() current_petal_colour = petal_colour.start._asdict() current_flower_centre_colour = flower_centre_colour.start._asdict() current_stem_colour = stem_colour.start._asdict() current_star_colour = star_colour.start._asdict() current_sun_colour = sun_colour.start._asdict() current_sun_y_position = sun_y_position.start """ animation_loop_first_version = """ for _ in range(number_of_steps): # Sky current_sky_colour["red"] += sky_colour_steps.red current_sky_colour["green"] += sky_colour_steps.green current_sky_colour["blue"] += sky_colour_steps.blue # Stars current_star_colour["red"] += star_colour_steps.red current_star_colour["green"] += star_colour_steps.green current_star_colour["blue"] + star_colour_steps.blue # Flower and Stem current_petal_colour["red"] += petal_colour_steps.red current_petal_colour["green"] += petal_colour_steps.green current_petal_colour["blue"] += petal_colour_steps.blue current_flower_centre_colour["red"] += flower_centre_colour_steps.red current_flower_centre_colour["green"] += flower_centre_colour_steps.green current_flower_centre_colour["blue"] += flower_centre_colour_steps.blue current_stem_colour["red"] += stem_colour_steps.red current_stem_colour["green"] += stem_colour_steps.green current_stem_colour["blue"] += stem_colour_steps.blue # Sun current_sun_colour["red"] += sun_colour_steps.red current_sun_colour["green"] += sun_colour_steps.green current_sun_colour["blue"] += sun_colour_steps.blue current_sun_y_position += sun_movement_steps """ print( timeit.timeit( animation_loop_first_version, setup=setup_first_version, number=1_000, ) )
The output of this script is:
0.7887224199999999
The second version takes approximately half the time to run compared to the first. Creating new named tuples in each frame is time-consuming!
Notice that the part of the code that works out the changes of colour and sun position do not take up too much time in both versions. The timings you obtain from these scripts is for 1,000
runs of the animation without displaying it on screen.
However, when you run the full animation code once, the animation takes a few seconds to run. That’s because displaying graphics on the screen is the most time-consuming part of the code.
Still, in other programs you may write, performance may be an issue. So it’s always helpful to think about when you should use one data type over another one. There are many factors to consider when choosing the data structures for your program. Efficiency is an important one of them.
Final Words
In this article, you explored using Python named tuples to write a Python turtle
animation of a sunrise scene.
You worked out the changes in colour and position needed in each animation frame for each item in your drawing. The result is that the colours and positions change smoothly as the animation runs.
Named tuples are ideal for defining the colours at the start and end of the animation. However, they’re not the best choice for storing the changing colours within the animation loop. Mutable data types are better suited when you need to update data often.
Get the latest blog updates
No spam promise. You’ll get an email when a new blog post is published
The post Sunrise: A Python Turtle Animation Using Named Tuples appeared first on The Python Coding Book.