Turtle library
Concepts
turtle draws with a single virtual pen — called a turtle — that sits on a window with a position (x,y coordinate) and a heading (the direction it's currently facing). forward() moves it in that direction, left()/right() change the heading, and if the pen is down, moving it traces a line behind it.
The window also reacts to keyboard and mouse input, which makes turtle a natural fit for small, no-install games. A game needs a real window and display to run in, so the examples below aren't runnable in the browser — copy them into a local .py file to see them in action.
The origins of this library predate ordinary people owning computers: it comes from Logo, a language built in 1967 for teaching programming, whose original "turtle" was an actual robot that dragged a pen across a sheet of paper on the floor.
Setup
turtle ships with the standard library — nothing to install.
from turtle import *
* imports everything at once; if you want to explicitly specify of what you're using, import the precise function names instead (from turtle import forward, left, done).
The screen
Everything gets drawn inside one window — the screen.
Screen setup
Start by creating a window..
setup(500, 500) # width, height
title('My Game') # optional
Background
A solid color or a full image, set once on the window itself — not something that needs redrawing every frame.
bgcolor('skyblue') # a solid color
bgpic('landscape.gif') # or a full image, stretched to fit the window
Anything more custom than a flat color or a single picture — a gradient, a tiled pattern, a drawn horizon — is drawn by hand instead, using the same shapes from Drawing shapes below. Unlike bgcolor()/bgpic(), a hand-drawn background has to be part of the loop, since clear() erases it along with everything else each frame.
clear()
rectangle(Vec2D(-200, -200), 400, 400, 'skyblue') # backdrop, drawn first
# ... draw everything else on top ...
update()
The Tkinter Canvas underneath
turtle's window is a Tkinter Canvas widget underneath — getcanvas() returns it directly, for mixing in real Tkinter widgets or features once turtle's own tools stop being enough.
Clear screen
clear() erases drawings, leaving everything else — position, shape, color, event bindings — untouched, which is why it's the one used every frame.
clearscreen() is a full reset instead: drawings gone, every turtle removed, background and bindings back to their defaults, tracer back on. More like starting the whole script over than clearing one frame — useful for a "play again" restart, not for the frame loop itself.
Colors
Anywhere a color is expected — color(), bgcolor(), dot()'s color argument — turtle accepts three formats, all borrowed from Tk rather than defined by Python itself.
| Format | Example |
|---|---|
| A named color string | 'skyblue' |
| A hex string | '#33cc8c' |
| An RGB tuple | (0.2, 0.8, 0.5) — each value 0.0–1.0 by default |
There's no small fixed list of named colors — turtle draws from the same X11 color names Tk uses, a few hundred names in all. colormode(255) switches RGB tuples to the more familiar 0–255 range instead of 0.0–1.0.
Closing the window
done() (covered under The game loop) keeps the window open until it's closed by hand. exitonclick() is a common alternative for a finished game: keep the window open, then close it on the next click instead of waiting on the window's own close button. bye() closes it immediately, from code, without waiting for a click at all.
exitonclick() # instead of done() — click anywhere to quit
The turtle cursor
The turtle is the only thing directly controllable at any moment. Other moveable parts are plain data (Positions and motion) instead of as turtles of their own. (The class-based Turtle() interface can create more than one, each independently controllable, but that's a different, more advanced style than the one covered here.)
Everything about the turtle itself otherwise falls into four groups: what it looks like, what it draws with (if anything), what shapes it traces, and where it is.
Shape
Show or hide
The turtle — the small controllable arrow shown by default — is separate from anything it draws. Hiding it doesn't erase existing lines or shapes, and drawing continues normally either way; only the cursor itself disappears.
hideturtle() # ht() — hide it
showturtle() # st() — show it again
isvisible() # True or False
Shape, color, size
shape(shape_name) switches between every built-in shapes.
| Shape name | Looks like |
|---|---|
'classic' |
A small, thin-tailed arrow — the default. |
'arrow' |
A plain triangular arrowhead, larger than 'classic'. |
'turtle' |
A small turtle outline. |
'circle' |
A filled circle. |
'square' |
A filled square. |
'triangle' |
A filled triangle. |
'blank' |
Nothing at all — another way to hide the turtle, besides calling hideturtle(). |
color() sets its outline and fill, and can be any color format as described in the above colors section.
shape('turtle')
shapesize(2) # scale it up 2x
color('darkgreen', 'green') # outline, fill
Custom images
register_shape() installs an image file or a custom polygon as a shape, usable anywhere shape() is — a way to swap the cursor for a small custom picture. A limitation is that the custom image won't rotate. The built in shapes above turn to face the turtle's heading as it moves. An image shape always faces the same direction.
register_shape('snake.gif')
In a game
Many games hide the turtle and draws its own shapes instead — the right call once there's a trail, or several independent pieces, that no single turtle could represent alone. A game with just one clearly visible player, though, doesn't need any of that: give the turtle a shape and a color, then move it directly with goto().
shape('turtle')
color('green')
def move():
global player
player = player + aim
goto(player) # the turtle itself is what moves on screen
ontimer(move, 100)
That suits something like a maze runner or a chase game — one visible character the player steers, with everything else (walls, an enemy) drawn separately around it.
Being a visible, real turtle also means it can be clicked directly: onclick(function) fires only when the click lands on the turtle's own shape — unlike onscreenclick() (Input), which fires no matter where on the window the click happens.
def on_hit(x, y):
print('hit!')
onclick(on_hit)
Trace movement
With tracer
The Tracer is whether or not you can see the animation of the turtle moving.
By default, turtle animates its own movement — forward(), goto(), etc. are drawn bit by bit, animated as if it is moving across the screen. This is called the tracer and by default it is True.
If you don't want that, the alternative is turning the tracer() off. Every move then happens instantly, with nothing new appearing on screen until you call update(). tracer() can be switched on or off again at any point in a script, though turning it off before the first update() avoids a first frame flashing briefly.
tracer(False)
update() # if tracer is off, then rely on update() to redraw things
A related but separate setting — speed(n) controls how fast each individual forward()/goto() animates, from 1 (slowest) to 10 (fastest), or 0 for no animation delay at all. It only matters while tracer() is left on; with tracer(False), nothing animates regardless of speed.
Partial animation
tracer() also accepts two numbers, tracer(n, delay) — show only every n-th update, with delay milliseconds between them, instead of turning animation off completely. Useful for speeding up something slow and complex without losing the animation altogether.
no_animation() block
A context manager wrapping the same idea as tracer(False)/tracer(True) — animation is off for whatever runs inside the block, then back on (and shown) once it exits. The same with pattern as opening a file, applied to animation instead of a file handle.
with no_animation():
circle(50) # drawn instantly, all at once
Without tracer
setup(420, 420, 370, 0)
tracer(False)
With tracer(False), nothing new appears on screen until update() is called — normally once per frame, after everything for that frame has been drawn, so a whole frame gets drawn and shown at once instead of stroke-by-stroke. clear() wipes the previous frame's drawing first, so shapes don't pile up on top of each other.
clear()
# ... draw everything for this frame ...
update()
Ink
The "pen" is really the ink behind it:
-
Down means the tip is touching the paper, so ink comes out as the turtle moves.
-
Up means it's lifted, so moving it leaves no line behind.
| Function | What it does |
|---|---|
up() / down() |
Lift or lower the pen — move without drawing, or draw a line while moving. |
isdown() |
Return whether the pen is currently down. |
pensize(width) |
Set the line's thickness. |
color(outline_color, fill_color) |
Set colors for outline and fill. |
pencolor(outline_color) |
Set outline color. |
fillcolor(fill_color) |
Set fill color. |
color('black', 'yellow') # set outline and fill at once
down()
pensize(3)
forward(50) # draws a 3px-thick line
up()
isdown() # False
goto(0, 0) # moves back without drawing
Drawing shapes
Shapes are drawn by moving the pen with up()/down() (pen up means move without drawing a line), goto(), forward(), and left(), then filling the outline with begin_fill()/end_fill().
| Function | What it does |
|---|---|
clear() |
Erase the previous frame's drawing. |
up() / down() |
Lift or lower the pen — move without drawing, or draw a line while moving. |
goto(x, y) |
Move the pen to an absolute position (also accepts a single Vec2D). |
forward(distance) |
Move the pen forward in its current heading, drawing a line if the pen is down. |
left(angle) |
Turn the pen's heading, in degrees. |
color(fill_color) |
Set the pen's fill/outline color. |
begin_fill() / end_fill() |
Start/stop filling the shape traced in between. |
pensize(width) |
Set the outline's thickness. |
update() |
Show everything drawn since the last update(). |
clear() always comes first and update() always comes last in a frame — everything in between is whatever needs drawing that frame. Within that: up() before moving somewhere without a line trailing behind, down() before tracing an outline; begin_fill() right before that outline, end_fill() right after it, with nothing in between that isn't part of the shape.
def square(point, size, fill_color):
"""Draw a filled square centered on point."""
x, y = point
up()
goto(x - size / 2, y - size / 2)
down()
color(fill_color)
begin_fill()
for _ in range(4):
forward(size)
left(90)
end_fill()
Dot
A filled circle, built into turtle directly — no custom function needed. dot(diameter, color) draws it centered on wherever the pen currently is.
up()
goto(0, 0)
dot(20, 'green')
Circle
circle(radius) traces an actual curved path instead of stamping an instant dot — the center ends up radius units to the turtle's left, and the pen itself ends up back on the circle once it's done.
circle(50)
An extent (an angle) draws only part of the circle — an arc, or a pie-slice shape once combined with begin_fill()/end_fill() — instead of the whole thing.
begin_fill()
circle(50, 90) # a quarter-circle arc
end_fill()
steps swaps the smooth curve for a regular polygon with that many sides instead — the same forward()/left() loop square() uses by hand, done automatically.
circle(50, steps=6) # a hexagon
Rectangle
Same idea as square(), with independent width and height, drawn from a corner instead of the center — the shape a paddle or panel-style element would use.
def rectangle(point, width, height, fill_color):
"""Draw a filled rectangle with point as its bottom-left corner."""
x, y = point
up()
goto(x, y)
down()
color(fill_color)
begin_fill()
for _ in range(2):
forward(width)
left(90)
forward(height)
left(90)
end_fill()
Stamping
When the built-in shape() already looks right, stamp() leaves a copy of it at the pen's current position — a shortcut over writing a custom drawing function like square() or rectangle(). It returns an id, so a specific stamp can be erased later with clearstamp(stamp_id).
shape('circle')
goto(food)
stamp_id = stamp()
Text
write(text) draws a string at the pen's current position — the way a score or a message gets shown, since none of the shapes above are built for it.
up()
goto(0, 180)
write('Score: 3', align='center', font=('Arial', 16, 'normal'))
align positions the text relative to that point ('left', 'center', or 'right') instead of always starting from it. Like everything else on screen, a score needs to be redrawn as part of the frame — clear() erases it too, so write() has to run again every time the score changes.
Positions and motion
A position is two numbers, x and y. turtle represents one with Vec2D, a tuple that also supports vector arithmetic — unlike a plain tuple, adding two Vec2Ds adds their coordinates instead of concatenating them.
from turtle import Vec2D
ball = Vec2D(0, 0)
aim = Vec2D(3, 5)
ball = ball + aim # Vec2D(3, 5) — moved by aim
x, y = ball # unpack like any other tuple — 3, 5
Vec2D is immutable
Vec2D has no .x/.y attributes to assign to — like any tuple, it can't be changed in place. Moving something means reassigning the variable to a brand-new Vec2D, not editing the old one.
aim = Vec2D(0, -10)
aim = Vec2D(10, 0) # a new Vec2D — not aim.x = 10
Reassigning from inside a function
Reassigning a global variable's name from inside a function needs global, covered on Functions — a game typically has at least one small function whose only job is reassigning a position or direction this way. Mutating something in place instead (trail.append(...), paddles[1] = paddles[1] + Vec2D(0, 20), both from "Many positions at once" below) doesn't need global, since the name itself is never reassigned — only reassignment does.
aim = Vec2D(0, -10)
def change(x, y):
global aim
aim = Vec2D(x, y)
The turtle's own position
The turtle itself always knows where it is — pos() returns its current location as a Vec2D, the same type used everywhere else on this page, so a separate variable isn't strictly needed if the pen itself is what's moving.
goto(50, 30)
here = pos() # Vec2D(50, 30)
towards(point) returns the angle from the turtle's current position toward another point, for aiming one thing at another instead of moving toward it directly. setheading(angle) then turns the pen to face that angle, in degrees, before forward() moves it.
setheading(towards(ball))
forward(5)
Many positions at once
A game's state is rarely just one lone position — a trail that grows over time, or several independent entities tracked at once. Both build on the same list/dict operations covered on Collections.
trail = [Vec2D(10, 0)]
trail.append(trail[-1] + aim) # grow by one at the end
trail.pop(0) # shrink by one at the start
paddles = {1: Vec2D(-200, 0), 2: Vec2D(190, 0)}
paddles[1] = paddles[1] + Vec2D(0, 20) # move just one of them
The game loop
ontimer(function, ms) calls a function once, after a delay. Having that function schedule itself again as its last line turns a single call into a repeating loop — the heartbeat of any turtle game: move, redraw, schedule the next frame.
| Function | What it does |
|---|---|
ontimer(function, ms) |
Run a function once, after a delay — the basis of the game loop. |
done() |
Keep the window open, listening for scheduled calls. |
Call the loop function once, by hand, to draw the first frame — after that, it reschedules itself with ontimer() every time it runs.
def move():
# ... update positions, redraw the screen ...
ontimer(move, 100) # call move() again in 100ms
move() # kick off the first frame
done() # keeps the window open, listening for the scheduled calls
Spawning and removing things over time
A loop can also grow or shrink a list of its own entities as it runs — occasionally adding a new one, and dropping ones that have drifted off-screen or otherwise stopped mattering, using the same list operations as Positions and motion's "Many positions at once". randrange() is from the random module, not turtle.
from random import randrange
if randrange(10) == 0:
entities.append(new_entity())
while entities and not inside(entities[0]):
entities.pop(0)
done()
A Python script normally runs top to bottom and exits once it reaches the last line. done() is always that last line — but instead of letting the script exit, it blocks: it hands control to the window and just sits there, waiting.
While it waits, it watches for the scheduled calls and input registered earlier — ontimer(), keyboard clicks, mouse clicks — and fires them as they come in. Those registering functions don't wait around themselves; each one just notes down a function to run later and immediately moves on. Without a final done() (or mainloop(), an alias for the same thing) to block and keep the window alive, the script would reach its own end and exit before any of that registered work got a chance to run.
- Before
done(): window setup, turtle setup, the function definitions, and the one manual call that kicks off the first frame. done()itself: called exactly once, by itself, as the very last line.- After
done(): nothing. That line never finishes, so anything placed below it never runs.
Input
Every kind of input turtle supports works the same way: register a function once, and it gets called automatically whenever the matching event happens — nothing actually listens for anything until done() starts the event loop at the end of the script, so registration itself can happen in any order.
Keyboard
listen() puts the window in a state where it's paying attention to keyboard events; onkey(function, key) then binds one key to a function, called with no arguments every time that key is pressed.
| Function | What it does |
|---|---|
listen() |
Start paying attention to keyboard events. |
onkey(function, key) |
Run a function, with no arguments, whenever a key is pressed. |
def change(x, y):
global aim
aim = Vec2D(x, y)
listen()
onkey(lambda: change(10, 0), 'Right')
onkey(lambda: change(-10, 0), 'Left')
Press vs release
onkey() is really an alias for onkeypress() — a key firing the moment it's pressed down. onkeyrelease(function, key) is the counterpart, firing when the key comes back up instead.
Mouse
onscreenclick(function) calls a function every time the window is clicked, passing the click's x and y coordinates as arguments.
| Function | What it does |
|---|---|
onscreenclick(function) |
Run a function, passed the click's x/y, whenever the window is clicked. |
def tap(x, y):
global ball
ball = ball + Vec2D(0, 30)
onscreenclick(tap)
Dragging and releasing
ondrag(function) calls a function repeatedly, passed the pointer's x/y, while the mouse moves with the button held down — for something dragged around rather than tapped. onrelease(function) is the counterpart to onscreenclick(), firing when a click ends instead of when it starts.
Dialog prompts
textinput(title, prompt) and numinput(title, prompt) pop up a small dialog box asking for a string or a number, returning what the player typed (or None if they cancelled). It's a separate native window, centered over the game window rather than drawn on the canvas.
Unlike everything else on this page, a dialog pauses until it's answered.
name = textinput('Player name', 'Enter your name:')
lives = numinput('Lives', 'How many lives?', default=3, minval=1, maxval=5)
Detecting collisions
Many games reduce to the same question: is this position touching that one?
def inside(point):
"""Return True if point is within the screen's boundaries."""
x, y = point
return -200 < x < 200 and -200 < y < 200
Distance
abs() on a Vec2D returns its length — subtracting two positions first gives the distance between them, without writing out a square root by hand.
paddle = Vec2D(-200, 0)
close_enough = abs(ball - paddle) < 15
Overlap
Checking whether a point falls within a range — a paddle's height, say — is a plain comparison, no vector math needed.
_, paddle_y = paddle
_, ball_y = ball
low = paddle_y
high = paddle_y + 50
touching = low <= ball_y <= high
Membership
A position can also collide with itself — checking whether it already appears somewhere in a list of positions, the same in used for any other membership check.
head = trail[-1] + aim
crashed = head in trail
Common patterns
Every block above is a small, general-purpose piece. Combined, a few recurring shapes cover most simple games — each sketched below as pseudocode, the shape to fill in with real building blocks from the sections above.
A single controlled object — one position (Positions and motion), moved by keyboard or mouse input (Input), redrawn every frame (The game loop).
position = starting point
direction = nothing, to start
def on_key(new_direction):
change direction to new_direction
def move():
position = position + direction
redraw the object at its new position
schedule the next frame
A trail that grows — a list of positions instead of one (Many positions at once), growing at one end and shrinking at the other as the controlled object moves, checked against its own history for a collision (Membership).
trail = [starting position]
direction = starting direction
def move():
new_head = trail[-1] + direction
if new_head is out of bounds or new_head in trail:
stop — game over
trail.append(new_head)
if new_head did NOT reach a target:
trail.pop(0) # shrink back down to the same length
# otherwise leave the tail alone — the trail grows by one
redraw every position in trail
schedule the next frame
Several independent entities — more than one position tracked at once: a dict keyed by name or number for entities that stick around the whole game (Many positions at once), or a list that grows and shrinks as entities come and go over time (The game loop's "Spawning and removing things over time").
entities = [] # or {}, for ones with names rather than a changing count
def move():
for each entity in entities:
move it
occasionally, append a new entity
remove any entity that's drifted off-screen or otherwise stopped mattering
redraw every entity
schedule the next frame
Reacting to a collision — once two positions are found to be touching (Distance, Overlap, Membership, or the boundary check that opens Detecting collisions), something specific has to change as a result — the collision check on its own doesn't do anything.
def move():
# ... update positions ...
if touching(a, b):
one of:
stop entirely, without scheduling another frame # a game-ending collision
change direction # a bouncing collision
update score, or remove one of the two # a scoring collision
redraw everything
schedule the next frame
Bouncing off a boundary — a special case of reacting to a collision, common enough on its own: hitting an edge flips the component of direction pointing into it, and leaves the other one alone, so the bounce looks like a reflection instead of a stop or a reversal.
def move():
position = position + direction
x, y = position
dx, dy = direction
if x is past the left or right edge:
direction = Vec2D(-dx, dy) # only the x part flips
if y is past the top or bottom edge:
direction = Vec2D(dx, -dy) # only the y part flips
redraw everything
schedule the next frame
Multiple players — more than one controlled object (Several independent entities), each moved by its own subset of key bindings instead of one shared direction.
players = {1: starting position, 2: another starting position}
def move_player(which, change):
players[which] = players[which] + change
on_key(lambda: move_player(1, up), key_for_player_1_up)
on_key(lambda: move_player(2, up), key_for_player_2_up)
# ... one binding per player, per direction ...
A permanent trail instead of redrawing — leaving the pen down the whole time (Ink as the game itself) instead of lifting it to reposition, so movement itself draws something that's never erased, rather than a shape cleared and redrawn every frame.
def move():
position = position + direction
goto(position) # pen stays down — this itself draws the trail
if position in trail:
stop entirely # crossed its own ink
trail.append(position)
schedule the next frame
Mixing and matching these — a controlled object and a growing trail, say, or several players and a score — is how a specific game takes shape from these general pieces.
More advanced games
turtle's window and shapes are enough for something like snake, flappy, or pong, but not for much more — no sprites, no sound, no real physics. For anything more advanced, pygame and arcade are the two most common next steps; both have their own official documentation, linked above.