Subsections of Python sessions
Session 1: Catch the Stars
In this session you build a mini game: a star falls down and you catch it with a basket. By the end of the session everything moves, the game keeps track of how many stars you catch, and you have your first game of your own.
What you’ll learn today
- Using variables to position objects
- The
draw() function: what you draw every frame - The
update() function: what changes every frame - Keyboard input: moving with the arrow keys
Step 0: Setup
- Open Thonny.
- Download the starter via this link, unpack the ZIP and open
main.py in Thonny. - Click the green Run button. You see a dark blue window with a basket at the bottom and a star at the top, but nothing moves yet.
- Problems? Take a look at Installing Thonny.
Step 1: Let the star fall
The update() function runs 60 times per second. So everything you put in it runs 60 times per second. That is how movement works in a game.
Right now there is only pass in update(). That means “do nothing”. Remove pass and add this:
def update():
star.y = star.y + 3
Click Run. The star now falls from top to bottom. When it falls off the screen, it disappears. We’ll fix that in a moment.
What happens here? star.y is the vertical position of the star. The bigger y, the lower on the screen. By adding + 3 every frame, the star drops a little each time.
The coordinate system: (0, 0) is at the top left, not at the bottom left like in maths. x goes to the right, y goes down. The star starts at y = 30 (almost at the top), the basket sits at y = 370 (almost at the bottom).

Move your mouse over the screen below to see the coordinates:
Move your mouse over the screen to see the coordinates.
Want to know more about how the screen works? → Coordinates explained
Adjust the sliders and watch how actor.x and actor.y change as the actor moves:
Step 2: ✅ Basic: move the basket
Time to steer the basket. Pygame Zero has a handy keyboard variable that keeps track of which keys are pressed.
Add this to update(), after the line about the star:
def update():
star.y = star.y + 3
if keyboard.left:
basket.x = basket.x - 5
if keyboard.right:
basket.x = basket.x + 5
Click Run and use the arrow keys. The basket moves along now!
Tip (optional, counts as Stretch): If you move the basket too far left or right, it disappears off the screen. Can you add a check that stops that? Hint: use WIDTH and compare it with basket.x.
Step 3: ⭐ Stretch: catch the star
Now you want something to happen when the basket touches the star. For that you use colliderect. It checks whether two rectangles overlap.
Add this to update():
if star.colliderect(basket):
star.y = 0
The star jumps back to the top when you catch it. But what if you miss it? Add this too:
if star.y > HEIGHT:
star.y = 0
Bonus: Make the star come back at a random spot every time. Add this at the top of the file:
Then replace star.y = 0 (in both places) with:
star.x = random.randint(20, WIDTH - 20)
star.y = 0
Now the star falls at a different spot every time. Much harder to catch!
Step 4: 🔥 Expert: add a score
A real game keeps track of what you score. For that you need a variable that lives outside the functions: a global variable.

