Do Not Try This At Home
A bit of silliness for the holiday season • But please, don't code like this. Please • Plus some out-of-the-norm commentary • There's nothing ordinary about today's article
Warning: The content of this article includes potentially distressing subject matter related to the misuse of Python tools. No programmers were harmed in the preparation of this article.
Do you read The Python Coding Stack to improve your Python and to learn better ways of writing code? Then, perhaps, don't read this article. Just skip this one and go and start eating your Christmas chocolates or panettones or whatever. The code I'll show you in this article is atrocious, horrendous, unintelligible. It's interesting and instructive, but don't try this at home, as they say!
Everything about this article is non-conventional (by my standards). So, I'll start by showing you the final version of the code, which I usually never do. Make sure you're sitting down:
Clear?
I thought so. Let's start from a blank script and write this code bit by bit. Although this is not how you should write your code, it's instructive to understand what's happening.
But we'll get to this mini-project shortly. This week, I'm moving the Stop Stack section I usually have at the end and bringing it further up in the article. I told you this article is topsy-turvy. We'll get to the Python code after Stop Stack.
Stop Stack
#44
There are times when everything happens at once. This is one of those times for me. In early 2024, I'm shifting all my time and attention to "creating content". I don't like the phrase "content creator" as I believe it focuses on the wrong things. "Creator" focuses on the creation of the content by the author when the real focus is on the consumption by the audience. And the word "content" places the focus on the material itself when what really matters is the clear communication of the concepts to help the audience learn effectively.
Anyway. "Content creator" it is! I have two major projects launching over the coming weeks:
You may have heard me talk about one of these. You may already be a member! The Python Coding Place has been rolling out gradually over the past months.
But the biggest rollout is happening on the 15th of January when the Members' Area goes live. I've got an extensive curriculum of video courses, a members' forum, weekly videos, cohort courses, and regular workshops, including ones delivered by guest speakers.
I was silly enough to set a pre-launch offer of $95 for lifetime access, which is way too good an offer. But I promised to keep this offer until the launch date, so there's nothing much I can do now!
The second major project is one I've been keeping under wraps, but it's now only days away from a soft launch. Many of you may not know that much of my work over the years has focused on teaching Python coding to children and teenagers.
Coding for kids typically falls into one of two categories: it's either rigorous but boring, or it's fluffy and lightweight, focusing on gimmicks rather than real learning.
I set out to change this when I set up Codetoday in 2016 and developed a curriculum that's academically rigorous but also creative and engaging. We've delivered this curriculum successfully to thousands of students over the years.
I started teaching children in North London in 2016. Codetoday now teaches students from all over the UK. It's now time for me to go further… Stay tuned if you know any teenagers ready to take coding seriously.
Recently published articles on The Python Coding Stack:
The Key To The 'key' Parameter in Python A parameter named
key
is present in several Python functions, such assorted()
. Let's explore what it is and how to use it.What's All the Fuss About 'lambda' Functions in Python? Python's
lambda
functions are seemingly obscure, until they aren't. They're almost mystical, until unveiled. Let's shed some light to dispel the obscurity and lift the mystique.In Conversation: Pawel and Stephen Discuss Matplotlib's New-ish subplot_mosaic() In recent years, Matplotlib introduced a new function for plotting several plots in one figure. We had a chat about
subplot_mosaic()
In Conversation: Rodrigo and Stephen Discuss Analogies When Learning To Code A conversation between Rodrigo Girão Serrão and Stephen Gruppetta on analogies in programming
A Touch of Randomness Makes The Magic Sparkle • A Python
turtle
Animation Let's build this sparklyturtle
animation step by stepNumPy for Numpties Introducing an infrequent, loose series of articles on NumPy
Stats on the Stack
Age: 8 months, 1 week, and 1 day old
Number of articles: 44
Total subscribers: 1,646
On the Paid tier: 60
Each article is the result of years of experience and many hours of work. I hope you enjoy each one and find them useful. If you're in a position to do so, you can support this Substack further with a paid subscription. In addition to supporting this work, you'll get access to the full archive of articles. Alternatively, if you become a member of The Python Coding Place, you'll get access to all articles on The Stack as part of that membership.
The Turtle Screen Saver Animation
Here's the animation I'll create in this article:
But I won't write this animation in a conventional manner. The code in this article came about as a joke, as a dare at the end of a meeting with colleagues: "I bet you can't write this animation without using a while
loop?" So, of course, we started discussing how to achieve this feat.
And while we're there, let's also avoid traditional for
loops, just for fun!
Let's start by creating a screen 700 pixels wide and 700 pixels high. I'll also set the background colour to dark grey using the RGB value (0.2, 0.2, 0.2)
. There's nothing strange about the code so far:
There's also an n_turtles
variable with the number of turtles we'll have in the animation.
Creating A List of Turtle
Objects
Next, we need to create a list to store all 100 turtles. Usually, we'd create an empty list and then use a for
loop. In the for
loop, we can create the Turtle
object, set its initial values by calling several Turtle
methods, and append to the list. Here's what this code would look like:
Let's assume we only want to create the Turtle
objects and append them to the list:
In this case, we don't need to create the variable name cute_turtle
as we can create the Turtle
instance directly within the parentheses of .append()
. But I left it in to be consistent with the previous code snippet.
In Python, we often prefer to replace this pattern with a list comprehension:
However, we need to customise the Turtle
object by raising the pen, turning it left, changing its colour, and changing its shape. Can we fit all this into a single list comprehension?
The Two-List Comprehension Approach
If we already had the list of turtles cute_turtles
, we could use a tuple and rely on the fact that all the actions needed are methods. This means they can be used as expressions directly within the tuple:
There are two list comprehensions in this code snippet. You've already seen the first one earlier. The second list comprehension starts with a tuple. The tuple contains the return values from the four Turtle
methods used: .left()
, .color()
, .penup()
, and .shape()
. All of those methods return None
since their purpose is to change the object's state, not return a value. Therefore, the tuples created in the list comprehension contain four references to None
.
What's the point of that?
The purpose of the tuple is not to store the None
values. Instead, it's a way of combining the four method calls into one expression. This single tuple can then be used in a list comprehension.
I've mentioned this already, including in the title, but I'll say it again. This is not the way you should write code. The purpose of this article is not to show you best practices but to explore what's possible. You can think of this as an academic exercise.
The methods within the tuple in the list comprehension are called when the list is created. Therefore, the code calls the methods for each of the 100 Turtle
objects in the list since the list comprehension loops across the list cute_turtles
. The for
statement in the list comprehension uses enumerate()
, which allows us to loop through the items and their indices. The indices are assigned to the name idx
, and the Turtle
objects in the list are assigned to cute_turtle
.
The methods rotate each turtle so that they're all facing in different directions, change their colours using random RGB values, raise the pen so that the turtles don't draw lines when they move, and change their shapes to the "turtle" shape available in the turtle
module.
When you run the code above, you'll see all the turtles appear in the centre, facing in different directions:
You can't see the turtles clearly as they're all in the same spot. We'll make them move soon.
The One-List Comprehension Approach
But let's treat this as an extreme challenge. Can we do everything in a single list comprehension instead of two?
The problem is that we need to create a turtle and call all the methods for each turtle. We need to assign a variable name to the Turtle
object. Let's try the following version in which the Turtle
is created in the same list comprehension that calls the various methods. I also included calls to window.tracer(0)
and window.update()
to make the animation display all the changes instantaneously:
The for
statement in the list comprehension now loops through range(n_turtles)
. If you're using an IDE, you'll likely see lots of red underlining in this version of the code. And when you run the code, you get the following error:
File ... line 17
cute_turtle = turtle.Turtle(),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
The line that assigns the Turtle
object to a variable name is a statement. It doesn't return a value. Therefore, it can't be used as a tuple item. We can only use expressions as tuple items because expressions return a value.
However, since Python 3.8, we have an expression we can use to assign a value to a variable. This is the assignment expression, but most call it the walrus operator. Let's replace the assignment statement, which uses the =
operator, with the assignment expression, which uses the walrus operator :=
instead:
And this now works since the first item in the tuple is an expression which returns the Turtle
object. The tuple created now contains five items. The first is the reference to the Turtle
object, and the rest are all None
, which are the return values of the methods used. But when the methods are called, the variable name cute_turtle
has already been assigned to the Turtle
created on the first line of the tuple. Therefore, the state of each Turtle
is updated.
It's been a few paragraphs since my last warning, so here it is again. Don't do this! It makes the code unreadable, and there are neater options.
Creating An Animation Without The while
Loop
Now, it's time to work on the animation. The ideal solution is to create a while
loop to run the animation. This would look like this:
Note that this assumes that cute_turtles
is a standard list of turtles rather than the list of tuples we created earlier. The for
loop iterates through all the turtles in the list and moves them forward. The while
loop keeps repeating the for
loop forever.
But the dare was to avoid using while
loops. And I won't use classic for
loops either, just to make things harder!
We can move all the turtles using a list comprehension again:
But this only moves all the turtles once. We need to keep moving them forever. But while
loops and for
loops are not the only way to repeat an operation in Python. In fact, there are several other ways. I'll pick one of these, the built-in map()
function.
The map()
function needs two arguments: a function and an iterable. Here's an example using map()
for those unfamiliar with this built-in function. This code snippet runs in the REPL/Console:
The list [2, 4, 6, 8]
is the iterable, and the function is a lambda
function. You can read about lambda
functions in this recent article on The Python Coding Stack. Each value in the list is passed to the lambda
function, and the generator yields the value returned by the lambda
function. Since map()
returns a generator, we need to convert the generator into a list
to evaluate all the items.
Let's use this REPL example as the building block for our animation. I'll keep the list [2, 4, 6, 8]
as the iterable for now but replace the lambda
function:
This is starting to look weird. Let's see what's in the new lines of code:
The
map()
function returns a generator. Therefore, it's passed tolist()
so that all the elements in the generator are evaluated.The
map()
function has two arguments:A
lambda
functionThe list
[2, 4, 6, 8]
The
lambda
function has one parameterx
, which is unused, and a list comprehension as its return value.The list
cute_turtles
is a list of tuples, and each tuple contains five items. We only want to use the first item in each tuple, which is theTurtle
object. The rest are references toNone
, which we can discard. These references toNone
are collected and discarded through the*_
in thefor
statement.
The list comprehension returned by the lambda
function moves all the turtles by one pixel. However, the lambda
function is called four times, once for each element in [2, 4, 6, 8]
. The values in this list are not used. Therefore, you can have any value in this iterable.
When you run this code, each one of the 100 turtles moves by four pixels. This is hard to see, so let's replace the list [2, 4, 6, 8]
with a longer iterable:
I replaced the list of four numbers with range(30)
, which is also an iterable. It has 30
items. I also replaced the unused parameter name x
in the lambda
function with an underscore. Each turtle now moves 30 steps.
Note that the call to window.update()
comes after the whole map()
function is executed. Therefore, all the turtles will appear immediately in the final position. To fix this and show the turtles moving one step at a time, we'll need to update the screen after each lambda
function has been completed rather than after the map()
function.
Instead of returning just the list comprehension, the lambda
function can return a tuple with the list comprehension and a call to window.update()
:
The lambda
function's return value is now a tuple containing the list comprehension and the call to window.update()
.
Moving each turtle forward isn't the only thing we need the animation to do. We must also turn each turtle when it hits one of the four edges. The arithmetic is different for the top and bottom edges and the left and right ones. We can wrap all the expressions we need for making the turtles bounce off the edges in a tuple within the list comprehension:
The list comprehension now contains a tuple with three expressions:
The call to
.forward()
to move the turtles one step forward.A conditional expression (sometimes referred to as the ternary operator) to change the turtle's heading to 180 minus its current heading when the turtle hits either the left or right edge. The conditional expression needs an
else
clause, which is set toNone
.Another conditional expression to deal with the top and bottom edges.
I also replaced range(30)
with range(500)
to go through 500 iterations of the animation. This allows the turtles to bounce off the edges so we can test our code. When you run this code, you'll see the turtles move away from each other from the centre of the screen and then "bounce" off the edges of the screen.
You see now why I'm not recommending you code like this? This code is hard to read. This is just a bit of fun to explore weird coding patterns.
An Infinite Loop
This works. But the lambda
function is called for each item of the iterable, which is a finite number. Yes, we can increase the number in range()
to make the animation longer. But how can we make this an infinite loop?
The solution is to create an infinite iterable. They do exist! One option is to use itertools.cycle()
, which cycles through the elements of an iterable forever:
The iterable used in this final version is itertools.cycle([None])
. It doesn't matter what's the argument of cycle
as long it's a non-empty iterable. Recall that the elements of the iterable used in map()
, which is itertools.cycle([None])
, are passed to the lambda
function. But the lambda
function's parameter is not used. This is why it doesn't matter what we pass into it!
And this completes the animation. This is likely to be one of the most unreadable programs you'll ever deal with. And I hope you never write code like this. Unless it's a dare!
Code in this article uses Python 3.12
No programmers were harmed - but I feel slightly bad for the tortured lambda syntax! Great writeup.