Click the button below to see how score changes every time you catch a star:
Add this at the top, outside all functions (after the lines about basket and star):
In draw(), add:
screen.draw.text(f"Score: {score}", topleft=(10, 10), color="white")
In update(), change the catch code so the score goes up:
if star.colliderect(basket):
global score
score = score + 1
star.x = random.randint(20, WIDTH - 20)
star.y = 0
global score tells Python: “I mean the score from outside this function, not a new local variable.” Without that line you get an error message.
Click Run. The score sits at the top left and goes up every time you catch a star!
Showcase
Show your game to a coach and a buddy. Tell them what you made: what works, and what extras you added.
See you next time!
“Next time: things that FIGHT BACK. And they go BOOM, with real sound.”
Take it home
Try one of these extensions at home. Each one comes with a code hint to get you going.
Limit lives — Add levens = 3. On a miss (star.y > HEIGHT): levens -= 1. Stop the game when levens == 0 with game_over = True and show “Game Over” in draw().
Random star colour — On every catch: give the star a new colour with random.randint(50, 255) for red, green and blue. Start at 50, not at 0 — otherwise the star is sometimes black and invisible.
Background — Draw fixed stars in the background in draw() before the score. Use screen.draw.filled_circle(x, y, 2, "white") at 20 random positions. Seed random.seed(42) so they don’t flicker.
Second star — Make sterren = [star, Actor("star")]. Give every star its own x. Update both of them in update() and draw them in draw().
High score — Keep track of high_score. If score > high_score: high_score = score. Show f"Best: {high_score}" next to the score.
Smaller basket — After every 5 points: basket.width = max(30, basket.width - 5). Reset basket.width on a restart.
Bomb — Add a red circle that falls now and then. Use a timer bom_timer and random.randint to decide when it shows up. If the basket hits the bomb: score -= 2.
Difficulty — After every 5 stars: raise the falling speed by 1. Build in a maximum: max(3, star_snelheid).
Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.
View the cheatsheet for this session
Session 2: Dodge Meteors
Your spaceship drifts in the dark of space while meteors come down from above. Dodge them, or lose a life. The longer you survive, the faster they go. And this time you also build Dojo Defender: the bigger project that grows every session.
What you’ll learn today
- Using lists to keep track of several objects
- Spawning (creating) and removing objects
- Keeping track of lives and showing a game-over screen
- Dojo Defender: adding your own spaceship to the bigger project
Step 0: Setup
- Open Thonny.
- Download the starter via this link, unpack the ZIP and open
main.py. - Click Run. You see your spaceship at the bottom, but no meteors are falling yet.
- Problems? Take a look at Installing Thonny.
Step 1: Let meteors spawn
Fill in the spawn_meteor() function. A meteor is an Actor that starts at the top of the screen at a random x position:
import random
def spawn_meteor():
rock = Actor('meteor')
rock.x = 0
rock.y = random.randint(30, WIDTH - 30)
meteors.append(rock)
Call the function in update(), but not every frame, otherwise the screen fills up straight away. The update() function runs 60 times per second. Use that to keep a counter:
See below how fast a meteor moves at different speeds and how many pixels that is per second:
Use a counter:
# at the top, outside the functions:
timer = 0
def update():
global timer
timer += 1
if timer % 60 == 0: # every second (60 frames)
spawn_meteor()
Click Run. After a second the first meteor appears, but it doesn’t move yet.
Step 2: ✅ Basic: let meteors fall and disappear
Let all the meteors in the list drop, and remove them when they fall off the screen:
def update():
global timer
timer += 1
if timer % 60 == 0:
spawn_meteor()
for rock in meteors[:]: # [:] = copy so you can remove safely
rock.y += 4
if rock.y > HEIGHT + 30:
meteors.remove(rock)
Detecting collisions: If a meteor hits the ship, you lose a life. Add this in the loop:
if rock.colliderect(ship):
meteors.remove(rock)
global lives
lives -= 1
if lives <= 0:
# TODO: game over (step 3)
pass
Step 3: ⭐ Stretch: game over and speeding up
Game over: Add a variable game_over = False at the top. If lives == 0, set game_over = True. In draw(), show text when game_over is True:
if game_over:
screen.draw.text(
"GAME OVER",
center=(WIDTH / 2, HEIGHT / 2),
color="red",
fontsize=60,
)
Stop the movement in update() when game_over is True:
def update():
if game_over:
return
...
Raising the speed: Make meteors go faster as the score rises:
snelheid = 3 + score // 5 # every 5 points, +1 speed
rock.y += snelheid
Step 4: 🔥 Expert: score and several meteor sizes
Score: Every second you survive, +1 point. Add global score and raise it in update():
if timer % 60 == 0:
global score
score += 1
spawn_meteor()
Several sizes: Make small, medium and large meteors. Use a schaal variable and adjust the hitbox:
def spawn_meteor():
rock = Actor('meteor')
rock.x = random.randint(30, WIDTH - 30)
rock.y = 0
rock.schaal = random.choice([0.5, 1.0, 1.5])
meteors.append(rock)
Smaller meteors move faster; bigger ones are harder to dodge but give more points.
Dojo Defender
Download the Dojo Defender starter for today.
This is the base of the project that grows every session. Today you add your spaceship and make it move with the arrow keys.
Were you not there last time? No problem — download the solution from last time and you can join in straight away.
The complete Dojo Defender worksheet is on the Dojo Defender page.
Showcase
Show your game to a coach and a buddy. How long do you survive? Who gets the highest score?
See you next time!
“Next time: your own spaceship SHOOTS BACK. Enemies in formation, and you blast them away.”
Take it home
- Easy: change the background colour to deep purple or dark green.
- Medium: add a sound when a meteor hits the ship (
sounds/hit.wav). - Hard: make meteors faster as there are more of them on the screen (not just the score).
- Very hard: add power-ups: a golden star that gives you an extra life when you touch it.
View the cheatsheet for this session
Session 3: Space Blaster
Enemies stand in formation at the top of the screen. You’re at the bottom with a spaceship and one goal: knock them all out before they reach the bottom. Press SPACE to shoot, and don’t miss.
What you’ll learn today
- Keeping track of bullets in a list
- Creating a bullet when you press SPACE
- Collision between bullets and enemies
- Game over when an enemy reaches the bottom
Step 0: Setup
- Open Thonny.
- Download the starter via this link, unpack the ZIP and open
main.py. - Click Run. You see the ship at the bottom and 12 enemies in a grid, but nothing moves and you can’t shoot yet.
Step 1: Let the ship move
The ship already moves with the arrow keys. Check the code in update(). Make sure the ship doesn’t go off the screen:
if keyboard.left and ship.x > 30:
ship.x -= 5
if keyboard.right and ship.x < WIDTH - 30:
ship.x += 5
Step 2: ✅ Basic: shooting with SPACE
Fill in the shoot() function. A bullet is an Actor that starts at the position of the ship:
def shoot():
kogel = Actor('bullet')
kogel.x = ship.x
kogel.y = ship.y - 20
bullets.append(kogel)
Call shoot() when the space bar is pressed. Use on_key_down (Pgzero style):
def on_key_down(key):
if key == keys.SPACE:
shoot()
Make the bullets move up in update() and remove them when they go off the screen:
for kogel in bullets[:]:
kogel.y -= 8
if kogel.y < -10:
bullets.remove(kogel)
Step 3: ⭐ Stretch: hitting enemies
colliderect checks whether the rectangular hitbox of two actors overlaps. Move your mouse over the screen below to see when that happens:
Check in update() whether a bullet hits an enemy:
for kogel in bullets[:]:
kogel.y -= 8
if kogel.y < -10:
bullets.remove(kogel)
continue
for vijand in enemies[:]:
if kogel.colliderect(vijand):
bullets.remove(kogel)
enemies.remove(vijand)
global score
score += 10
break
Win condition: If enemies is empty, you’ve won:
if not enemies:
screen.draw.text("YOU WIN!", center=(WIDTH/2, HEIGHT/2), color="yellow", fontsize=60)
Step 4: 🔥 Expert: enemies move and game over
Make the enemies come down slowly. Use a timer:
vijand_timer = 0
def update():
global vijand_timer
vijand_timer += 1
if vijand_timer % 90 == 0: # every 1.5 seconds
for vijand in enemies:
vijand.y += 20
Game over when an enemy reaches the bottom:
for vijand in enemies:
if vijand.y > HEIGHT - 30:
global game_over
game_over = True
Faster shooting rate: Build in a cooldown so the ninja can’t shoot endlessly fast:
shoot_cooldown = 0
def update():
global shoot_cooldown
if shoot_cooldown > 0:
shoot_cooldown -= 1
def on_key_down(key):
global shoot_cooldown
if key == keys.SPACE and shoot_cooldown == 0:
shoot()
shoot_cooldown = 15
Showcase
Show your game to a coach. How fast do you clear all the enemies? Compare scores with a buddy.
See you next time!
“Next time: two players, one ball: PONG. And you learn real pygame without pgzrun.”
Take it home
- Easy: change the colour of the bullets or the enemies.
- Medium: make enemies move left and right (one direction per step, turn around at the edge).
- Hard: add a second wave: when all the enemies are gone, spawn a new row that comes down faster.
- Very hard: let enemies shoot back too: send a bullet down at random every N frames.
View the cheatsheet for this session
Dojo Defender
Download the Dojo Defender starter for today.
Were you not there last time? Download the solution from session 1 and start from there.
The complete worksheet: Dojo Defender Session 2.
Session 4: Pong
Two paddles, one ball: the classic game of Pong. But there’s a surprise: this session you write the game in real pygame, without pgzrun. You learn to set up the game loop yourself. That’s how most real games work.
What you’ll learn today
- The pygame game loop: events, update, draw
pygame.Rect for rectangles and collisions- Making the ball bounce (reversing the speed)
- Keeping score for two players
Step 0: Setup
- Open Thonny.
- Download the starter via this link, unpack the ZIP and open
main.py. - Click Run. You see two paddles and a yellow ball in the middle, but the ball doesn’t move yet.
Difference with pgzrun: There are no draw() and update() functions any more. Everything sits in one while running: loop.
Step 1: Make the ball move
Find the lines:
# bal.x += ball_dx # turn this on when you're done
# ball.y += ball_dy
Remove the # in front of both lines. But first set a starting speed. Find ball_dx, ball_dy = 0, 0 and change it to:
Click Run. The ball moves now, but it disappears off the edge. We’ll fix that in step 2.
Step 2: ✅ Basic: the ball bounces off the walls and the paddles
Add the following after ball.x += ball_dx and ball.y += ball_dy:
# Bounce off the top and bottom edge
if ball.top <= 0 or ball.bottom >= HEIGHT:
ball_dy = -ball_dy
# Bounce off the paddles
if ball.colliderect(left_paddle) or ball.colliderect(right_paddle):
ball_dx = -ball_dx
The ball now bounces forever. Also add limits for the paddles, so they don’t go off the screen. Have a look at the code that’s already in the starter.
Step 3: ⭐ Stretch: keeping score
If the ball leaves the screen on the left, the right-hand player scores. If it goes out on the right, the left-hand player scores:
# Ball out on the left → right scores
if ball.left <= 0:
score_right += 1
ball.center = (WIDTH // 2, HEIGHT // 2)
ball_dx = 4
# Ball out on the right → left scores
if ball.right >= WIDTH:
score_left += 1
ball.center = (WIDTH // 2, HEIGHT // 2)
ball_dx = -4
The score is already drawn in draw(). Check that the variables are right.
Step 4: 🔥 Expert: win condition and speeding up
Win at 5 points: Add a variable won. When a player reaches 5, stop the game:
if score_left >= 5:
won = "Left wins!"
if score_right >= 5:
won = "Right wins!"
Show the winner on the screen and stop the movement.
Speeding up: After every rally the ball gets a bit faster:
if ball.colliderect(left_paddle) or ball.colliderect(right_paddle):
ball_dx = -ball_dx * 1.05 # 5% faster
ball_dy = ball_dy * 1.05
Build in a maximum: ball_dx = max(-12, min(12, ball_dx)).
Showcase
Play a match against a buddy. First to 5 points wins. Show it to a coach.
See you next time!
“Next time: Breakout: the ball smashes bricks to pieces. And there’s SOUND.”
Take it home
- Easy: change the colour of the ball or the paddles.
- Medium: make one paddle bigger as that player falls behind (handicap).
- Hard: add an AI opponent: the right paddle follows the ball automatically.
- Very hard: add power-ups: an item that doubles the ball or makes the paddle bigger.
Dojo Defender
Today Dojo Defender moves to real Pygame!
Download the starter.
Were you not there? Download the solution from session 2.
Worksheet: Dojo Defender Session 3.
View the cheatsheet for this session
Session 5: Breakout
A ball bounces back and forth and smashes rows of coloured bricks to pieces. You steer the paddle to keep the ball in the air. And this time you hear it too: there are sound effects on every collision.
What you’ll learn today
- Loading and playing sound with
pygame.mixer - Collisions with several objects (a list of bricks)
- Making the ball bounce in the right direction
- Building a win and lose screen
Step 0: Setup
- Open Thonny.
- Download the starter via this link, unpack the ZIP and open
main.py. - Click Run. You see the paddle, the ball and the bricks. The ball already moves horizontally but not vertically. You’re going to change that.
Step 1: Make the ball move vertically
Find this line in the starter:
ball_dx, ball_dy = 4, 0
# ball_dy = -4 ← remove the # to make the ball move!
Remove the # in front of ball_dy = -4. Now the ball has a vertical speed. Add movement in the game loop (look for the update section):
ball.x += ball_dx
ball.y += ball_dy
Click Run. The ball moves diagonally but disappears off the edges.
Step 2: ✅ Basic: ball bounces off walls and paddle
Add bouncing:
# Top, left and right edge
if ball.left <= 0 or ball.right >= WIDTH:
ball_dx = -ball_dx
if ball.top <= 0:
ball_dy = -ball_dy
# Paddle
if ball.colliderect(paddle):
ball_dy = -ball_dy
if snd_hit:
snd_hit.play()
Lives: If the ball reaches the bottom, you lose a life:
if ball.top > HEIGHT:
levens -= 1
# Reset the ball on top of the paddle
ball.center = (paddle.centerx, paddle.top - BALL_SIZE)
ball_dy = -4
Step 3: ⭐ Stretch: breaking bricks
Check the collision with every brick in the loop:
for steen in stenen[:]:
if ball.colliderect(steen['rect']):
stenen.remove(steen)
ball_dy = -ball_dy
score += 10
if snd_hit:
snd_hit.play()
break # break only one brick per frame
Win condition: If all the bricks are gone, the ninja has won. Show a message:
if not stenen:
# show win screen
Step 4: 🔥 Expert: lives, explosion sound and speed
Explosion sound when a brick breaks (use snd_explode from the starter):
if snd_explode:
snd_explode.play()
Game over at 0 lives: show a screen and stop the ball.
Speeding up as fewer bricks are left:
snelheid_factor = 1 + (1 - len(stenen) / totaal_stenen) * 0.5
ball_dx_huidig = ball_dx * snelheid_factor
A new row of bricks when everything is gone: call stenen = maak_stenen() again and raise the difficulty.
Showcase
Who clears the most bricks? Compare scores and show your coach the longest streak.
See you next time!
“Next time: gravity. Your character falls, and you have to make it jump on platforms.”
Take it home
- Easy: change the colours of the bricks to your favourite colours.
- Medium: make some bricks twice as strong (they have to be hit twice).
- Hard: add a power-up that makes the paddle wider for a while.
- Very hard: add a second ball that spawns when score > 50.
View the cheatsheet for this session
Dojo Defender
Dojo Defender gets a menu and new enemy types!
Download the starter.
Were you not there? Download the solution from session 3.
Worksheet: Dojo Defender Session 4.
In this session you build a micro-platformer. Your player has gravity, can jump, and collects stars on platforms. For the first time you use pygame directly, without pgzrun, and you add sound effects with pygame.mixer.
What you’ll learn today
- Simulating gravity with a
vel_y variable - Jumping by making
vel_y negative - Detecting platform collisions with
pygame.Rect - Collecting items (collision with stars)
- Playing sounds with
pygame.mixer
Step 0: Setup
- Open Thonny.
- Download the starter via this link, unpack the ZIP and open
main.py in Thonny. - Click the green Run button. You see a window with a player, platforms and stars, but the player doesn’t fall yet.
- Problems? Ask a coach.
Step 1: Gravity
In a platformer the player falls down all the time, unless they’re standing on a platform. We simulate that with vel_y: a vertical speed that grows every frame because of gravity.
Find the # STEP 1 comment in main.py and replace the pass with:
# Apply gravity
vel_y += GRAVITY
player_rect.y += vel_y
# Check whether the player is standing on a platform
on_ground = False
for platform in platforms:
if (player_rect.colliderect(platform) and vel_y > 0):
player_rect.bottom = platform.top
vel_y = 0
on_ground = True
GRAVITY = 0.5 is already at the top of the file. Click Run. The player now falls onto the nearest platform and stays there.
What happens here? Every frame vel_y gets a bit bigger (the player accelerates downwards). If the player touches a platform while falling down (vel_y > 0), you put their bottom edge (bottom) exactly on top of the platform and stop the fall (vel_y = 0).
Step 2: ✅ Basic: jumping
A platformer without jumping is only half finished. Press SPACE to jump: that’s nothing more than suddenly making vel_y negative.
Find the # STEP 2 comment and add:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player_rect.x -= SPEED
if keys[pygame.K_RIGHT]:
player_rect.x += SPEED
# Jumping — only when you're standing on the ground
if keys[pygame.K_SPACE] and on_ground:
vel_y = -12
Click Run and test:
- Arrow keys: move left/right
- SPACE: jump up
Tip: If the jump feels too high or too low, change -12 to -10 (jump lower) or -15 (jump higher).
Step 3: ⭐ Stretch: collecting stars
Let’s add something to collect. The starter already has a stars list with pygame.Rect objects. You write the collision detection.
Find the # STEP 3 comment:
for star in stars[:]: # [:] so you can remove safely while looping
if player_rect.colliderect(star):
stars.remove(star)
score += 1
And in the drawing function (look for # DRAW STARS):
for star in stars:
pygame.draw.polygon(screen, YELLOW, star_points(star.centerx, star.centery, 12, 5))
star_points() is already defined in the starter. It works out the points of a 5-pointed star.
At the top of the game there’s score = 0. Draw the score on the screen (look for # DRAW SCORE):
score_surf = font.render(f"Stars: {score}", True, WHITE)
screen.blit(score_surf, (10, 10))
Step 4: 🔥 Expert: sound and win screen
Now you add a jump sound and a win screen for when all the stars have been collected.
Loading sound (this is already in the starter; check that jump.wav is in the sounds/ folder):
pygame.mixer.init()
jump_snd = pygame.mixer.Sound("sounds/jump.wav")
Playing the sound when you jump: add it to the jump code:
if keys[pygame.K_SPACE] and on_ground:
vel_y = -12
jump_snd.play()
Win screen: add it after the star loop:
if len(stars) == 0:
win_surf = font.render("You win! All stars collected!", True, YELLOW)
screen.blit(win_surf, (WIDTH // 2 - win_surf.get_width() // 2, HEIGHT // 2))
pygame.display.flip()
pygame.time.wait(3000)
running = False
Showcase
Show your game to a coach and a buddy. Can you collect all the stars before the time runs out?
Dojo Defender
Today’s Dojo Defender snapshot builds on session 5:
- Parallax star background — three layers of stars, each with its own speed
- Particle effects — exhaust, explosion debris, sparks
- Destructible asteroids — big → med → small → gone
Download the Dojo Defender starter or solution.
See you next time!
“Next time: click on bugs and ZAP them away, with your own classes.”
Take it home
Try one of these extensions at home:
- Easy: change the colour of the player when they’re in the air.
- Medium: add a timer. The player has 30 seconds to collect all the stars.
- Hard: add an enemy that walks back and forth on a platform; if the player touches it, the game restarts.
- Very hard: make the player double jump (press SPACE twice for a second jump in the air).
Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.
View the cheatsheet for this session
Session 7: Bug Zapper
Bugs fall down from the top. You shoot up to hit them. In this session you learn OOP: you write your first class of your own (Bug), and then two subclasses: one that swings and one that dives at you. Classes are the building blocks of almost every big program.
What you’ll learn today
- Writing a class with
__init__ and your own methods - Inheritance: a subclass that extends an existing class
- Keeping track of bullets in a list
- Collision detection between bullets and bugs
Step 0: Setup
- Open Thonny.
- Download the starter via this link, unpack the ZIP and open
main.py in Thonny. - Click Run. You see a screen with a player at the bottom, but no bugs and no bullets yet.
- Problems? Ask a coach.
Step 1: Spawning bugs and shooting
The Bug class
Find the # STEP 1 — BUG CLASS comment and write:
class Bug:
def __init__(self, x, y):
self.x = x
self.y = y
self.alive = True
def update(self):
self.y += 2 # falls downwards
def draw(self, screen):
pygame.draw.circle(screen, GREEN, (int(self.x), int(self.y)), 15)
# Eyes
pygame.draw.circle(screen, BLACK, (int(self.x) - 5, int(self.y) - 4), 3)
pygame.draw.circle(screen, BLACK, (int(self.x) + 5, int(self.y) - 4), 3)
Spawning bugs: the starter already has a timer. Find # SPAWN BUG:
bugs.append(Bug(random.randint(30, WIDTH - 30), -20))
Shooting: find # SHOOT (this is in the event loop at K_SPACE):
bullets.append(pygame.Rect(player_x - 3, player_y - 20, 6, 12))
Update the bullets every frame (find # UPDATE BULLETS):
for b in bullets[:]:
b.y -= 10
if b.y < 0:
bullets.remove(b)
pygame.draw.rect(screen, YELLOW, b) # this is already there
Step 2: ✅ Basic: hitting bugs
Now you write the collision detection: when a bullet hits a bug, the bug dies.
Find # STEP 2 — COLLISION:
for bug in bugs[:]:
bug.update()
bug.draw(screen)
if bug.y > HEIGHT + 20:
bugs.remove(bug)
continue
bug_rect = pygame.Rect(bug.x - 15, bug.y - 15, 30, 30)
for bullet in bullets[:]:
if bullet.colliderect(bug_rect):
bug.alive = False
bullets.remove(bullet)
score += 1
break
if not bug.alive:
bugs.remove(bug)
Click Run. Shoot at the bugs: they disappear when you hit them!
Step 3: ⭐ Stretch: SlingerBug
Now you make a subclass: a Bug that swings back and forth.
import math
class SlingerBug(Bug):
def __init__(self, x, y):
super().__init__(x, y)
self.start_x = x
self.timer = 0
def update(self):
self.timer += 0.05
self.x = self.start_x + math.sin(self.timer) * 60
self.y += 1.5 # a bit slower, but harder to hit
def draw(self, screen):
super().draw(screen)
# Orange circle around it as a visual cue
pygame.draw.circle(screen, ORANGE, (int(self.x), int(self.y)), 18, 2)
Spawning SlingerBugs: add this to the spawn code (for example every 3rd bug):
if random.random() < 0.3:
bugs.append(SlingerBug(random.randint(30, WIDTH - 30), -20))
else:
bugs.append(Bug(random.randint(30, WIDTH - 30), -20))
super().__init__(x, y) calls the __init__ of the parent class (Bug), so you don’t have to write self.x, self.y and self.alive again.
Step 4: 🔥 Expert: DuikBug
The DuikBug detects the position of the player and dives straight at it.
class DuikBug(Bug):
def __init__(self, x, y, player_x_ref):
super().__init__(x, y)
self.player_x_ref = player_x_ref # function that gives the current player x
self.speed_x = 0
self.speed_y = 1
def update(self):
target_x = self.player_x_ref()
dx = target_x - self.x
dist = max(abs(dx), 1)
self.x += dx / dist * 2
self.y += 2.5
def draw(self, screen):
super().draw(screen)
pygame.draw.circle(screen, RED, (int(self.x), int(self.y)), 18, 2)
Spawning: use a lambda to pass the player x:
bugs.append(DuikBug(random.randint(30, WIDTH - 30), -20, lambda: player_x))
Bonus: Draw an HP bar. Start with lives = 3 and take one off when a bug reaches the bottom.
Showcase
Show your game to a coach and a buddy. How many bugs can you zap before your lives run out?
See you next time!
“Next time: an endless highway in the sky, and everything SCROLLS past.”
Take it home
Try one of these extensions at home:
- Easy: make the bugs bigger or faster as the score goes up.
- Medium: add a
lives system with 3 lives, and when a bug reaches the bottom you lose one. - Hard: write a
PantserbBug that needs 3 hits before it dies (use an hp attribute). - Very hard: add mouse control: the player moves to the mouse position and shoots automatically every 0.5 seconds.
Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.
Dojo Defender: Boss
In this session you also keep building on Dojo Defender. You add a boss that shows up every 5 waves with 3 phases.
View the Dojo Defender session 6 for the instructions.
The starter and solution are ready for you.
View the cheatsheet for this session
Session 8: Sky Highway
Your ship flies through an endless highway in the sky. The background scrolls past you, red obstacles come from the right, and stars give points. The longer you survive, the faster it gets!
What you’ll learn today
- Making a scrolling background with a
scroll_x variable - Spawning objects that move from right to left
- Collision detection with lives
- Difficulty scaling: raising the speed as the score climbs
Step 0: Setup
- Open Thonny.
- Download the starter via this link, unpack the ZIP and open
main.py in Thonny. - Click Run. You see a static screen with a ship and a sky background, but nothing moves yet.
- Problems? Ask a coach.
A scrolling background gives the illusion of movement. We draw the background twice next to each other and shift the position every frame.
Find # STEP 1 and add:
# Move the background to the left
scroll_x -= speed
if scroll_x <= -WIDTH:
scroll_x = 0
# Draw the background twice
screen.blit(bg_image, (int(scroll_x), 0))
screen.blit(bg_image, (int(scroll_x) + WIDTH, 0))
scroll_x starts at 0 and gets smaller every frame. When it reaches -WIDTH we jump back to 0: a seamless loop.
Click Run. The sky now scrolls past your ship!
Step 2: ✅ Basic: obstacles and lives
Obstacles come from the right and move to the left. If you hit them, you lose a life.
Updating obstacles (find # STEP 2):
for obs in obstacles[:]:
obs.x -= speed
if obs.x < -obs.width:
obstacles.remove(obs)
pygame.draw.rect(screen, RED, obs)
# Collision
if ship_rect.colliderect(obs):
obstacles.remove(obs)
lives -= 1
if lives <= 0:
running = False
Drawing lives:
for i in range(lives):
pygame.draw.rect(screen, RED, (10 + i * 25, 10, 18, 18))
Spawning obstacles (in the spawn timer event):
h = random.randint(40, 120)
obstacles.append(pygame.Rect(WIDTH, random.randint(50, HEIGHT - h - 50), 30, h))
Step 3: ⭐ Stretch: collecting stars
Stars give points and disappear when you hit them.
Find # STEP 3:
for star in stars[:]:
star.x -= speed
if star.x < -20:
stars.remove(star)
continue
pygame.draw.circle(screen, YELLOW, star.center, 10)
if ship_rect.colliderect(star):
stars.remove(star)
score += 1
# Drawing the score:
score_surf = font.render(f"Score: {score}", True, WHITE)
screen.blit(score_surf, (WIDTH - 120, 10))
Spawning stars (in the same spawn timer, but less often):
if random.random() < 0.4:
stars.append(pygame.Rect(WIDTH, random.randint(40, HEIGHT - 40), 20, 20))
Step 4: 🔥 Expert: spawning and scaling the speed
Now you make the game harder and harder as the score climbs.
Scaling the speed (find # STEP 4):
speed = BASE_SPEED + score * 0.1
BASE_SPEED = 4 is already at the top. At score 20 everything goes twice as fast!
Adjusting the spawn interval: the higher the score, the more often new obstacles come:
spawn_interval = max(500, 1800 - score * 30) # min. 500ms
pygame.time.set_timer(SPAWN_EVENT, spawn_interval)
Game over screen:
if not running:
go_surf = font.render("GAME OVER", True, RED)
screen.blit(go_surf, (WIDTH // 2 - go_surf.get_width() // 2, HEIGHT // 2 - 30))
sc_surf = font.render(f"Score: {score}", True, WHITE)
screen.blit(sc_surf, (WIDTH // 2 - sc_surf.get_width() // 2, HEIGHT // 2 + 10))
pygame.display.flip()
pygame.time.wait(3000)
Showcase
Show your game to a coach and a buddy. Who gets the highest score?
See you next time!
“Next time: a boss that fights back, in THREE phases.”
Take it home
Try one of these extensions at home:
- Easy: add a second kind of obstacle (for example a blue one that flies higher or lower).
- Medium: add power-ups. Catch a green capsule for a temporary shield (the ship can be hit once).
- Hard: let the ship move up and down with the arrow keys, and keep it inside the screen so it can’t fly off.
- Very hard: add parallax scrolling: a second background layer (clouds) that scrolls slower than the first layer.
Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.
Dojo Defender: Your own version
Today you also keep building on Dojo Defender — but not with step-by-step instructions. You decide what you build.
Download the Dojo Defender starter — this is the complete game with everything in it (ship, enemies, asteroids, waves, boss). Pick a feature from the list, plan it briefly with a coach, and get started.
👉 Everything about Dojo Defender is on the Dojo Defender page.
View the cheatsheet for this session
Session 9: Boss Battle
The boss is waiting for you. It moves back and forth, shoots bullets and gets more dangerous as its HP drops. You shoot back, keep track of its HP on a bar, and survive three phases. This is the hardest session, and the most satisfying one if you make it through.
What you’ll learn today
- Drawing an HP bar on the screen
- A state machine: the boss behaves differently in each phase
- Making the boss shoot with a bullet timer
- Activating phases at HP thresholds
Step 0: Setup
- Open Thonny.
- Download the starter via this link, unpack the ZIP and open
main.py in Thonny. - Click Run. You see the boss at the top, your ship at the bottom. No movement, no bullets yet.
- Problems? Ask a coach.
Step 1: Making the boss move
The boss moves back and forth across the screen. When it hits the edge, it turns around.
Find # STEP 1 and add:
boss_rect.x += boss_speed
# Turn around at the edge
if boss_rect.right >= WIDTH or boss_rect.left <= 0:
boss_speed = -boss_speed
boss_speed = 3 is already at the top. Click Run: the boss thunders from left to right!
Moving the player and shooting (find # PLAYER):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] and player_rect.left > 0:
player_rect.x -= PLAYER_SPEED
if keys[pygame.K_RIGHT] and player_rect.right < WIDTH:
player_rect.x += PLAYER_SPEED
# Shoot (find # SHOOT in the event loop):
player_bullets.append(pygame.Rect(player_rect.centerx - 3, player_rect.top - 15, 6, 15))
Step 2: ✅ Basic: hitting the boss and the HP bar
Collision player bullet with boss:
Find # STEP 2:
for b in player_bullets[:]:
b.y -= 8
if b.y < 0:
player_bullets.remove(b)
continue
pygame.draw.rect(screen, YELLOW, b)
if b.colliderect(boss_rect):
player_bullets.remove(b)
boss_hp -= 1
if boss_hp <= 0:
running = False # player wins!
Drawing the HP bar (find # HP BAR):
# Background of the bar
pygame.draw.rect(screen, DARK_RED, (10, 10, 300, 20))
# Filled bar based on the current HP
breedte = int(300 * boss_hp / BOSS_MAX_HP)
pygame.draw.rect(screen, RED, (10, 10, breedte, 20))
# Label
hp_surf = font.render(f"Boss HP: {boss_hp}", True, WHITE)
screen.blit(hp_surf, (320, 10))
Click Run. Shoot at the boss: the HP bar shrinks!
Step 3: ⭐ Stretch: the boss shoots back
The boss fires a bullet downwards every N frames.
Find # STEP 3:
boss_shoot_timer += 1
if boss_shoot_timer >= boss_shoot_interval:
boss_shoot_timer = 0
boss_bullets.append(pygame.Rect(boss_rect.centerx - 4, boss_rect.bottom, 8, 16))
Updating boss bullets:
for b in boss_bullets[:]:
b.y += 6
if b.y > HEIGHT:
boss_bullets.remove(b)
continue
pygame.draw.rect(screen, ORANGE, b)
if b.colliderect(player_rect):
boss_bullets.remove(b)
player_lives -= 1
if player_lives <= 0:
running = False
Draw player_lives as hearts or red blocks at the bottom of the screen.
Step 4: 🔥 Expert: three phases
Now the boss reacts to its HP:
- Phase 1 (HP 100 to 50): normal behaviour
- Phase 2 (HP 50 to 25): move faster, shoot more often
- Phase 3 (HP < 25): maximum speed, double bullets
Find # STEP 4:
if boss_hp > 50:
boss_phase = 1
boss_speed_abs = 3
boss_shoot_interval = 60
elif boss_hp > 25:
boss_phase = 2
boss_speed_abs = 5
boss_shoot_interval = 35
else:
boss_phase = 3
boss_speed_abs = 7
boss_shoot_interval = 20
# Adjust the speed without changing direction:
boss_speed = boss_speed_abs * (1 if boss_speed > 0 else -1)
Phase 3: double bullets (add this in the shoot code):
if boss_phase == 3:
boss_bullets.append(pygame.Rect(boss_rect.left + 10, boss_rect.bottom, 8, 16))
boss_bullets.append(pygame.Rect(boss_rect.right - 18, boss_rect.bottom, 8, 16))
else:
boss_bullets.append(pygame.Rect(boss_rect.centerx - 4, boss_rect.bottom, 8, 16))
Win screen at boss_hp <= 0:
screen.fill(BLACK)
win = font.render("BOSS DEFEATED!", True, YELLOW)
screen.blit(win, (WIDTH // 2 - win.get_width() // 2, HEIGHT // 2))
pygame.display.flip()
pygame.time.wait(3000)
Showcase
Show your game to a coach and a buddy. Who reaches phase 3 and survives?
See you next time!
“Next time: your own game, from scratch, completely your own idea.”
Take it home
Try one of these extensions at home:
- Easy: change the colour of the boss per phase (green → orange → red).
- Medium: add a score: the faster you beat the boss, the more points.
- Hard: let the boss shoot diagonal bullets in phase 2 too (slanted left and right).
- Very hard: add a second player who can join in with WASD keys.
Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing.
View the cheatsheet for this session
Session 10: My Game
This is your session. No fixed steps, no compulsory mechanics. You decide what you build, and today you make it.
Your game, your rules
In nine sessions you learned:
- Variables, functions and game loops
- Keyboard and mouse input
- Sprites, collision detection and score
- Sound and animation
- Classes and inheritance
- Scrolling backgrounds, spawning and difficulty scaling
- HP bars, state machines and boss AI
All those building blocks are ready. Today you combine them in your own game.
Choose your concept
No idea yet? Here are a few genres to start from:
Dodge game: dodge objects that come faster and faster. Simple but addictive.
Platformer: jump from platform to platform, collect items, avoid enemies.
Shooter: shoot enemies that come from above or from the side.
Puzzle game: slide blocks, connect dots, or solve a pattern.
Racing game: design a course and get to the finish as fast as you can.
Tell a story: no score, no lives. Let the player click and make choices.
You can also combine things: a shooter with puzzle elements, a platformer with a boss at the end. Anything goes.
Plan your game
Before you write code, answer these three questions:
- What does the player do? (move, shoot, jump, click, …)
- What is the goal? (get a score, beat the boss, reach the end, …)
- What makes it hard? (enemies, obstacles, a timer, …)
Write your answers on a piece of paper or in a comment at the top of your code. That helps you stay focused when you get stuck.
Animation: switching sprite per frame
Do you want a loop animation, an explosion or a blinking object? Make a list of image names and switch actor.image every N milliseconds. The demo below shows how that works: use the slider to change the speed and the button to pause:
In Python you then use a list of image names and a timer:
frames = ['lopen_1', 'lopen_2', 'lopen_3', 'lopen_4']
anim_timer = 0
FRAME_DUUR = 10 # frames (at 60 fps = ~167 ms)
def update():
global anim_timer
anim_timer += 1
speler.image = frames[(anim_timer // FRAME_DUUR) % len(frames)]
Getting started
Download the starter via this link. The starter has:
- A working pygame game loop
- An empty
draw() and update() section - Colour constants and a font variable ready to go
- Comments that show where you can put what
Open main.py and start with Step 1 of your own plan. The cheatsheet on this page has all the patterns from the previous sessions at hand.
Stuck? Ask a coach. Describe what you want to achieve, not just what isn’t working. Then we can work out the best approach together.
Finished too fast? Add sound, make a start screen, or add a second player.
Showcase
At the end of the session everyone presents their game. You don’t have to finish it. Show what you made and tell:
- What is the concept of your game?
- Which technique from a previous session did you use?
- What would you still like to add if you had more time?
What now?
CoderDojo stops after ten sessions. Building things yourself doesn’t stop.
Keep building. Small projects teach you more than big courses. Make a game for your brother or sister, your pet, or just for yourself.
Need inspiration?
CoderDojo will be back. Will you?
Keep building!
View the cheatsheet for this session
Subsections of Git sessions
Session 11: Git: Your code's history book
Imagine: you work on your game for three hours. You add a cool new weapon. But… now the rest of your game doesn’t work anymore. And you no longer know what exactly you changed. Three hours of work gone.
Git is the solution. Git remembers every version of your files. Like an endless “undo” button that never forgets what you did.
In this session you install Git, you make your first repository and you record what you do for good: step by step, version by version. And the fun part: Git doesn’t only work for code. Also for notes, school reports and stories.
What is Git?
Git is a version control system. It keeps track of what you change in your files, when you did it, and why. Every time you finish a piece of work, you make a commit: a snapshot of all your files at that moment.
What makes Git special:
- Every version is kept. You can always go back to a previous commit if something breaks.
- You work locally. Git runs on your computer. No internet needed.
- Git is fast. Even projects with thousands of files are checked in milliseconds.
Git was made by Linus Torvalds, the same person who built Linux. He needed something to work on the Linux kernel with thousands of programmers at the same time. In 2005 he built Git in a weekend. Today almost every developer in the world uses it.
Git is not only for code
Git was built for source code, but it works with every text file. A text file is any file you can open and read with Notepad or a simple editor: .py, .html, .css, .json, .txt, .csv, .md (Markdown), .yml, .toml, you name it.
What Git is not good at: binary files. Those are files that don’t consist of readable text: images (.png, .jpg), videos (.mp4), compiled programs (.exe), Word documents (.docx). Git can track them, but it is slower and you can’t see what exactly changed. With text files git diff shows every letter you added or removed. With a .png Git only says “this file has changed”, but not what changed about the image.
Markdown and notes
Many note-taking apps these days use Markdown (.md files) to store your documents. Think of:
- Obsidian: your notes are just
.md files in a folder on your computer - Notion: exports to Markdown
- Joplin: stores everything as Markdown
- Typora, Zettlr, Logseq: all Markdown-based
Because Markdown files are ordinary text files, you can let Git loose on them. You keep the history of your notes, you can go back to a previous version of a document, and you can share your notes via GitHub, exactly like you do with code.
Putting a folder of Markdown notes under Git is just as simple as a Python project:
mkdir mijn-notities
cd mijn-notities
git init
echo "# My Notes" > start.md
git add start.md
git commit -m "First note: start page"
From that moment on Git keeps every change to your notes. Writing a report for school in Markdown? Commit every paragraph. Working on worldbuilding for a story? Git remembers every version of your plot.
When to use Git for notes and when not
| Do use Git | Better use something else |
|---|
Notes in Markdown (.md) | Notes in Word (.docx) |
Configuration files (.json, .yml) | Large images or videos with your notes |
| Writing: reports, stories, blogs | Real-time collaboration like Google Docs (Git is too slow for that) |
| Your CoderDojo cheatsheets | Passwords or personal diaries (unless private repo) |
In short: anything that is text you can put in Git. Anything that is binary, better not.
Why version control?
Without Git your workflow looks like this:
game.py
game_backup.py
game_backup2.py
game_final.py
game_final_echt.py
game_final_echt_nieuwe_wapens.py
Recognise that? With Git you only need one file: game.py. Git remembers the rest.
What version control gives you:
- Never lose work again. Everything you commit keeps existing. Always.
- Understand what happened. You see exactly which lines you changed and when.
- Experiment without risk. Make a separate copy of your code (a branch), try something weird, and throw it away if it doesn’t work. Your original code stays intact.
- Work together. Several people can work on the same project at the same time without overwriting each other’s code.
What do you NOT put in Git?
Git is made for text files: files you write yourself: code, notes, configuration. There are things you’d better keep out of Git:
| Do put in Git | Do NOT put in Git |
|---|
Your Python files (.py) | Passwords, API keys, tokens |
| Images your game uses | Files your program creates itself (.exe, .pyc) |
Sound files (.wav, .mp3) | The __pycache__ folder |
| Text files, configuration | Large files (>50 MB) like videos |
Your README.md | Settings of your own specific computer |
Why no passwords in Git? Because Git remembers everything, forever. Even if you delete a password later, it is still in the history. Anyone who ever sees your code can scroll back to that commit. And if your code ever ends up on GitHub, your password is on the internet. Forever.
Use a .gitignore file to tell Git which files it should ignore. You’ll find out about that in a moment.
Step 0: Git setup
Windows
- Go to git-scm.com and download the Windows installer.
- Double-click the downloaded file.
- Click through the installation. The default options are fine.
- Important: when picking the text editor, choose Nano or Notepad++ (not Vim, unless you already know Vim).
- After the installation, open Git Bash (search for it in the start menu).
Mac
Open Terminal (search for “Terminal” in Spotlight) and type:
If Git is not installed, macOS asks you to install the developer tools. Click “Install”. Done.
Linux (Ubuntu/Debian)
Checking that it works
Open a terminal (Git Bash on Windows) and type:
You see something like git version 2.47.0. That means: Git is ready.
Step 1: Tell Git who you are
Git wants to know who you are, so it can put your name on every commit. You do this once, after that Git remembers it.
git config --global user.name "Your Name"
git config --global user.email "your@email.com"
Use the same email address you will use for GitHub later. That makes linking easier.
Tip: --global means: this setting applies to all your Git projects. You only have to do this once.
Step 2: Making your first repository
A repository (or “repo”) is a folder where Git keeps track of the versions. Let’s make one.
First make a new folder for your project:
mkdir mijn-eerste-git-project
cd mijn-eerste-git-project
Now you tell Git: “This folder is a repository.”
You see: Initialized empty Git repository in .../mijn-eerste-git-project/.git/
Git has created a hidden .git folder. That is where it keeps everything: the history, the commits, the configuration. Never touch that folder by hand, because Git manages it itself.
Step 3: Committing your first file
Create a file. For example a Python file with one line:
echo 'print("Hello Git!")' > hallo.py
Now you tell Git: “Keep an eye on this file.”
With git add you put files in the staging area: a waiting room. You say to Git: “I want these changes in my next commit.”
Check what is ready:
Git shows:
On branch master
Changes to be committed:
new file: hallo.py
Now comes the commit itself:
git commit -m "My first commit: added hallo.py"
What happens here? -m stands for “message”. The text between quotes is your commit message: a short explanation of what you changed. Git has now made a permanent snapshot of your project.
Good commit messages: Write in English (that is the convention in almost all projects). Start with a verb: “Add”, “Fix”, “Update”. Keep it short: 72 characters max.
Step 4: Tracking changes
Open hallo.py in a text editor. Change the line to:
print("Hello Git! You remember my code.")
Save the file. Now look at what Git sees:
Git knows that hallo.py has changed. But what exactly has changed?
git diff shows exactly which lines you added (green with +) and removed (red with -). This is one of the most powerful things about Git: you literally see what changed, character by character, since your last commit.
Commit the change:
git add hallo.py
git commit -m "Update: greeting extended"
Step 5: Back in time
This is where Git gets magical. Suppose: you regret your last change. You want to go back to the first version.
First show all commits:
You see something like:
a1b2c3d Update: greeting extended
e4f5g6h My first commit: added hallo.py
Every commit has a unique code, a hash. That long code (shortened to 7 characters) identifies exactly that one version of your project.
To see what hallo.py looked like in the first commit:
git show e4f5g6h:hallo.py
There is your original print("Hello Git!") again.
Do you really want to go back in time? That is possible:
git checkout e4f5g6h -- hallo.py
Now hallo.py is back the way it was in your first commit. Check with cat hallo.py. You see the old version. But don’t worry: your second commit is not gone. With git checkout master -- hallo.py you go back to the newest version.
Step 6: What does NOT go in Git: .gitignore
Create a file that Git should ignore:
echo "GEHEIM_WACHTWOORD=12345" > geheimen.txt
git status now shows this file as “untracked”. Git has seen it, but doesn’t know if you want to track it. You can ignore it with a .gitignore file:
echo "geheimen.txt" > .gitignore
Now run git status again and geheimen.txt has disappeared from the list. Git ignores it. (Do add .gitignore itself to Git with git add .gitignore and git commit, so others know what has to be ignored too.)
A good .gitignore for a Python project:
# Python
__pycache__/
*.pyc
*.pyo
# Secrets
*.env
secrets.txt
# Your editor
.vscode/
.idea/
Showcase
Show a coach and a buddy:
- Your repository with at least 3 commits.
- The result of
git log --oneline: your commit history. - A
git diff that shows what you changed. - Your
.gitignore file.
See you next time!
“Next time: two versions of your code at the same time. You work on a new feature while the old version keeps working. That’s called branches, and it’s what makes Git really powerful.”
Take it home
- Easy: Make a repository for your notes. Put a Markdown file with what you learned today in Git, and commit it.
- Medium: Change something in your code or notes, use
git diff to look at the change, and commit it with a good message. - Hard: Make three commits with different changes. Use
git show to look at each version. Use git checkout to go back to the first commit, and then to the last one again. - Very hard: Make a Python project with a good
.gitignore. Make sure __pycache__/ and .pyc files really are ignored (check with git status).
Stuck? Ask a coach at the next dojo, or just try something else. Programming is doing. Learning Git is doing.
Session 12: Git Branches: Parallel universes for your code
Last session you learned to make commits: snapshots of your code on one timeline. Everything neatly one after the other.
But what if you want to try out a new idea? Adding a laser gun to your game, for example. You don’t know whether it will work. You don’t want to break your working code.
That is what branches are for.
A branch is a copy of your code where you can experiment freely. Your main code (the master branch) stays safe. If your experiment works, you paste it back into master. Doesn’t it work? You throw the branch away. Your master branch never noticed a thing.
Note: On GitHub and in many new projects the main branch is called main instead of master. That is exactly the same, only the name is different. In this course we use master, but if you come across main online, you know it means the same thing.
What you’ll learn today
- What a branch is and how Git stores branches internally
- Creating, switching and merging branches
- What a merge conflict is and how to solve it
- When you should and shouldn’t use a branch
Step 0: Recap: where are you now?
Open your terminal in last session’s project. Check where you are:
git status
git log --oneline
You have a few commits on master. This is your stable base: you’ll branch off from here in a moment.
Step 1: Your first branch
You’re going to add a new feature: a highscore.txt file that keeps the highest score. But first you make a branch:
git branch highscore-feature
Git has now created a new branch. But you are still on master. Check which branches there are:
You see:
highscore-feature
* master
The asterisk next to master means: “this is where you are now.” Switch to your new branch:
git switch highscore-feature
(Older Git versions use git checkout highscore-feature. Both work.)
git branch one more time: the asterisk is now next to highscore-feature. Everything you commit now happens on this branch. master stays untouched.
Tip: You can create a branch and switch to it straight away in one command:
git switch -c mijn-nieuwe-branch
-c stands for “create”. This is faster and you never forget to switch.
Step 2: Doing work on a branch
Create a new file:
echo "HIGHSCORE: 0" > highscore.txt
Commit your work:
git add highscore.txt
git commit -m "Add highscore.txt with start value 0"
Add another file that reads the highscore:
echo 'def lees_highscore():' > highscore_manager.py
echo ' with open("highscore.txt") as f:' >> highscore_manager.py
echo ' return int(f.read().split(": ")[1])' >> highscore_manager.py
Commit again:
git add highscore_manager.py
git commit -m "Add highscore_manager: function to read the highscore"
Now look at your log:
You see all your commits from master, plus the two new ones. Git remembers the whole chain.
Step 3: Two worlds at once
Switch back to master:
Look at what is here:
highscore.txt and highscore_manager.py are… gone. They only exist on the highscore-feature branch. On master everything is still exactly as you left it.
This is the magic of branches: two (or more) versions of your project, at the same time on your computer, without getting in each other’s way.
Switch back and forth once more to feel it:
git switch highscore-feature # the files are back
git switch master # the files are gone again
Step 4: Merging with git merge
Your highscore feature works. Time to put it back into master. This is called merging.
Make sure you are on master:
Merge the branch:
git merge highscore-feature
Git shows something like Updating a1b2c3d..e4f5g6h and Fast-forward. Your highscore-feature commits are now part of master. Check:
highscore.txt and highscore_manager.py are now on master. All commits from the feature branch have been included in the history.
Step 5: What are merge conflicts?
Suppose: you have changed something on master in highscore.txt, for example “HIGHSCORE: 100” instead of “HIGHSCORE: 0”. And at the same time you have changed highscore.txt on another branch too, to “HIGHSCORE: 500”.
If you now try to merge, Git doesn’t know which version is the right one. That is a merge conflict. Git asks you to decide.
Let’s provoke it on purpose.
On master:
echo "HIGHSCORE: 100" > highscore.txt
git add highscore.txt
git commit -m "Update highscore to 100 on master"
Make a new branch and change the same file there:
git switch -c andere-highscore
echo "HIGHSCORE: 500" > highscore.txt
git add highscore.txt
git commit -m "Update highscore to 500"
Switch back to master and try to merge:
git switch master
git merge andere-highscore
Git says: CONFLICT (content): Merge conflict in highscore.txt. Open highscore.txt in your editor. You see:
<<<<<<< HEAD
HIGHSCORE: 100
=======
HIGHSCORE: 500
>>>>>>> andere-highscore
These are the two versions. HEAD is your master version. Below it is the version from andere-highscore. You choose which one you keep, or you combine them.
Remove the markers and choose one version:
Save the file. Tell Git that the conflict is resolved:
git add highscore.txt
git commit -m "Merge: chose highscore from andere-highscore"
Conflict solved. Git can carry on again.
Step 6: Cleaning up branches
Your feature is merged. You don’t need the branch anymore:
git branch -d highscore-feature
git branch -d andere-highscore
-d stands for “delete”. Git refuses to delete a branch that hasn’t been merged yet: that is a safety valve. If you really want to throw it away without merging, you use -D (capital letter). But be careful with that.
When a branch, when not?
| Do use a branch | No branch |
|---|
| New feature that takes time | Fixing a typo in a comment |
| Experiment you don’t know will work | Small bugfix of one line |
| Something that can break your working code | You work alone and your change is clear |
| You work together with others | (with solo work master is often enough) |
Rule of thumb: If you doubt, make a branch. A branch costs nothing. Losing code does.
Tips for working smoothly with branches
- Keep branches short. A branch of 3 hours is fine. A branch of 3 weeks becomes a merge nightmare: the longer you wait to merge, the more has changed on
master in the meantime, and the bigger the chance of conflicts. - One thing per branch. “Add highscore” is a good branch. “Add highscore, menu, and new levels” is too much: split it up.
- Merge often. When you finish your feature, merge it back into
master straight away. Small, frequent merges are easier than one big monster merge. - Give branches clear names.
highscore-feature is good. test123 or branch2 are bad. In a week you won’t remember what was in test123. - Check where you are before you start.
git branch or git status tells you which branch you are on. Make this a habit, so you avoid accidentally writing code on master that really belongs on a feature branch.
Showcase
Show a coach and a buddy:
git branch with at least two branches (including master).- A different file or different code on each branch.
- Switching back and forth: the files appear and disappear.
- A successful merge of a feature branch into
master.
See you next time!
“Until now all your code only lives on your laptop. Next time we put it online. You make a GitHub account, push your repository to the internet, and your game can be seen all over the world. And: you can work together with others. GitHub: your code out into the world.”
Take it home
- Easy: Make a branch for a small feature in an existing project. Commit your work and merge it back.
- Medium: Deliberately create a merge conflict (two branches that change the same file) and solve it.
- Hard: Work with three branches at the same time on the same project. Merge them back into
master one by one. Check after every merge whether everything still works. - Very hard: Make a branch, commit three changes, switch back to
master, make a change in the same file there too, and merge the branch. Solve the conflict carefully and check with git log --graph what the history looks like.
Session 13: GitHub: Your code online, for everyone
Until now all your code lives on your laptop. Handy for you, but nobody else can get to it. And if your laptop crashes, you lose everything.
GitHub is a website where you put Git repositories online. It is a backup, a portfolio, and a collaboration platform in one. Today you put your code on GitHub. At the end of this session anyone with an internet connection can look at your projects, and you at theirs.
What is GitHub?
GitHub is not the same as Git. Git is the version control system that runs on your computer. GitHub is a website (github.com) that hosts Git repositories: a kind of Google Drive for code, but with Git underneath.
What GitHub adds to Git:
- Backup in the cloud. Your code is not only local. Laptop broken? Your code survives.
- Working together. Several people can work on the same repo. GitHub manages who is allowed to do what.
- Pull requests. You ask permission to put your changes into someone’s project, with discussion and review.
- Issues. Reporting bugs, suggesting features, tracking tasks: all in your repo.
- Portfolio. Your GitHub profile is your business card. Employers look at it.
GitHub is not the only option; there are alternatives such as GitLab and Bitbucket. But GitHub is the biggest, with more than 100 million users. In these sessions we use GitHub.
Step 0: Making a GitHub account
- Open github.com in your browser.
- Click on Sign up.
- Fill in your email address. Use the same address you set in session 11 with
git config: that saves hassle. - Choose a username. This becomes your identity on GitHub. Pick something you’ll still like in 5 years. You can change your username later, but old links to your profile won’t work anymore.
- Verify your account via the email GitHub sends.
- Choose the Free subscription. Everything you need is free: unlimited public and private repositories.
Tip for coaches: check whether the ninjas have access to their email during the session. Some laptops are not logged in to webmail. Have a plan B ready: if email verification doesn’t work, ninjas can verify GitHub later at home. The rest of the session works without verification too.
Step 1: Your first repository on GitHub
You have a local project on your computer (from session 11). You are going to put it online now.
On GitHub, click the + icon at the top right → New repository.
Fill in:
| Field | What you fill in |
|---|
| Repository name | mijn-eerste-git-project (same as your local folder) |
| Description | “My first Git project (CoderDojo session 11)” |
| Public / Private | Choose Public: others may see your code |
| Initialize with README | Do NOT tick: you already have a local project |
Click Create repository.
GitHub now shows instructions. Choose the block under "…or push an existing repository from the command line". Copy those three commands: you’ll need them in a moment.
Step 2: Linking your local repo to GitHub
In your terminal (in your project folder):
git remote add origin https://github.com/JOUW_USERNAME/mijn-eerste-git-project.git
What happens here? remote is an external location where your repo also lives. origin is the name you give that location. “origin” is the default name for your most important remote. You can have several remotes (for example origin for GitHub, backup for GitLab), but one is usually enough.
Check whether it worked:
You see:
origin https://github.com/JOUW_USERNAME/mijn-eerste-git-project.git (fetch)
origin https://github.com/JOUW_USERNAME/mijn-eerste-git-project.git (push)
fetch = this is where you get code from. push = this is where you send code to. Usually they are the same.
Step 3: Putting your code online with git push
Now comes the moment: sending your local commits to GitHub.
git push -u origin master
push = send my commits to the remote. -u origin master = “remember that master on my computer belongs with master on origin.” Next time you can simply type git push, because Git remembers the link.
GitHub asks you to log in. There are two ways:
- Via the browser: GitHub opens a browser window, you click “Authorize”. Done.
- With a Personal Access Token: create one via GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic). Give the token a name, tick
repo, copy the token, and paste it as the password in the terminal.
For ninjas: method 1 (browser) is the easiest. If that doesn’t work, help with a token.
After a successful push, refresh your GitHub page. Your files are there! Your hallo.py, your commit history: everything.
Step 4: Pushing changes
Make a change locally:
echo 'print("GitHub is cool!")' >> hallo.py
git add hallo.py
git commit -m "Add GitHub enthusiasm"
Push to GitHub:
You no longer need -u origin master, because Git knows the way. Refresh GitHub: your new commit is there.
Step 5: Starting a new project via GitHub
This is the reverse route: you start on GitHub and bring it to your computer.
On GitHub: New repository. Call it python-spelletje. This time do tick Add a README file; GitHub then makes a repo with one file in it. Click Create repository.
Now you bring this repo to your computer. This is called cloning:
cd ~ # go to your home folder
git clone https://github.com/JOUW_USERNAME/python-spelletje.git
GitHub creates a folder python-spelletje with the README in it. And it is a full Git repository straight away: the .git folder is already in it. You don’t need to do git init.
cd python-spelletje
git log --oneline # you see the first commit GitHub made for you
Now you can carry on working locally and push your commits to GitHub, exactly like in Steps 3 to 4.
Step 6: What if someone else makes changes too?
Suppose: you worked on python-spelletje at home and pushed. Now you are on the dojo laptop and want to carry on. You don’t have the newest version yet.
git pull fetches the newest commits from GitHub to your computer. It is two actions in one: git fetch (fetching) + git merge (merging with your local code).
Always do git pull before you start working, otherwise you fall behind and you get merge conflicts.
Summary of the workflow:
| Command | What it does |
|---|
git pull | Fetch the newest version from GitHub |
Work on your code, make commits (git add, git commit) | |
git push | Send your commits to GitHub |
Remember: pull before you start, push when you’re done.
Showcase
Show a coach and a buddy:
- Your GitHub profile with at least one repository.
- The commit history of your repo on GitHub (click on “X commits” above the file list).
- A fresh
git push that has just arrived on GitHub.
See you next time!
“Now your code is online. But what do you put in a commit message? How do you write a message that is still clear in three months? And how do you work with a team without breaking things? Next time: the professional side of Git: best practices every developer uses.”
Take it home
- Easy: Push an existing local project to a new GitHub repository.
- Medium: Make 3 commits locally, push them, and check on GitHub whether they are all visible.
- Hard: Clone a repo from GitHub, make a change, commit, and push back.
- Very hard: Simulate working on two computers: clone a repo, make a commit and push. Go to another folder, clone the same repo again (as if it were another computer), make a different commit, and try to push. What happens? Solve it with
git pull.
Session 14: Git Best Practices: Write commits you'll still understand later
You can type Git commands. Your code is on GitHub. But there’s a difference between “using Git” and “using Git well.”
This session is about the habits experienced developers apply every day. No new commands, just sharper rules. At the end you write commit messages you’ll still get something out of a year from now, and you work with a branching strategy that keeps your project tidy.
What you’ll learn today
- How to write a commit message that says something
- Three simple rules for branching
- What
git stash is and when to use it - The pull-before-you-push rule
Part 1: Commit messages that say something
A bad commit message looks like this:
git commit -m "fix"
git commit -m "update"
git commit -m "changed stuff"
Three months from now you look back. What was “fix”? Which “things”? You don’t know any more. You have to dig through the code to work out what happened. That’s a waste of time.
The good commit message: 3 parts
A professional commit message has three parts:
1. A short summary (max 72 characters)
Write in English. Start with a verb in the present tense. This is called “imperative mood”: as if you’re giving an order to the codebase. Think: “Add”, “Fix”, “Remove”, “Update”, “Refactor”.
Add highscore tracking
Fix star spawning off-screen
Remove unused collision code
2. A blank line
3. A longer explanation (optional, if it’s needed)
Add highscore tracking
Saves the highest score in highscore.txt. The score is
updated after every game-over. The file is created if it
doesn't exist yet.
This replaces the old method where the score only lived in
memory and got lost when the game closed.
The longer explanation tells why you changed something, not just what. The code itself already shows the “what”. The “why” is what you need in three months.
Commit message rules at a glance
| Do this | Not this |
|---|
| “Add player health bar” | “added health bar thing” |
| “Fix collision detection on left wall” | “fix” |
| “Remove debug print statements” | “cleanup” |
| Max 72 characters for the title | A whole story in the title |
| Present tense, English | Past tense (“added”, “fixed”) |
Why English? Because nearly every open source project uses English. Code is international. You write your Python code in English too (def calculate_score). Your commit messages follow that same language.
Why present tense? A commit describes what this change does, not what you did. “Add feature” (this commit adds a feature), not “Added feature” (I added a feature once). Git itself uses present tense too: Merge branch 'feature', not Merged.
Multi-line commits in the terminal
You can write a commit message over several lines by not closing the quotes:
git commit -m "Add highscore tracking
Saves the highest score in highscore.txt. The file is
created automatically on the first game-over."
Note: you only type the closing quote on the last line. Everything in between is part of your message.
When do you commit?
Commit often. Commit small.
Not: working for three hours, one enormous commit with 47 changed files.
Yes: a commit every 15 to 30 minutes, each time one logical piece is done.
A rule of thumb: if your commit message contains “and”, your commit is too big. “Add highscore tracking and fix menu bug and update sprites” → those should be three separate commits.
Small commits make it easier to:
- Understand what changed and when
- Undo one specific change without breaking other things
- Solve merge conflicts (small chunks = fewer conflicts)
Part 2: Branching: three simple rules
You already know how to make branches (session 12). Now: when and why.
Rule 1: master always works
The master branch is your shop window. Someone who clones your repo has to be able to run your code without error messages. master never contains half-finished work.
Not OK on master: code with # TODO: still needs finishing, commented-out functions, experiments you’ll “fix later.”
Fine on master: working code. Full stop.
Rule 2: One feature, one branch
A branch does exactly one thing. No more.
- Good:
feature/player-health-bar - Bad:
feature/health-bar-and-menu-and-sound-effects
If, while working on a feature, you think: “Oh, I can quickly fix that menu bug too”: stop. Make a new branch for the bugfix. Merge that one first. Then carry on with your feature.
Rule 3: Branches have a short life
A branch that exists for more than a few days becomes a problem. master changes in the meantime, and your branch falls behind. The longer you wait to merge, the bigger the chance of merge conflicts.
Merge your branch as soon as the feature is done. For small things (a 10-minute bugfix): you might not even need a separate branch.
Branch naming
Give branches names you’ll still understand a week from now:
feature/highscore-tracking
bugfix/star-spawn-offscreen
experiment/particle-effects
The prefix (feature/, bugfix/, experiment/) immediately tells you what kind of work is in this branch. It’s a convention, not a technical requirement, but it helps.
Part 3: Handy workflow commands
git stash: putting work aside
Say: you’re working on a feature branch. Your code is half done. You want to switch to master for a quick bugfix, but you don’t want to commit your half-finished work. That’s what git stash is for:
Git stores all your changes temporarily and puts your working directory back to the last commit. Now you can switch, do the bugfix, commit, and come back:
git switch master
# ... do the bugfix, commit ...
git switch feature-branch
git stash pop
git stash pop brings your stored changes back. Everything is as you left it: half done, but safely kept.
git pull --rebase: a cleaner history
Normally git pull does a merge. That creates an extra merge commit. Sometimes you don’t want that; you want a straight line of commits without branches.
This fetches the newest commits and puts your local commits on top of them, as if you made them after the remote commits. The result is a linear history: no merge commits that only say “Merge branch ‘master’”.
Note: only use --rebase on branches that you alone use. On shared branches a plain git pull (merge) is safer.
The pull-before-you-push rule
Before you git push:
Always. Even if you think nobody else pushed. It takes one second and stops GitHub from refusing your push with “Updates were rejected because the remote contains work that you do not have locally.”
This rule matters most when you work together. If you’re the only one working on a repo, the risk is smaller, but the habit is worth gold.
Part 4: The .gitignore you always need
Every project has files that don’t belong in Git. Your .gitignore is your first line of defence against accidentally committing passwords.
Minimal .gitignore for a Python project:
# Python
__pycache__/
*.pyc
*.pyo
# Virtual environment
venv/
.venv/
# Environment variables (often contain passwords/keys)
.env
# Your editor
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
Why ignore __pycache__/? These are compiled Python files that your program creates automatically. They differ per Python version and per computer. Putting them in Git only adds noise.
Why ignore .env? .env files often contain secret keys, API keys, database passwords. Those must never, ever go into Git.
Showcase
Show a coach and a buddy:
- Your last 5 commits, with good commit messages.
- Your
.gitignore file. - A branch name that follows the feature/ prefix convention.
See you next time!
“Now you work with Git like a professional. But there’s one more concept that every team uses: the pull request. Your code is ready, but before it goes into master, someone else has to approve it. How does that work? Next time: pull requests, the review wall that every change has to pass through.”
Take it home
- Easy: Rewrite the commit messages of your last 5 commits. Use the rules from this session.
- Medium: Create a branch with the
feature/ prefix, work in it, merge it back, and delete it. Do the same for a bugfix/ branch. - Hard: Use
git stash to save half-finished work, switch to master, make a quick fix, push, and come back to your feature with git stash pop. - Very hard: Create a situation where
git pull fails (clone your repo in two folders, make a different commit in both, push the first one, and try to push the second). Solve it: git pull, fix the merge conflict, and only then push.
Session 15: Pull Requests: Your project's front door
You built a feature on a branch. Good commit messages, tidy work. Now you want to get your code into master.
You could do git merge and be done. But in a team that doesn’t work. Someone else has to look at your code first: check for mistakes, for style, for things you missed. That is a pull request.
A pull request (PR) is a request: “Bring my changes in.” You open it on GitHub. Your teammates look at your code and give feedback. You change things. Only when everyone is happy does the code go to master.
In this session you make your first pull request. You review a buddy’s. And you find out what real teamwork with Git feels like.
What you’ll learn today
- What a pull request is and why teams use it
- Opening a PR on GitHub
- Updating your PR after feedback
- Reviewing someone else’s PR
- Merging a PR and cleaning up the branch
Step 0: Preparation: working in pairs
Today you work together with a buddy. Agree who is the author (who builds the feature) and who is the reviewer (who looks at the PR). Halfway through you swap.
The author needs a GitHub repo that the reviewer has access to. The easiest way:
- The author creates a repo on GitHub (public).
- The author goes to Settings → Collaborators → Add people.
- The author invites the reviewer with their GitHub username.
- The reviewer accepts the invitation (via email or GitHub notifications).
Now you can both push to the same repo.
Alternative (without collaborator access): the reviewer forks the author’s repo and opens a PR from the fork to the original. But today we keep it simple: direct collaborator access.
Step 1: Creating and pushing a feature branch (author)
The author clones the repo (if that hasn’t happened yet) and creates a feature branch:
git clone https://github.com/AUTEUR_USERNAME/ons-samenwerk-project.git
cd ons-samenwerk-project
git switch -c feature/groet-functie
Create a new file:
echo 'def groet(naam):' > groet.py
echo ' return f"Hallo {naam}!"' >> groet.py
Commit and push:
git add groet.py
git commit -m "Add groet function
Returns a greeting for the given name."
git push -u origin feature/groet-functie
Step 2: Opening a pull request (author)
- Go to your repo on GitHub.
- GitHub now shows a yellow bar: “feature/groet-functie had recent pushes”: click Compare & pull request. (If you don’t see the bar: click the Pull requests tab, then New pull request, choose
base: master ← compare: feature/groet-functie.) - Write a clear title: “Add groet function”
- Write a description. A good PR description has three parts:
## What
A function `groet(naam)` that returns a greeting.
## Why
We need a reusable greeting for the user interface.
This stops us having to write the same string in
several places.
## How to test
```python
from groet import groet
print(groet("CoderDojo")) # "Hallo CoderDojo!"
5. Click **Create pull request**.
Your PR is now open. Everyone with access to the repo can see it. GitHub shows the diff: exactly which lines you added (green) and removed (red).
## Step 3: Reviewing a PR (reviewer)
The reviewer opens the PR on GitHub. These are the steps of a review:
### 1. Read the description
Do you understand what this PR does? If not, ask for clarification.
### 2. Look at the diff
Click the **Files changed** tab. GitHub shows every changed line. Look at:
- **Does the code work?** Would you run it yourself if you cloned it?
- **Is the style consistent?** The same naming conventions as the rest of the project?
- **Are there mistakes?** Typos, missing imports, logic errors?
- **Are there no secrets in it?** Passwords, API keys, tokens?
- **Is it understandable?** Can someone seeing this code for the first time follow what happens?
### 3. Give feedback
Click a line to add a comment. Be specific:
**Good feedback:**
> In `groet.py` line 2: what happens if `naam` is empty? Maybe add a default value?
**Bad feedback:**
> This isn't good.
### 4. Submit your review
Click **Review changes**. You have three options:
- **Comment**: just feedback, no decision.
- **Approve**: the code is good, it can be merged.
- **Request changes**: something has to change before the PR can be merged.
On your first review: choose **Approve** if everything looks good, or **Request changes** if you want something changed.
## Step 4: Handling feedback (author)
The reviewer left a comment. The author changes the code:
```bash
# Make sure you're on your feature branch
git switch feature/groet-functie
Change groet.py based on the feedback. For example:
def groet(naam="wereld"):
return f"Hallo {naam}!"
Commit and push:
git add groet.py
git commit -m "Add default parameter to groet function"
git push
The new commit automatically appears in the PR. The reviewer sees the update and can look again.
Step 5: Merging and cleaning up
If the reviewer has Approved, it’s time to merge.
On GitHub, in the PR, you click Merge pull request. Choose Create a merge commit (the default option). Click Confirm merge.
The feature branch is now merged into master. On GitHub you can delete the branch with the Delete branch button that appears after merging.
Locally you have to clean up too:
git switch master
git pull # fetch the merge that just happened on GitHub
git branch -d feature/groet-functie # delete the local branch
Your master is now up to date. Your feature branch is gone, locally and on GitHub. Ready for the next piece of work.
The full PR workflow at a glance
1. git switch -c feature/iets ← Create a feature branch
2. Work, commit, work, commit
3. git push -u origin feature/iets ← Push to GitHub
4. Open a PR on GitHub ← Ask for a review
5. Reviewer looks, gives feedback
6. Change code, commit, push ← Handle feedback
7. Reviewer approved
8. Merge the PR on GitHub ← Code to master
9. git switch master && git pull ← Bring master up to date locally
10. git branch -d feature/iets ← Clean up the branch
Reviewing someone else’s PR: how do you do that well?
A review is not: “Looks good.” A good review is specific, constructive and respectful.
Questions you ask yourself during a review:
- Do I understand what this code does? (If not: ask for better comments or a clearer description.)
- Does the code do what the PR description promises?
- Can this be simpler?
- Is anything missing? (Error handling, edge cases, documentation)
- Are there secrets, passwords or personal data in it?
- Does the code follow the style of the rest of the project?
How to give feedback:
Start with what’s good. Then be specific about what could be better. End with a clear conclusion.
Example:
The function works exactly as described. Two suggestions:
- On line 2: a default value for
naam would be handy. - Consider adding a docstring that explains what the function returns.
Other than that it looks good. If you change these two things, I’ll gladly approve it.
Showcase
Swap roles with your buddy and do the whole workflow once more: now you are the reviewer.
Show a coach:
- An open pull request on GitHub with a description.
- At least one review comment on a buddy’s PR.
- A successfully merged PR: the feature code is now on
master.
See you next time!
This was the last Git session. What you can do now:
- Version-control your code with Git (
init, add, commit, log, diff) - Experiment on branches without breaking
master (branch, switch, merge) - Share your code online via GitHub (
push, pull, clone) - Write professional commit messages
- Work together via pull requests: opening and reviewing
This is not “extra”. This is how developers work every day. Whether you make games, build websites, or train AI: Git and GitHub are the tools that keep your project manageable. Use them.
Take it home
- Easy: Open a pull request for a change in one of your own projects.
- Medium: Ask a friend or family member to make a GitHub account. Invite them as a collaborator and let them review a PR.
- Hard: Contribute to a real open source project. Search GitHub for issues with the
good first issue label. Fork the repo, make a fix, and open a PR. - Very hard: Build a small project with two others. Use branches, pull requests and reviews. Work as if you’re in a professional team.
Session 16: Markdown: Write notes that look like the web
You might know HTML from the internet: <h1>, <p>, <a>. But for notes and documentation HTML is far too much typing. That’s what Markdown is for.
Markdown is a mini-language you write in plain text. It looks readable without having to convert it first. And with one push of a button it becomes neat HTML: headings, lists, links, tables.
In this session you learn the whole basis of Markdown. At the end you write notes that look professional, on GitHub, in Obsidian, or in any other program that understands Markdown.
What you’ll learn today
- Making headings and paragraphs
- Formatting text: bold, italic, strikethrough
- Lists and bullet points
- Links to other pages and websites
- Adding images
- Making tables
- Horizontal lines and code blocks
Why Markdown?
Markdown is everywhere:
- GitHub: every repo has a
README.md in Markdown. Issues, pull requests, wikis: all Markdown. - Obsidian, Joplin, Logseq: note-taking apps that save your documents as
.md files. - Discord, Slack, Reddit: support Markdown for quick formatting in messages.
- Hugo (this website!): all the session pages you see here are written in Markdown.
- Jupyter Notebooks: the text cells are Markdown.
The big advantage: you write plain text. No complicated tags. And the result is always readable, even as plain text.
Step 0: Creating a Markdown file
Markdown files end in .md. Make one:
mkdir mijn-notities
cd mijn-notities
echo "# My First Note" > test.md
You can open .md files in any text editor: Notepad, VS Code, Thonny, Obsidian. VS Code has a built-in preview: click the icon with the magnifying glass and the book at the top right.
Step 1: Headings and paragraphs
You make headings with #. The more #, the smaller the heading:
# This is a title (h1)
## This is a subtitle (h2)
### This is a smaller heading (h3)
#### Even smaller (h4)
You make a paragraph by just typing text. A blank line between them starts a new paragraph:
This is the first paragraph. It just runs on.
This is the second paragraph. Because there's a blank line in between.
Tip: you use one # only once per document: for the title. The rest starts at ##.
Step 2: Formatting text
**This is bold**
*This is italic*
~~This is strikethrough~~
You can also **combine *bold and italic*** in the same sentence.
The result:
**bold** becomes bold*italic* becomes italic~~strikethrough~~ becomes strikethrough
Note: no spaces between the asterisks and the text. ** bold ** doesn’t work. **bold** does.
Some editors also accept underscores: __bold__ and _italic_. But asterisks are the standard: use those.
Step 3: Horizontal lines
You make a horizontal line with three (or more) dashes:
Or with asterisks:
The result is a line across the full width. Use it sparingly: one or two per document is enough. Too many lines make your notes messy.
Step 4: Lists
Unnumbered list
Use -, * or +:
- Python
- Git
- Markdown
- GitHub
You can nest lists by indenting (2 or 4 spaces):
- Programming languages
- Python
- JavaScript
- Version control
- Git
- GitHub
Numbered list
1. Open Thonny
2. Write your code
3. Click Run
It doesn’t matter which numbers you type: Markdown numbers automatically. This:
1. Step one
1. Step two
1. Step three
…gives exactly the same result as 1, 2, 3. Handy if you want to add a step later without renumbering everything.
Checklist (GitHub style)
- [x] Install Git
- [x] Make your first commit
- [ ] Learn branches
- [ ] Open a pull request
[x] is ticked, [ ] is open. Works in GitHub issues, pull requests, and many note-taking apps.
Step 5: Links
External links (to websites)
[click here for the CoderDojo site](https://python.coderdojohasselt.be)
The word between [ ] is the clickable text. The URL between ( ) is the destination.
Internal links (to other pages in your project)
[View session 11](11-git-intro/)
This links to the file 11-git-intro/_index.md in the same folder.
Quick link (show the URL directly)
This shows the URL as a clickable link, without separate text.
Step 6: Images
An image is almost the same as a link, but with a ! in front:

The text between [ ] is the alt text: it appears if the image doesn’t load, or is read out by screen readers.
Images and Git: Small images (SVG, small PNGs) you can put in Git. Big images (>1 MB) and videos better not, because they make your repo slow.
Step 7: Tables
| Command | What it does |
|----------|-------------|
| `git init` | Create a new repository |
| `git add` | Get files ready for a commit |
| `git commit` | Record changes |
| `git push` | Send commits to GitHub |
The | characters form the columns. The --- line separates the header from the data. The : in the separator line determines the alignment:
| Left aligned | Centred | Right aligned |
|:-----------------|:-----------:|------------------:|
| left | middle | right |
Step 8: Code and code blocks
Inline code
Use backticks for short pieces of code in a sentence:
Use `git status` to see what has changed.
Code blocks
For several lines of code you use three backticks:
```python
def groet(naam):
return f"Hallo {naam}!"
```
The language (python) after the first backticks gives syntax highlighting. This works for dozens of languages: bash, html, css, javascript, json, yaml, markdown.
Code block without a language
```
This is just text
without syntax highlighting
```
Step 9: Blockquotes
Use > for quotes or important remarks:
> Git is not a backup. Git is a time machine.
> Someone on the internet
Several > lines one after another form one block. An empty > line starts a new paragraph inside the quote.
Step 10: Everything together: a real README
A good README.md combines everything you know now. Here’s an example:
# My Project
A short description of what this project does.
## Installation
```bash
git clone https://github.com/jouw-naam/mijn-project.git
cd mijn-project
```
## Usage
1. Open `main.py` in Thonny.
2. Click **Run**.
3. Choose a level and start.
## What you'll learn
- [x] Pygame Zero basics
- [x] Collision detection
- [ ] Highscore system
- [ ] Sound effects
## Links
- [CoderDojo Hasselt](https://coderdojohasselt.be)
- [Source code on GitHub](https://github.com/jouw-naam/mijn-project)
Cheatsheet
| What | Markdown |
|---|
| Heading level 1 | # Title |
| Heading level 2 | ## Subtitle |
| Heading level 3 | ### Smaller heading |
| Bold | **bold** |
| Italic | *italic* |
| Strikethrough | ~~strikethrough~~ |
| Horizontal line | --- or *** |
| List (unordered) | - item |
| List (numbered) | 1. item |
| Checklist | - [ ] open item |
| Link | [text](url) |
| Image |  |
| Inline code | `code` |
| Code block | ```language |
| Quote | > quote |
| Table | | column | column | |
Showcase
Show a coach and a buddy:
- A Markdown document with at least one heading, a list, a link and bold text.
- A table with at least two columns and three rows.
- A checklist with ticked and open items.
See you next time!
Markdown is simple, but it’s everywhere. Your READMEs on GitHub, your notes in Obsidian, your documentation: everything becomes more readable with Markdown. Use it. Don’t overdo the formatting. Good notes are clear, not busy.
Take it home
- Easy: Write a
README.md for your favourite Python project. Use headings, a list and a code block. - Medium: Make a note in Obsidian (or another Markdown editor) about what you learned this month. Use all the formatting from the cheatsheet at least once.
- Hard: Build a “cheatsheet” page in Markdown for a topic of your choice (Git commands, Python syntax, maths formulas). Use tables and nested lists.
- Very hard: Make a Markdown document with a nested list that goes at least three levels deep, a table with alignment, and a code block with syntax highlighting.