💻 Computer Science · Middle School · CS 060

Middle School Coding: From Blocks to Python

This is a first programming course for someone who has used a computer for years but never told one what to do. It starts away from the screen, with algorithms as exact instructions and a way to check them by hand using a trace table. Then you build in Scratch, the free block language from the Scratch Foundation, where blocks snap together and you can watch every idea move: sequences, repeat…

Start the interactive course (quizzes, progress, videos) →

Free forever. No sign-up, no ads. 24 lessons. The full lesson text is below so you can read it right here.

Module 1: Instructions a Computer Can Follow

Algorithms as exact instructions, checked by hand before any computer is involved: what makes a list of steps an algorithm, how to turn vague instructions into exact ones, and how a trace table follows an algorithm step by step, including steps that choose and steps that repeat.

Algorithms: Instructions With Nothing Left to Guess

  • Define an algorithm and test a list of steps against four checks: exact, doable, in order, and finishing.
  • Rewrite vague instructions as exact ones, including units and a starting position.
  • Follow an algorithm literally and record the result of each step in a trace table.

A spacecraft lost over two kinds of seconds

On 23 September 1999, NASA's Mars Climate Orbiter fired its main engine to slow down and slip into orbit around Mars. It passed behind the planet, just as planned. It never came out the other side.

The orbiter had been flying since 11 December 1998. On the way, small thrusters gave it little pushes, and software on the ground worked out how big each push had been. The teams had agreed in writing that those numbers would be in newton-seconds, a metric unit. One program wrote them in pound-seconds instead. A pound-second is about 4.45 newton-seconds, so every push went into the records as much smaller than it really was.

Nothing in the numbers said which unit they used. A number on its own is just a number. The navigation software treated every value as newton-seconds, because that is what it expected, and it planned the path into orbit from numbers that were wrong. The plan was for the orbiter to pass 226 kilometres above Mars. Investigators later estimated that it came within 57 kilometres. Anything below 80 kilometres was too low to survive.

No computer in this story got its arithmetic wrong. Each one did exactly what its instructions and its data told it to do. That is the first fact of programming, and the rest of this course grows from it. A computer follows instructions exactly, and it has no idea what you meant.

What an algorithm is

An algorithm is a list of steps that solves a problem, written so exactly that the follower never has to guess. The follower can be a person or a machine. A recipe is close to an algorithm. So are the instructions for building a flat-pack shelf, or the rule a teacher uses to turn marks into grades.

The word is very old. Muhammad ibn Musa al-Khwarizmi was a scholar at the House of Wisdom in Baghdad, born around the year 780. He wrote a book about doing sums with the Hindu-Arabic numerals, the digits 0 to 9 that you use every day. A Latin version of the book carried his name in its title, Algoritmi de numero Indorum, and the word algorithm grew out of that name.

A program is an algorithm written in a language a computer can run. In this course you will write programs in two languages. The first is Scratch, where you snap coloured blocks together. The second is Python, where you type the instructions as text. The language changes, but the thinking underneath stays the same, and the thinking comes first.

Four checks for any algorithm

How do you know whether a list of steps is good enough to call an algorithm? Ask it four questions. If any answer is no, a computer following it will go wrong, or get stuck, or never stop.

CheckThe question to askA step that fails it
ExactDoes every step have only one meaning?"Add a little water."
DoableCan the follower really do this step?"Pick the funniest joke." A machine cannot judge funny.
In orderWill the steps work in this order?"Butter the toast" coming before "Toast the bread."
FinishesIs there a point where it stops?"Keep stirring." Until when?

Two more things belong in every algorithm, even when nobody writes them down: what goes in, and what comes out. A grading rule takes a mark in and gives a grade out. A route finder takes two places in and gives a list of turns out. Programmers call these the input and the output, and you will meet both words in almost every lesson from here on.

Turning vague into exact

People fill gaps without noticing. If a friend says "walk to the door", you pick a route, step round the chair, and stop before you hit the wood. A computer can do none of that unless every part is spelled out. Here are some everyday instructions, with versions exact enough for a machine.

VagueExactWhat was added
Walk to the door.Walk forward 6 steps, then stop.A distance, and a place to stop
Turn.Turn right 90 degrees.A direction and an amount
Bake until done.Bake for 12 minutes at 180 degrees Celsius.A time, and a temperature with its unit
Do it a few times.Repeat these 2 steps 4 times.A count, and which steps repeat
Put it next to the big one.Put the red block on top of the blue block.Names instead of "it" and "the big one"

Look at the third row. "180 degrees Celsius" carries its unit with it. The Mars orbiter's numbers did not, and that is how two teams could read the same value in two different ways. Saying the unit is part of being exact.

Be the computer

The best way to test an algorithm is to follow it literally, doing only what each step says. Take a sheet of squared paper and put a dot in a square near the middle. Call that square S. Then follow these steps exactly.

  1. Stand on S, facing the top of the page.
  2. Move forward 3 squares.
  3. Turn right 90 degrees.
  4. Move forward 2 squares.
  5. Turn right 90 degrees.
  6. Move forward 3 squares.

Where did you end up? You can keep track in a table. Write your position as two numbers: squares across from S, where right counts as positive, and squares up from S. After each step, write down where you are and which way you face.

After stepAcrossUpFacing
100up
203up
303right
423right
523down
620down

You finish 2 squares to the right of S, facing down the page, having walked three sides of a rectangle. Step 1 mattered more than it looked. Start facing a different way and every later step lands somewhere else.

Now change one word. Make step 3 say "turn left" instead of "turn right", and follow the steps again. After step 3 you face left. Step 4 takes you to 2 squares left of S and 3 up. Step 5 says turn right, and turning right while facing left leaves you facing up. Step 6 then takes you 3 more squares up, to 2 left and 6 up. One word moved the finish 4 squares sideways and 6 squares up. Programs behave the same way. A single changed word can send them somewhere completely different, and they will not warn you.

A table like this is called a trace table. It records the state of things after each step: here, a position and a direction. You will use trace tables all through this course, because they let you check a program with a pencil before you trust it to a machine.

Where this course fits, and what you need

If you have done Computers and Coding for Kids on this site, you have met loops, if-then choices and debugging on paper. This course turns those ideas into real code that runs. After it comes High School Programming: Python from the First Line, which builds longer programs and moves faster. You do not need the earlier course to start here.

The course follows the main programming ideas that the K-12 Computer Science Framework lists for grades 6 to 8: algorithms, variables, control (loops, events and if-then choices), procedures you name and reuse, and the way a program is designed and developed. Here is the route.

ModuleWhat you doWhere
1Write and trace algorithmsPaper
2Sequences, loops, events, variables and decisions with blocksScratch
3 to 6The same ideas as typed code, then lists and functionsPython
7Debugging as a methodPython
8 and 9Projects: a drawing, a quiz game, a text adventure, two simulations and one of your ownPython

Everything runs in a web browser, with nothing to download or install. Scratch lives at scratch.mit.edu. For Python you will use the free Code Editor from the Raspberry Pi Foundation, at editor.raspberrypi.org. Neither tool makes you sign up to build and run things. Before you start Scratch, show the site to a parent or guardian, because Scratch's terms ask a grown-up to agree to them for anyone under 18. And one rule for the whole course: never type your real full name, address, school or phone number into a program you might share.

Common misconceptions

  • "The computer will work out what I meant." It cannot. It has only what you wrote. The orbiter's software read pound-seconds as newton-seconds because nothing told it otherwise.
  • "An algorithm has to be written in code." No. A clear recipe or a list of directions is an algorithm. Code is one way of writing an algorithm down so that a machine can follow it.
  • "Each problem has one correct algorithm." Most problems have many. Two people can write different steps that reach the same answer. Some will be shorter, faster or easier to check, which is why programmers compare them.
  • "If it worked once, it is right." One good run proves only that one run. The grid walk works in the middle of a big sheet and fails if S is near the edge, because "move forward 3 squares" walks off the page.

Where this leaves us

Key idea: An algorithm is a list of steps exact enough that the follower never has to guess, and a computer is the most literal follower there is.

You now have four checks to run on any list of steps: is each step exact, is it doable, are the steps in a working order, and is there a point where it stops? You have seen why units and starting positions are part of being exact. You have also traced an algorithm with a table, one step at a time, the way a computer would follow it. The next lesson takes the trace table further, with steps that choose between two paths and steps that repeat.

Sources

  1. Mars Climate Orbiter Mishap Investigation Board. (1999). Phase I report. National Aeronautics and Space Administration. (Launch on 11 December 1998 and loss on 23 September 1999; the root cause in the Small Forces ground software, which reported pound-seconds where newton-seconds were specified; the planned 226 km pass, the estimated 57 km, and the 80 km survival limit.) NASA
  2. O'Connor, J. J., and Robertson, E. F. (1999). Abu Ja'far Muhammad ibn Musa Al-Khwarizmi. MacTutor History of Mathematics, University of St Andrews. (The House of Wisdom in Baghdad, his dates, and the word algorithm coming from the Latin title of his book on Hindu-Arabic numerals.) University of St Andrews
  3. K-12 Computer Science Framework. (2016). Framework statements by concept: Algorithms and Programming. (The grade 6 to 8 subconcepts: algorithms, variables, control, modularity and program development.) k12cs.org
  4. Scratch Foundation. (2026). Scratch terms of service, last updated 22 January 2026. (The eligibility section: a user under 18 needs a parent or legal guardian to agree to the terms first.) Scratch Help Center
  5. Raspberry Pi Foundation. (2026). Code Editor. (Python in the browser with no setup, and code that runs without logging in; observed 16 October 2026.) editor.raspberrypi.org
Key terms
Algorithm
A list of steps that solves a problem, written so exactly that the follower never has to guess.
Program
An algorithm written in a language that a computer can run, such as Scratch or Python.
Input
What goes into an algorithm, such as a mark to be graded or two places for a route.
Output
What comes out of an algorithm, such as a grade or a list of turns.
Ambiguous
Having more than one possible meaning. 'Add a little water' is ambiguous; 'add 50 ml of water' is not.
Trace table
A table that records the state of things, such as a position or a value, after each step of an algorithm.

Tracing an Algorithm With a Table

  • Trace an algorithm that uses sequence, selection and repetition, recording every step in a table.
  • Choose test inputs, such as negative numbers, that can expose a hidden assumption in an algorithm.
  • Explain why halving the range finds a number from 1 to 100 in at most seven guesses.

Seven cards, one question

Seven cards lie face down in a row. Their numbers are 14, 3, 27, 9, 27, 5 and 18, but you do not know that yet. You may turn over one card at a time, and you may keep one number in your head. Which card has the largest number?

You could do this without thinking about it. The trouble is that "without thinking about it" is no use to a computer. Here is the same method written as an algorithm. It is set out in pseudocode: careful plain English, laid out like a program, but not in any real programming language. The words in capitals control what happens next.

  1. Turn over the first card. Remember its number as BIGGEST SO FAR.
  2. REPEAT for each card that is still face down:
    • Turn the card over.
    • IF its number is greater than BIGGEST SO FAR, remember its number as the new BIGGEST SO FAR.
  3. When no cards are left, say BIGGEST SO FAR.

Read it once more before going on, and try to predict the answer it will give. Predicting first, then checking, is a habit worth building now. A study in 13 schools with 493 students aged 11 to 14 taught programming with a method that starts every task with "predict what this will do", and those students did better on a later test than students taught without it.

Tracing it, row by row

To trace the algorithm, you follow it exactly and write down what you know after each card. The columns are the card's place in the row, the number on it, the answer to the IF question, and what BIGGEST SO FAR holds afterwards.

CardNumber on itGreater than BIGGEST SO FAR?BIGGEST SO FAR afterwards
114first card, so no question14
23no14
327yes27
49no27
527no: 27 is equal, not greater27
65no27
718no27

The output is 27. Two things in this table are worth a closer look.

First, most rows say "no", and you still write them. A "no" is information. It tells you the IF question was asked and answered, and that nothing changed. If a trace skips those rows, you cannot tell a step that ran from a step that was forgotten.

Second, look at card 5. It shows 27, the same as BIGGEST SO FAR, and 27 is not greater than 27. So the algorithm keeps the 27 it found on card 3. If step 2 had said "greater than or equal to", it would switch to card 5's 27 instead. The number would be the same. But if someone asked which card held the largest number, the two versions would give different answers. Two small words, "or equal", change the result.

Three ways to join steps

Look at how the steps fit together. Some steps simply follow each other: turn the card, then compare. Step 2 repeats. Inside the repeat, one step happens only IF something is true. Those three ways of joining steps are all this algorithm needs, and they are all any algorithm needs.

In May 1966, two Italian computer scientists, Corrado Böhm and Giuseppe Jacopini, published a mathematical proof about flow diagrams. In plain words, it showed that any algorithm drawn as a flow diagram can be rebuilt from three kinds of joint: steps in a row, a choice between two paths, and a repeat. Every program in this course, from a Scratch cat to a Python simulation, is built from these three.

StructureWhat it meansPseudocode wordIn ScratchIn Python
SequenceSteps run one after another, in ordernone: just the next lineBlocks stacked under each otherLines written one below another
SelectionA step runs only if a condition is trueIF, ELSEThe light orange 'if then' and 'if then else' blocksif, elif and else
RepetitionSteps run again and againREPEATThe light orange 'repeat', 'forever' and 'repeat until' blocksfor and while

A condition is a question with a yes or no answer, such as "is this number greater than BIGGEST SO FAR?" Selection uses a condition to pick a path. Repetition can use one too, to decide when to stop. You will meet all three structures as Scratch blocks in Module 2 and as Python in Modules 3 to 5.

A tidy change, and a wrong answer

Suppose someone tidies the algorithm up. Step 1 feels fussy, so they change it: "Set BIGGEST SO FAR to 0. Then REPEAT for every card: turn it over, and IF its number is greater than BIGGEST SO FAR, remember it." Try it on the seven cards. It still says 27. It looks like a harmless improvement.

Now try different input. Three cards hold the lowest temperatures from three winter nights, in degrees Celsius: -4, -9 and -2.

CardNumber on itGreater than BIGGEST SO FAR?BIGGEST SO FAR afterwards
startnonenone0
1-4no0
2-9no0
3-2no0

The output is 0, a number that is on none of the cards. The first version would have said -2. The tidy version quietly assumed that every number is at least 0. On the first set of cards that was true, so the test passed and hid the problem.

This is why programmers test with inputs chosen to break things. Inputs at the edges of what is possible are called edge cases. For a largest-number algorithm, good edge cases include all negative numbers, only one card, every card the same, and the largest card first or last. One passing test with friendly numbers proves very little.

Guess my number

Here is a game with a repeat that has no fixed count. A friend thinks of a whole number from 1 to 100. You guess, and they answer "too high", "too low" or "correct". This algorithm plays it.

  1. Set LOW to 1 and HIGH to 100.
  2. REPEAT until the guess is correct:
    • Guess the middle: add LOW and HIGH and halve the total. If that leaves a half, drop it, so 37.5 becomes 37.
    • IF the answer is "too high", set HIGH to the guess minus 1.
    • IF the answer is "too low", set LOW to the guess plus 1.

Trace it with the secret number 37.

Guess numberLOWHIGHGuessAnswerChange
1110050too highHIGH becomes 49
214925too lowLOW becomes 26
3264937correctstop

Three guesses. Now trace it with 83, which is less kind. The guesses go 50, 75, 88, 81, 84, 82 and finally 83: seven guesses. Each wrong guess throws away about half of the numbers still possible. One hundred becomes about 50, then 25, 12, 6, 3 and finally 1. After six halvings only one number can be left, so the seventh guess must hit it. If you try every secret from 1 to 100, none of them needs more than seven guesses. Guessing 1, 2, 3 and so on could take a hundred.

This halving method is called binary search, and it is one of the classic searching algorithms. Notice what kind of repeat it uses. The card algorithm repeats once for each card, so you know the count before you start. Guess my number repeats until the guess is correct, and nobody knows in advance how many rounds that will take. Scratch and Python both have a way to write each kind of repeat, and you will use both.

Common misconceptions

  • "Rows where nothing changes can be left out of a trace." Keep them. A row saying "no change" shows the step ran. Without it you cannot tell a step that ran from one you skipped by mistake.
  • "Starting BIGGEST SO FAR at 0 is a safe shortcut." Only if no number can be below 0. With winter temperatures it gives an answer that is not on any card.
  • "Equal counts as greater." "Greater than" means strictly larger. 27 is not greater than 27, and that decides which of two equal cards an algorithm keeps.
  • "Every repeat has to say how many times it runs." Some do, like "for each card". Others run until a condition comes true, like "until the guess is correct", and the number of rounds is only known at the end.

The short version

The point: A trace table lets you run an algorithm by hand, and the most useful traces are the ones you run on inputs chosen to break it.

Every algorithm is built from three joints: sequence, selection and repetition. Selection and some repeats depend on a condition, a yes or no question. You traced the largest-card algorithm and saw that "greater than" and "greater than or equal to" pick different cards. You saw a tidy change pass one test and fail on negative numbers, which is what edge cases are for. And you traced binary search, a repeat with no fixed count, which never needs more than seven guesses for 1 to 100. Next you meet the same three structures as coloured blocks in Scratch, where you can watch them run.

Sources

  1. Böhm, C., and Jacopini, G. (1966). Flow diagrams, Turing machines and languages with only two formation rules. Communications of the ACM, 9(5), 366-371. https://doi.org/10.1145/355592.365646 (Abstract read: flow diagrams reduced to a small set of base diagrams.)
  2. Wikipedia. (2026). Structured program theorem. (Background on the Böhm and Jacopini result stated as sequence, selection and iteration.) Wikipedia
  3. Sentance, S., Waite, J., and Kallia, M. (2019). Teaching computer programming with PRIMM: A sociocultural perspective. Computer Science Education, 29(2-3), 136-176. https://doi.org/10.1080/08993408.2019.1608781 (Predict, Run, Investigate, Modify, Make, evaluated in 13 schools with 493 students aged 11 to 14; the PRIMM group did better on the post-test than the control group.)
  4. Computer Science Education Research Group, University of Canterbury. (n.d.). Searching algorithms. CS Unplugged. (Sequential and binary searching taught without a computer.) CS Unplugged
  5. K-12 Computer Science Framework. (2016). Framework statements by concept: Algorithms and Programming. (Grades 6 to 8, control: programmers select and combine control structures such as loops, event handlers and conditionals.) k12cs.org
Key terms
Pseudocode
Steps written in careful plain English and laid out like a program, but not in any real programming language.
Sequence
Steps that run one after another, in the order they are written.
Selection
A step that runs only if a condition is true, written IF and ELSE in pseudocode.
Repetition
Steps that run again and again, either a set number of times or until a condition comes true.
Condition
A question with a yes or no answer, such as 'is this number greater than 27?'
Edge case
An input at the limits of what is possible, such as negative numbers or a single card, chosen to expose hidden assumptions.
Binary search
A way to find a value by guessing the middle and throwing away the half that cannot hold it, again and again.

Module 2: Block Code in Scratch

Scratch, the free block language, in the browser with no account: sprites, the stage and sequences, the three loop blocks and pen drawing, event scripts and messages, then variables and if-then-else in a small game, with sharing and remixing done safely and with credit.

Scratch: Sprites, the Stage and Your First Sequence

  • Open the Scratch editor without an account and find the block palette, code area, stage and sprite pane.
  • Build a script as a sequence of blocks under a hat block, and predict what it does before running it.
  • Explain why a stack of move blocks with no waits looks like a single jump, and fix it with wait or glide blocks.

A cat in the middle of the stage

Open a new Scratch project and the Scratch Cat is standing at x: 0, y: 0, the exact centre of a stage 480 steps wide and 360 steps tall. It faces direction 90, which means it points to the right. Everything you build in this module starts from that cat, standing still, waiting for instructions.

Scratch is a free programming language made for young people, and the Scratch help pages say it is designed especially for ages 8 to 16. You build programs by snapping blocks together, so you cannot make a typing mistake, and you can watch every instruction happen on the stage.

Before you start, show the Scratch website to a parent or guardian. Scratch's terms ask a grown-up to agree to them for anyone under 18. Then go to scratch.mit.edu and click Create at the top of the page. The editor opens straight away. You will see "Join Scratch" and "Sign in" at the top. You do not need either one for anything in this module.

Finding your way around

The editor has four main areas. Down the left side is the block palette, the shelf of blocks you can use. In the middle is the code area, where you drag blocks to build scripts. At the top right is the stage, where the sprites act out your program, with a green flag and a red stop sign above it. Below the stage is the sprite pane. It shows Sprite1, the cat, with boxes for its x, y, size and direction.

A sprite is any character or object that can run scripts. The palette sorts blocks into categories, each with its own colour. The colour tells you what kind of job a block does, which helps when you are hunting for one.

CategoryColourWhat its blocks doOne example
MotionblueMove, turn and place spritesmove 10 steps
LookspurpleSpeech bubbles, costumes, sizesay Hello! for 2 seconds
SoundpinkPlay soundsstart sound Meow
EventsyellowStart scripts when something happenswhen green flag clicked
Controllight orangeWaiting, repeating and choosingwait 1 seconds
Sensinglight blueNotice touches, keys and the mousekey space pressed?
OperatorsgreenArithmetic, comparisons and random numberspick random 1 to 10
Variablesdark orangeStore and change valueschange my variable by 1
My Blockspink-redBlocks you design yourselfMake a Block

Your first sequence

Build this script. Drag each block from the palette and drop it so that it clicks into place under the one before. White numbers in a block can be changed by clicking them and typing.

  1. The yellow when green flag clicked block, from Events. It has a rounded top, like a hat, because nothing can go above it.
  2. The blue go to x: 0 y: 0 block, from Motion. Set both numbers to 0.
  3. The purple say Hello! for 2 seconds block, from Looks.
  4. The blue move 10 steps block. Change 10 to 100.

Before you click anything, predict what will happen. Then click the green flag. The cat says Hello! in a speech bubble for 2 seconds, then appears 100 steps to the right, at x: 100. Click the flag again and the same thing happens again, starting from the centre.

The stack of blocks is a script, and the yellow block on top is a hat block: it waits for an event, here a click on the green flag, and then starts the blocks under it. The blocks run from top to bottom, one after another. That is a sequence, the first of the three structures from the last lesson. Swap the say block and the move block, and the cat jumps first and talks second. Order is part of the instructions.

Now pull the go to block out and click the flag three times. The cat starts each run from wherever the last one left it: x: 100, then 200, then on towards the right edge of the stage. Clicking the flag does not reset anything by itself. Setting up the starting state at the top of a script is called initialising, and it is the Scratch version of "stand on S, facing the top of the page".

The walk that looked like a jump

Here is a script meant to make the cat walk across the stage in four strides.

  1. when green flag clicked
  2. go to x: -200 y: 0
  3. move 50 steps
  4. move 50 steps
  5. move 50 steps
  6. move 50 steps

Click the flag and there is no walk. The cat is simply at x: 0, as if it had jumped there. All four moves happened, but you never saw the cat at -150, -100 or -50.

The reason is how Scratch draws. It runs scripts in short bursts and redraws the stage about 30 times a second. A loop pauses at the end of each pass so the stage can be redrawn, and a wait block pauses too. A plain stack with no loop and no wait does not pause at all, so it runs from top to bottom between two redraws. The stage only ever shows where the stack ended.

The first fix is to give the screen time. Put a light orange wait 1 seconds block, from Control, after each move, and change 1 to 0.2. Now the trace looks like this.

BlockCat's x afterwardsTime since the click
go to x: -200 y: 0-2000 seconds
move 50 steps-1500 seconds
wait 0.2 seconds-1500.2 seconds
move 50 steps-1000.2 seconds
wait 0.2 seconds-1000.4 seconds
move 50 steps-500.4 seconds
wait 0.2 seconds-500.6 seconds
move 50 steps00.6 seconds

You see four hops, each one held long enough to notice. Add the purple next costume block after each move and the cat swaps between its two drawings as it goes, which looks like legs moving.

The second fix is a different block. The blue glide 1 secs to x: 0 y: 0 block slides the sprite smoothly to a position over the time you give it, and the script waits until the glide is finished. One glide block replaces all four moves and four waits, if a smooth slide is what you want.

The walking script now has twelve blocks, and most of them say the same thing four times. Writing the same blocks again and again is exactly the job a loop does, and loops are the next lesson.

Blocks that wait, and blocks that do not

Some blocks take time and make the script wait until they finish. Others start something and move straight on. Mixing them up is the most common timing surprise in Scratch.

Waits until finishedStarts it and moves on
say Hello! for 2 seconds: the next block runs after 2 secondssay Hello!: the bubble appears and the next block runs at once, and the bubble stays up
play sound Meow until done: the next block runs when the sound endsstart sound Meow: the sound starts and the next block runs at once
glide 1 secs to x: 0 y: 0: the next block runs when the glide endsgo to x: 0 y: 0: the sprite is there instantly

Try this pair. Script A: say Hello! for 2 seconds, then move 100 steps. Script B: say Hello!, then move 100 steps. In A the cat talks, then jumps. In B the cat jumps immediately, still holding its speech bubble. Neither is wrong. They are different instructions, and you choose the one that matches what you mean.

Saving without an account

Click File in the menu bar at the top of the editor, then Save to your computer. Scratch downloads your project as a file ending in .sb3. To carry on later, open the editor, click File, then Load from your computer, and pick that file. This works without an account, and the project stays on your own computer where nobody else can see it.

Saving online and sharing projects on the Scratch website need an account. If you want one later, ask a parent or guardian to help you set it up. Lesson 6 covers sharing and how to do it safely.

Common misconceptions

  • "Four move blocks make the cat walk." They make it jump. With no wait or loop between them, the whole stack runs before the stage is redrawn.
  • "The green flag puts everything back where it started." It only starts scripts. If your script does not set the starting position, the cat begins wherever it was left.
  • "Blocks run in the order I dragged them in." They run from top to bottom in the order they are stacked, however you built the stack.
  • "Any block in the code area runs when I click the flag." Only blocks attached under a hat block run when its event happens. A loose block just sits there, though clicking on it runs it once.

What you now know

Remember: A script runs its blocks from top to bottom, and a stack with no wait finishes before the stage is redrawn, so you only see where it ends.

You can open Scratch with no account, find the palette, code area, stage and sprite pane, and tell a block's job from its colour. You built a sequence under a yellow hat block and saw that changing the order changes the result. You set a starting position at the top of the script, so every run begins the same way. You fixed a walk that looked like a jump with waits or with a glide, and you know which blocks wait and which move straight on. And you can save a project to your own computer without signing up for anything.

Sources

  1. Scratch Foundation. (2024). What is Scratch, and what can I do with it? Scratch Help Center. (Designed especially for young people ages 8 to 16.) Scratch Help Center
  2. Scratch Foundation. (2026). Scratch Foundation home page. ("Scratch is free for everyone"; Scratch moved from MIT to the Scratch Foundation, an independent nonprofit.) scratchfoundation.org
  3. Scratch Foundation. (2024). Why can't I save my project? Scratch Help Center. (File, then Save to your computer, saves a copy on your own computer.) Scratch Help Center
  4. Scratch Wiki contributors. (2026). Coordinate system and Direction (value). Scratch Wiki. (The 480 by 360 stage with (0, 0) at the centre; x from -240 to 240 and y from -180 to 180; direction 90 pointing right as the default.) Scratch Wiki
  5. Scratch Foundation. (2026). scratch-vm source code: runtime.js, sequencer.js and scratch3_looks.js. GitHub. (A stage of 480 by 360; scripts stepped 30 times a second in the compatibility mode the editor uses; scripts keep running until a redraw is requested; say for seconds waiting until its time is up.) GitHub
  6. Scratch Wiki contributors. (2026). Glide () secs to x: () y: () (block). Scratch Wiki. (Moves a sprite smoothly over the given time; the script waits until it finishes.) Scratch Wiki
  7. Maloney, J., Resnick, M., Rusk, N., Silverman, B., and Eastmond, E. (2010). The Scratch programming language and environment. ACM Transactions on Computing Education, 10(4), article 16. https://doi.org/10.1145/1868358.1868363 (Abstract read: a visual language for learners primarily aged 8 to 16, built to support learning by tinkering.)
Key terms
Sprite
A character or object on the Scratch stage that can run scripts, such as the cat called Sprite1.
Stage
The area where sprites act out a program: 480 steps wide and 360 tall, with x: 0, y: 0 at the centre.
Script
A stack of blocks that runs from top to bottom when its hat block's event happens.
Hat block
A block with a rounded top, such as 'when green flag clicked', that starts the script beneath it.
Sequence
Instructions that run one after another in the order they are written or stacked.
Initialising
Setting up the starting state, such as the sprite's position, at the beginning of a script.
.sb3 file
The file Scratch saves when you choose File, then Save to your computer. Load it again with Load from your computer.

Repeat, Forever and Repeat Until

  • Choose between Scratch's repeat, forever and repeat until blocks by asking how the loop should stop.
  • Draw regular shapes with the Pen extension, using 360 divided by the number of sides as the turn.
  • Trace a loop inside a loop and count how many times its innermost blocks run.

Eight blocks for one square

Drawing a square in Scratch takes eight blocks if you write every step: move 100 steps, turn 90 degrees, and the same pair three more times. Eight blocks is not terrible. But a hexagon needs twelve, and a shape with 36 sides needs 72, nearly all of them copies. Copies are also where mistakes hide: change the size of the square and you must find and fix four separate move blocks.

A loop says it once and repeats it. The light orange Control category has three loop blocks, and choosing between them is mostly a matter of answering one question: how should this loop stop?

Getting a pen

To see shapes, the sprite needs to draw as it moves. Click the Add Extension button in the bottom left corner of the editor and choose Pen. A new Pen category appears in the palette, with blocks such as erase all, pen down, pen up, set pen color to and change pen size by. While the pen is down, the sprite leaves a line wherever it goes.

Here is the square with a loop. Every block is described by its colour and words, so you can find it in the palette.

  1. yellow: when green flag clicked
  2. Pen: erase all
  3. blue: go to x: 0 y: 0
  4. blue: point in direction 90
  5. Pen: pen down
  6. light orange: repeat 4, and inside its C-shaped mouth:
    • blue: move 100 steps
    • blue: turn 90 degrees, using the turn block with the clockwise arrow
  7. Pen: pen up

The loop holds two blocks and runs them four times. Here is the trace, writing the cat's position and direction at the end of each pass. Remember that direction 90 means pointing right and 180 means pointing down.

PassxyDirection after the turn
start0090 (right)
11000180 (down)
2100-100-90 (left)
30-1000 (up)
40090 (right)

The square sits below and to the right of the centre, and the cat ends where it began, facing the way it started. To change the size, you now change one number instead of four.

Something else changed too. In the last lesson, four move blocks in a row looked like one jump. Inside a loop, Scratch pauses at the end of every pass so the stage can be redrawn. That is why you can watch the square being drawn, side by side, even though there is no wait block.

Three loops, side by side

BlockHow many passesWhen it stopsGood for
repeat 10The number you type in it, known before it startsAfter that many passesShapes, counting, an animation of a set length
foreverNo limitNever by itself: only the red stop sign or a stop block ends itGames and anything that keeps checking or moving
repeat until, with a conditionUnknown in advanceWhen its condition becomes true, checked before each passWaiting for something to happen, such as reaching a wall

Look closely at the forever block in the palette. It has no bump on its bottom edge, so nothing can snap underneath it. That is not a quirk. A forever loop never finishes, so a block placed below it could never run, and Scratch simply does not let you put one there.

Repeat until has a six-sided gap for its condition. You fill it with a pointed block, such as the light blue Sensing block touching mouse-pointer?, whose menu also offers edge. The loop checks the condition first and only runs the blocks inside if it is still false. So if the condition is already true when the loop is reached, the inside runs zero times and the script moves straight on.

The 360 rule for any shape

Why 90 degrees for a square? Follow the cat round. It starts facing right and finishes facing right, so in total it has turned exactly once round: 360 degrees. Four equal turns that add up to 360 must be 90 each. The same reasoning works for any regular shape, so the turn is always 360 divided by the number of sides.

ShapeSides: the repeat numberTurn at each cornerTotal turning
Triangle3120360
Square490360
Pentagon572360
Hexagon660360
Octagon845360
Almost a circle3610360

The triangle is the one people get wrong. Its inside angles are 60 degrees, so 60 feels right. But the cat does not turn by the inside angle. It turns by how far it must swing away from the line it was walking along, and at a triangle's corner that is 120. Turn by 60 three times and the cat has only turned 180 degrees, half a lap, so the shape never closes. You do not even need to work out the division yourself: drop the green Operators block for division into the turn block and type 360 and 3 into it.

A loop inside a loop

Put one loop inside another and you get patterns that would take hundreds of blocks to write by hand. This script draws a flower of squares:

  1. when green flag clicked, erase all, go to x: 0 y: 0, pen down
  2. repeat 12, and inside it:
    • repeat 4, and inside that: move 60 steps, turn 90 degrees
    • turn 30 degrees

The outer loop runs 12 times. Each time, the inner loop draws a whole square, and then the cat turns 30 degrees before the next one. Twelve turns of 30 make 360, so the twelfth square lands just before the first, and the flower closes into a full circle of overlapping squares around the centre.

How many times does the move block run? Trace the counts rather than every position.

BlockRuns per outer passOuter passesTotal runs
move 60 steps41248
turn 90 degrees41248
turn 30 degrees11212

Inner counts multiply by outer counts: 4 times 12 is 48. Students often add them and get 16. Multiplying is right because the whole inner loop happens again on every outer pass.

Forever, with a way out

Some things should go on until the player stops them. Try this: when green flag clicked, then forever with two blue Motion blocks inside, move 10 steps and if on edge, bounce. The cat runs to the right wall, turns round and runs back, again and again. It flips upside down on the way back, because it is now pointing left. Add the blue set rotation style left-right block before the loop and it simply faces the other way instead.

This loop will never end on its own. Click the red stop sign above the stage to stop every script. Later you will meet the stop block, which a script can use to end itself when, say, a game is lost.

Now compare a repeat until version: go to x: -200 y: 0, point in direction 90, then repeat until touching edge?, with move 5 steps inside. The cat walks right in small steps and stops when it reaches the wall, and the script carries on to whatever comes after the loop. Forever never reaches the blocks after it. Repeat until does, the moment its condition is true.

Common misconceptions

  • "A forever loop stops by itself eventually." It never does. Only the stop sign or a stop block ends it, which is why nothing can be attached under it.
  • "Repeat until keeps going until the condition is false." The opposite: it stops when the condition becomes true. Python's while loop, later in the course, works the other way round, and mixing them up is a classic slip.
  • "A triangle needs turns of 60 degrees." The turn is 360 divided by 3, which is 120. Sixty is the inside angle, and three turns of 60 leave the shape open.
  • "A loop inside a loop runs the inner blocks inner plus outer times." It runs them inner times outer times: 4 times 12 is 48, not 16.

Pulling it together

In short: Pick a loop by how it should stop: repeat for a known count, repeat until for a condition, and forever for something that should run until the player stops it.

You added the Pen extension and drew a square with one loop instead of eight blocks, and you saw that a loop pauses each pass so the drawing is visible. You can work out the turn for any regular shape as 360 divided by the number of sides, and you know why 60 fails for a triangle. You traced a loop inside a loop by multiplying the counts. And you know that repeat until checks its condition first, stops when it becomes true, and may run zero times. The next lesson adds the other half of a game: scripts that wait for a key, a click or a message before they start.

Sources

  1. Scratch Wiki contributors. (2026). Pen extension. Scratch Wiki. (The Pen blocks, including erase all, pen down, pen up, set pen color to and change pen size by, enabled from the extensions section of the editor.) Scratch Wiki
  2. Scratch Wiki contributors. (2026). Forever (block). Scratch Wiki. (A C block and cap block with no bump at the bottom, ended only by the stop sign or a stop block.) Scratch Wiki
  3. Scratch Foundation. (2026). scratch-vm source code: scratch3_control.js. GitHub. (The repeat block runs one pass per frame; repeat until starts its inner blocks only while its condition is false.) GitHub
  4. Scratch Wiki contributors. (2026). Direction (value). Scratch Wiki. (0 is up, 90 right, 180 down and -90 left; the turn blocks change direction clockwise or anticlockwise.) Scratch Wiki
  5. Papert, S. (1980). Mindstorms: Children, computers, and powerful ideas. Basic Books. (Learning geometry by steering a turtle round a closed path, the idea behind the 360 rule.)
Key terms
Loop
A block or statement that runs the instructions inside it again and again.
repeat
The Scratch loop that runs its inside a fixed number of times, such as repeat 10.
forever
The Scratch loop that never ends by itself; only the stop sign or a stop block ends it.
repeat until
The Scratch loop that checks a condition before each pass and stops when it becomes true.
Pen extension
Optional Scratch blocks, added with the Add Extension button, that let a sprite draw lines as it moves.
Nested loop
A loop inside another loop. The inner blocks run inner count times outer count times.
360 rule
For a regular shape, the turn at each corner is 360 divided by the number of sides.

Events and Messages: Scripts That Wait

  • Use event hat blocks to start scripts from the green flag, a key, a click or a message.
  • Explain how several scripts started by events run side by side, and use broadcast and wait when order matters.
  • Choose between a key-press hat block and a forever loop that checks 'key pressed?' for smooth movement.

A program that sits and waits

Press the space bar in a Scratch project and nothing happens, unless some script is waiting for exactly that key. Every program so far has started when you clicked the green flag and then run from top to bottom. A game cannot work like that. It has to sit and wait for the player, then react to whatever they do, in whatever order they do it.

Something that happens while a program runs, such as a key press, a mouse click or a message from another sprite, is called an event. A program built from scripts that each wait for an event is called event-driven. Almost every app you use works this way. A phone's camera app does nothing until you tap the button, and then the tap starts a script.

The yellow hat blocks

In Scratch, events start scripts through the yellow hat blocks in the Events category. Each one waits for one kind of event.

Hat blockIts script starts whenTypical use
when green flag clickedThe green flag above the stage is clickedSetting things up at the start
when space key pressedThe chosen key is pressed: space, an arrow, a letter, a digit, or any keyJumping, firing, pausing
when this sprite clickedSomeone clicks on this spriteButtons, things to catch or pop
when backdrop switches toThe stage changes to the chosen backdropStarting a new level or scene
when loudness > 10The sound level or the timer goes over a numberClap-controlled projects, time limits
when I receive message1Any sprite or the stage broadcasts that messageOne sprite telling others what happened

A sprite can have any number of scripts, each under its own hat block. The stage can have scripts too: click the stage's panel at the right of the sprite pane to see its code area.

Building a scene, one script at a time

Here is a small scene, built in the order you would build it. The cat stands on the ground, a balloon drifts around the stage, the space bar makes the cat jump, and clicking the balloon pops it. Add the balloon with the Choose a Sprite button and pick Balloon1 from the library. It comes with a sound called Pop.

Script 1, the cat. when green flag clicked, then go to x: -150 y: -100, then say "Press space to jump. Click the balloon!" for 2 seconds. This is setting up, as in the last two lessons.

Script 2, the balloon. when green flag clicked, then show, then go to x: 150 y: 100, then forever, with glide 1 secs to random position inside. The balloon now wanders the stage for as long as the project runs.

Script 3, the cat. when space key pressed, then change y by 60, then wait 0.3 seconds, then change y by -60. That is a jump: up, a pause at the top, and back down.

Script 4, the balloon. when this sprite clicked, then start sound Pop, then hide, then broadcast popped. To make the message, open the menu on the broadcast block, choose New message, and type popped.

Script 5, the cat. when I receive popped, then say You got it! for 2 seconds.

Click the flag and play. Here is the whole project in one table. Reading a project this way, by event, is how you find your way round someone else's code.

SpriteWaits forThen does
Catgreen flaggoes to the start, gives instructions
Balloongreen flagshows itself, then wanders forever
Catspace keyjumps
Balloona click on itselfpops, hides, broadcasts popped
Catthe message poppedsays You got it!

Notice what you did not write anywhere: an order. Nothing says the jump comes before the pop. The player decides, by pressing keys and clicking. If they click the balloon while the cat is still giving instructions, the balloon pops at once, because Script 4 does not wait for Script 1. Each script answers its own event.

Many scripts at once

When you click the flag, Scripts 1 and 2 both start. Script 2 contains a forever loop, and Script 1 still runs. Scratch takes turns between running scripts so quickly that they look simultaneous. Each loop pass and each wait is a point where Scratch can switch to another script, which is how a forever loop in one script does not freeze everything else.

That raises a problem. When two scripts start from the same event, which one gets each turn first is hard to see and easy to change by accident. So never build a project that depends on one script beating another to the finish. If one thing must be complete before another starts, say so with a message.

broadcast and wait is the tool for that. Plain broadcast sends its message and carries straight on. Broadcast and wait sends the message and then pauses the sending script until every script started by that message has finished. Suppose the stage shows a title, then says "Level 1", then starts the level. Writing broadcast show title and wait, then broadcast start level, makes sure the title has finished before the level begins.

Smooth movement: asking instead of waiting

Try moving the cat with a hat block: when right arrow key pressed, then change x by 10. Hold the right arrow down. The cat moves once, pauses, and then moves in a stream. That pause is your computer's key-repeat delay. The hat block only fires when the computer reports a key press, and a held key is reported once, then again after a delay, then over and over. The Scratch Wiki warns against projects that depend on key repeat, because the delay varies from one computer to another.

The smoother way turns the question round. Instead of waiting to be told, the cat keeps asking. Under when green flag clicked, put a forever loop. Inside it put a light orange if then block, and in its six-sided gap put the light blue Sensing block key right arrow pressed?. Inside the if, put change x by 5. Now on every pass of the loop, about 30 times a second, the script asks whether the key is down, and moves if it is. There is no delay and no stutter. You will meet the if block properly in the next lesson; here it does one job, asking a yes or no question.

when right arrow key pressedforever, if key right arrow pressed?
Who starts the actionThe key press eventThe loop, by checking
Holding the keyOne move, a pause, then repeatsSmooth movement on every pass
Best forOne-off actions: jump, pause, fireContinuous movement

Common misconceptions

  • "Once a forever loop starts, no other script can run." Every running script gets turns. A forever loop in the balloon does not stop the cat from jumping.
  • "Scripts run in the order they appear in the code area." Each script runs when its event happens. Where it sits in the code area, or which sprite it belongs to, does not set an order.
  • "Broadcasting is how a sprite talks to the player." A broadcast is invisible. It is a message between scripts inside your project, not something shown on the screen and not something sent over the internet.
  • "The hat block is the best way to handle a held key." For one-off actions it is. For movement, a forever loop that checks key pressed? is smoother, because it does not depend on key repeat.

Summing up

Why this matters: Games, apps and websites are mostly waiting. Writing a program as a set of scripts that each answer one event is how you build something a person can interact with.

You met the yellow event hat blocks and built a scene from five scripts, none of which states an order, because the player supplies the order. You saw that scripts started together take turns so quickly that they look simultaneous, and that you should not rely on which one goes first. Broadcast sends a message to every when I receive script, and broadcast and wait also waits for them to finish. And you compared waiting for a key event with a loop that keeps asking whether the key is down. The next lesson brings in the last two big ideas from Module 1: values that change, held in variables, and choices made with if.

Sources

  1. Scratch Wiki contributors. (2026). When () key pressed (Events block). Scratch Wiki. (The keys that can be chosen; repeated firing depends on the operating system's key-repeat settings; a forever loop with key pressed? is often used for smoother movement.) Scratch Wiki
  2. Scratch Wiki contributors. (2026). Broadcast () and wait (block). Scratch Wiki. (Waits until all scripts activated by the broadcast have ended.) Scratch Wiki
  3. Scratch Foundation. (2026). scratch-gui sprite library, sprites.json. GitHub. (The Balloon1 sprite and its Pop sound.) GitHub
  4. K-12 Computer Science Framework. (2016). Framework statements by concept: Algorithms and Programming. (Grades 6 to 8, control: loops, event handlers and conditionals combined by programmers.) k12cs.org
  5. Scratch Foundation. (2026). scratch-vm source code: sequencer.js. GitHub. (Running scripts are stepped in turns, and a script gives up its turn at a loop pass or a wait.) GitHub
Key terms
Event
Something that happens while a program runs, such as a key press, a click or a message, that can start a script.
Event-driven program
A program built from scripts that each wait for an event and then react to it.
broadcast
A Scratch block that sends a message to every sprite and the stage, starting their 'when I receive' scripts.
broadcast and wait
Like broadcast, but the sending script pauses until every script started by the message has finished.
when I receive
The hat block that starts a script when a particular message is broadcast.
Key repeat
The way a computer reports a held key once, then again after a delay, then over and over.

Variables, Decisions and Sharing in Scratch

  • Create a variable, set it at the start of a script and change it as the program runs.
  • Use if then and if then else blocks with conditions, and explain why a check must sit inside a loop to keep working.
  • Share or remix a Scratch project safely: with a grown-up's help, nothing personal, and credit for others' work.

Four parts of a catch game

A catch game needs four things: an apple that falls, a bowl that moves, a score, and a rule that joins them together. You already have the first two from earlier lessons: loops to make things move, and events to start them. This lesson adds the last two, a value that changes and a decision, and builds the game one step at a time. Test after every step. If something breaks, you will know it was the step you just added.

Start a new project. Delete the cat by right-clicking it in the sprite pane and choosing delete. Then use Choose a Sprite to add two sprites from the library: Apple and Bowl.

Step 1: a bowl that follows the mouse

Give the Bowl this script: when green flag clicked, go to x: 0 y: -150, then forever, with set x to inside it. Into the gap in set x to, drop the light blue Sensing block mouse x, which reports how far across the stage the mouse pointer is. On every pass of the loop, the bowl copies the mouse's x position, so it slides left and right along the bottom of the stage as you move the mouse.

Step 2: an apple that falls

Give the Apple: when green flag clicked, go to x: 0 y: 170, then forever, with change y by -5 inside. Click the flag and the apple falls from the top of the stage. When it reaches the bottom it just stays there, stuck. It needs a rule: when you reach the bottom, go back to the top.

That rule is a decision, and decisions use the light orange if then block. Its six-sided gap holds a condition, a block that answers true or false. Inside the forever loop, after change y by -5, add: if (y position) < -170 then go to x: 0 y: 170. The less-than block is in the green Operators category, and y position is a round blue Motion block that reports the apple's current height.

Now the apple falls, reaches the bottom, and jumps back to the top, forever. Every time it falls in the same column, which is boring. Replace the 0 in the go to block with the green pick random -220 to 220 block. Now each fall starts from a different place across the top.

Step 3: a variable called score

A game needs to remember how many apples you have caught. A variable is a named place to store one value that can change while the program runs. Open the dark orange Variables category and click Make a Variable. Type the name score, leave it set to For all sprites, and click OK. A small box, called a monitor, appears on the stage showing score and its value.

Two blocks do most of the work. set score to 0 puts a value into the variable, replacing whatever was there. change score by 1 adds to the value already there. Put set score to 0 at the top of the Apple's green flag script, before the loop. That is initialising again: without it, a new game would start from the last game's score.

Step 4: the rule that joins them

Inside the Apple's forever loop, add a second if: if touching Bowl? then change score by 1, then go to x: (pick random -220 to 220) y: 170. The light blue touching block has a menu that lists the other sprites, so pick Bowl from it. Here is the whole Apple script.

  1. when green flag clicked
  2. set score to 0
  3. go to x: (pick random -220 to 220) y: 170
  4. forever:
    • change y by -5
    • if touching Bowl? then: change score by 1, and go to x: (pick random -220 to 220) y: 170
    • if (y position) < -170 then: go to x: (pick random -220 to 220) y: 170

Play it. Catch three apples and miss one, and the score changes like this.

What happensBlock that runsscore afterwards
Green flag clickedset score to 00
First apple lands in the bowlchange score by 11
Second apple lands in the bowlchange score by 12
Third apple falls past the bowlneither score block2
Fourth apple lands in the bowlchange score by 13

Step 5: faster, and an ending

The if then else block chooses between two paths: one set of blocks if the condition is true, the other if it is false. Exactly one of the two runs. Replace change y by -5 with: if score > 5 then change y by -8, else change y by -5. Now the apples fall faster once you have more than five, and on each pass the apple makes exactly one of the two moves.

To end the game, make a second variable called misses, set it to 0 at the start, and add change misses by 1 inside the if that catches an apple at the bottom. Then add one more if inside the loop: if misses = 3 then say Game over! for 2 seconds, then the light orange stop all block. The equals block is green, in Operators.

Move one block, break the game

Here is a mistake worth making on purpose. Drag the if touching Bowl? block, with everything inside it, out of the forever loop and drop it just above the loop, after the go to block. Run the game. The apple still falls, and it still lands in the bowl. The score never changes.

Trace it. The if block now runs once, before the loop starts. At that moment the apple is at the top of the stage, nowhere near the bowl, so the answer is no. Then the script enters the forever loop and never comes back up. The question "am I touching the bowl?" was asked exactly once, at the worst possible time.

Where the if block isHow often it asksScore when the apple lands
Inside the forever loopOn every pass, about 30 times a secondGoes up
Above the loopOnce, when the flag is clickedStays at 0

An if block does not keep watching. It checks its condition once, at the moment the script reaches it, and then the script moves on. Anything that must be checked again and again has to sit inside a loop. Put the block back inside the loop and the score works again.

Sharing, remixing and credit

Projects saved to your computer stay private. Sharing on the Scratch website needs an account, and if you want one, ask a parent or guardian to help set it up. When you share a project, Scratch's terms let other Scratchers use and change it inside Scratch, so share only what you are happy for others to build on.

Before you share, check the project the way a stranger would see it. The Scratch Community Guidelines say to keep personal and contact information private, and they list examples: real last names, phone numbers, addresses, hometowns, school names, email addresses, and links to social media. Check sprite names, costume drawings, sounds you recorded and the project's notes too, not just the code. Use a made-up name for any character.

Every shared project has a green Remix button. Remixing makes your own copy of someone else's project to change. The guidelines encourage it, as long as you give credit to everyone whose work you used and make a meaningful change. When you share a remix, Scratch adds a "Thanks to" line linking to the original. Write your own credit in the Notes and Credits box as well, saying what you took and what you changed. The same goes for sprites, sounds or music copied from other projects.

Comments are where kindness shows. The guidelines ask you to say something you like about a project and offer suggestions, not attacks. If a comment on your project is mean, do not argue with it. Use the Report button and let the Scratch Team deal with it.

Common misconceptions

  • "A variable remembers every value it has had." It holds one value at a time. set replaces it, and change adds to it. Old values are gone unless you store them somewhere else.
  • "change score by 1 sets the score to 1." It adds 1 to whatever the score already is. set score to 1 is the block that makes it exactly 1.
  • "An if block keeps watching for its condition." It checks once, when the script reaches it. That is why the touching check only works inside the forever loop.
  • "Anything shared on Scratch is free to copy without credit." The terms allow remixing, but the Community Guidelines ask you to credit everyone whose work you used and to make a meaningful change.

Looking back

The upshot: A variable lets a program remember, and an if lets it choose. Put the check inside the loop so it is asked on every pass.

You built a complete game from five tested steps: a bowl that follows the mouse, a falling apple, a score variable, a touching rule, and an ending. You used if then for a single choice and if then else to pick exactly one of two moves. You saw that moving one if block out of the loop leaves the score stuck at 0, because the question is asked only once. And you know how to share and remix safely: a grown-up helps with any account, nothing personal goes in a project, and other people's work gets credit. That completes Scratch. Every idea in this module comes back in the next one, typed as Python.

Sources

  1. Scratch Foundation. (2026). Scratch community guidelines. (Keep personal and contact information private, with the examples listed; give helpful feedback; credit everyone whose work you used and make a meaningful change; use Report instead of fighting.) Scratch
  2. Scratch Foundation. (2024). Remixing: What is it and why can every project on Scratch be remixed? Scratch Help Center. (Remixing as copying and changing a project, with credit to the original creator.) Scratch Help Center
  3. Scratch Wiki contributors. (2026). Remix. Scratch Wiki. (The green Remix button and the automatic "Thanks to" note linking a shared remix to the original.) Scratch Wiki
  4. Scratch Foundation. (2026). Scratch terms of service, last updated 22 January 2026. (Accounts needed for some features; a parent or guardian must agree for users under 18; sharing licenses other users to use and change content within Scratch.) Scratch Help Center
  5. Scratch Foundation. (2026). scratch-gui sprite library, sprites.json. GitHub. (The Apple and Bowl sprites used in the game.) GitHub
Key terms
Variable
A named place that stores one value, such as a score, which can change while the program runs.
set and change
set score to 0 replaces the value; change score by 1 adds to the value already there.
Monitor
The small box on the Scratch stage that shows a variable's current value.
if then
A block that runs the blocks inside it only if its condition is true, checked once each time the script reaches it.
if then else
A block that runs one group of blocks if its condition is true and the other group if it is false.
Remix
Your own changed copy of someone else's shared Scratch project, which should credit the original.
Notes and Credits
The box on a Scratch project page for explaining the project and crediting other people's work.

Module 3: The Same Ideas, Typed: Python Basics

The move from blocks to typed Python in a free browser editor that needs no install and no account: printing output, how Scratch blocks map onto Python lines, variables and the assignment sign, the main data types, and programs that ask questions with input and turn the answers into numbers.

From Blocks to Text: Your First Python Program

  • Open the Raspberry Pi Foundation Code Editor, write a program that prints text, and run it without an account.
  • Match familiar Scratch blocks to their Python equivalents, including print, time.sleep and the structures to come.
  • Read a first Python error message and use its type and line number to find the mistake.

A language named after a comedy show

During the Christmas holidays of 1989, a programmer named Guido van Rossum, who worked at a research institute in the Netherlands called CWI, had time on his hands and started building a new programming language. He was also reading the scripts of Monty Python's Flying Circus, a BBC comedy series from the 1970s. He wanted a name that was short, unique and slightly mysterious, so he called the language Python. In February 1991 he posted it online for other people to use.

Python is the language you will use for the rest of this course. Everything you learned in Scratch still applies. A program is still a sequence of instructions, with loops, choices, variables and events. What changes is that you type each instruction as text instead of snapping a block into place.

Opening the Code Editor

You will write Python in the Code Editor from the Raspberry Pi Foundation, a charity that makes free computing resources for young people. It runs in your web browser, so there is nothing to download or install.

  1. Go to editor.raspberrypi.org. The page says "Start coding, no setup required!"
  2. Under "What do you want to code with?", choose Python.
  3. A project opens with one file, called main.py. The middle of the screen is where you type. Below it is a Run button. On the right is the Text output panel, where your program's results appear.

You do not need to log in to write or run code. The editor keeps your work for the current browser session, and the Download button saves a copy to your computer. There is also a "Log in to save" button. That is optional. A Raspberry Pi Foundation account lets you save projects online, and anyone under 13 needs a parent or guardian to set it up, using the grown-up's email address. Everything in this course works without one.

This course uses this editor for a reason worth knowing: Trinket, another browser editor that its makers say millions of learners and educators used, shut down on 31 August 2026. Tools come and go. The Python you learn does not depend on any one of them.

Your first program

Click in main.py and type this line exactly, including the brackets and the quote marks.

print("Hello, world!")

Before you click Run, predict what the output panel will show. Then run it.

Hello, world!

print is a function: a named command that Python already knows. The brackets after it hold what you give it to work with. The text inside the quote marks is called a string, which is the programming word for a piece of text. The quote marks show where the string starts and ends, and they are not printed. Notice that the output has no quote marks and no brackets: print shows the value, not the code that made it.

Now a longer program. Each line runs in turn, from the top, just like a stack of blocks.

print("Hello!")
print("My name is Pixel.")
print("I have", 3, "lives left.")
print()
print("Game over")
Hello!
My name is Pixel.
I have 3 lives left.

Game over

Line 3 gives print three things, separated by commas: a string, the number 3, and another string. print shows them in order with one space between each. Line 4, an empty print, prints an empty line. The number 3 has no quote marks, because it is a number and not text. You will see why that difference matters in the next two lessons.

Scratch and Python, side by side

Most Scratch blocks you know have a Python version. You will meet each of these properly in the lesson shown, but it helps to see the whole map now.

Scratch blockPythonLesson
when green flag clickedClicking Run: the program starts at line 1this one
say Hello!print("Hello!")this one
blocks stacked in orderlines written one below anotherthis one
wait 1 secondstime.sleep(1), after import timethis one
set score to 0score = 08
change score by 1score = score + 18
ask and wait, then answerinput()9
if then, if then elseif, else10 and 11
repeat 10for count in range(10):12
forever, repeat untilwhile13
Make a Blockdef15
the red stop signthe Stop button that replaces Run while a program runs13

Here is the wait block in action, in a countdown. The first line is a comment, explained below. import time loads Python's time module, a set of ready-made tools, and time.sleep(1) pauses the program for one second, like Scratch's wait 1 seconds.

# A countdown, like a Scratch script with wait blocks
import time

print("3")
time.sleep(1)
print("2")
time.sleep(1)
print("1")
time.sleep(1)
print("Go!")
3
2
1
Go!

The four lines appear one at a time, a second apart. Without the sleep lines, all four would appear at once, for the same reason Scratch's four move blocks looked like one jump.

What blocks did for you

Scratch's designers wrote that its blocks are shaped to fit together only in ways that make sense. A condition is six-sided and only fits a six-sided gap. A loop is C-shaped to show that blocks go inside it. So in Scratch you could never type a misspelled word or leave a bracket open. In Python you can, and everyone does.

That is the trade. A 2017 study compared two high school classes using blocks and text versions of the same environment for five weeks. Students using blocks learned more and were more interested in further computing courses. Students using text felt their work was closer to what professional programmers do. This course uses both, in that order: blocks to learn the ideas, then text to use them.

Typing code means paying attention to four things that blocks handled for you.

  • Spelling. print works and pirnt does not.
  • Capital letters. Python is case-sensitive: print and Print are different names, and only one of them exists.
  • Pairs. Every opening bracket needs a closing one, and every opening quote mark needs a matching closing one.
  • Indentation. Some lines must be pushed in by four spaces. You will meet this in the lesson on if.

Comments: notes for humans

A line that starts with # is a comment. Python ignores everything from the # to the end of the line. Comments are notes for people reading the code, including you, next week.

# This program greets the player
print("Welcome to the game!")   # this line runs first
print('Single quotes work too.')
print("It's fine to put an apostrophe inside double quotes.")
Welcome to the game!
Single quotes work too.
It's fine to put an apostrophe inside double quotes.

A string can use double quotes or single quotes, as long as both ends match. Double quotes are handy when the text itself contains an apostrophe, as in the last line.

Your first error messages

Sooner or later Python will refuse to run something. Here are four real mistakes and exactly what Python says about each one. In each case the program was saved as main.py and run.

print("Hello)
  File "main.py", line 1
    print("Hello)
          ^
SyntaxError: unterminated string literal (detected at line 1)
print("Hello"
  File "main.py", line 1
    print("Hello"
         ^
SyntaxError: '(' was never closed
Print("Hello")
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    Print("Hello")
    ^^^^^
NameError: name 'Print' is not defined. Did you mean: 'print'?
print "Hello"
  File "main.py", line 1
    print "Hello"
    ^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?

Read an error message from the bottom up. The last line says what kind of error it is and describes it. A SyntaxError means Python could not even understand the line, like a sentence with a missing word. A NameError means Python understood the line but met a name it does not know. Above that, the message gives the file and the line number, then shows the line itself, with ^ marks pointing at where Python noticed the trouble.

The Code Editor shows the same facts squeezed into one line, plus a plain-English hint. For a misspelled print on line 2, it showed: NameError: name 'pirnt' is not defined. Did you mean: 'print'? on line 2 of main.py. Below that it explained that the name is used on line 2 but does not exist.

One honest warning. The outputs in this course come from Python 3.14. The Code Editor runs Python 3.12, and programs that draw with turtle run on a different engine inside the editor. So now and then the wording of a message may differ a little from what you see here. The type of error and the line number are what matter, and those will match. Module 7 turns reading errors into a full method.

Common misconceptions

  • "print shows the quote marks too." The quote marks only tell Python where the string starts and ends. The output shows the text inside them.
  • "Python does not care about capital letters." It does. Print, PRINT and print are three different names, and only print is built in.
  • "An error message means I broke something." Nothing is broken. Python is telling you which line it could not run and why. It is the most useful help you will get.
  • "Text code is a different kind of thinking from blocks." The thinking is the same: sequence, loops, choices and variables. Only the way you write it down has changed.

The takeaway

Bottom line: A Python program is a list of typed instructions that runs from the top, and every Scratch idea you know has a Python form.

You opened a free browser editor with no account, printed strings and numbers, and saw a program run line by line. You have a map from Scratch blocks to Python, and you used time.sleep as a wait block. You know what blocks used to do for you, which is why spelling, capital letters, brackets and quote marks now matter. And you have read four real error messages, from the bottom up, using the type and the line number. Next, Python's version of Scratch's variables, and why its equals sign does not mean what it means in maths.

Sources

  1. Python Software Foundation. (2026). General Python FAQ. (Van Rossum began during the 1989 Christmas holidays while at CWI; the name from Monty Python's Flying Circus; the February 1991 posting.) docs.python.org
  2. Python Software Foundation. (2026). Built-in functions: print, and time: time.sleep. (print separates its values with a space by default; sleep pauses for a number of seconds.) docs.python.org
  3. Raspberry Pi Foundation. (2026). Code Editor, and the editor's open-source code and tests, editor-ui. GitHub. (Runs Python with no setup and no login; Pyodide for ordinary programs and Skulpt for turtle; tests for input, time.sleep and the Stop button. Observed on 16 October 2026 running Python 3.12.1.) GitHub
  4. Code Club, Raspberry Pi Foundation. (2026). Accounts for creators. (The Code Editor works without an account; accounts are for saving; under-13s need a parent or guardian to set one up.) codeclub.org
  5. Trinket. (2026). Trinket has shut down. (Shut down on 31 August 2026 after being used by millions of learners and educators.) trinket.io
  6. Resnick, M., Maloney, J., Monroy-Hernández, A., Rusk, N., Eastmond, E., Brennan, K., Millner, A., Rosenbaum, E., Silver, J., Silverman, B., and Kafai, Y. (2009). Scratch: Programming for all. Communications of the ACM, 52(11), 60-67. (Blocks shaped to fit together only in ways that make syntactic sense; hexagons for Booleans; C-shaped control blocks.) MIT Media Lab
  7. Weintrop, D., and Wilensky, U. (2017). Comparing block-based and text-based programming in high school computer science classrooms. ACM Transactions on Computing Education, 18(1), article 3. https://doi.org/10.1145/3089799 (Abstract read: a five-week study; greater learning gains and interest with blocks; text seen as closer to professional practice.)
Key terms
print
A built-in Python function that shows values in the output, separated by spaces.
Function
A named command, such as print, that you run by writing its name followed by brackets.
String
A piece of text in a program, written between matching quote marks.
Comment
A note for human readers, starting with #, which Python ignores when it runs the program.
SyntaxError
An error meaning Python could not understand a line, such as one with a missing quote mark or bracket.
NameError
An error meaning Python met a name it does not know, often a spelling or capital-letter mistake.
Case-sensitive
Treating capital and small letters as different, so that Print and print are different names.

Variables: Names That Hold Values

  • Explain that = in Python stores a value under a name, and trace how variables change line by line.
  • Tell apart the str, int and float types and use the arithmetic operators, including // and %.
  • Choose valid, readable variable names and fix the type and name errors beginners meet first.

An equation that cannot be true

x = x + 1 looks like a mistake to anyone who has done algebra. No number is one more than itself, so as an equation it has no answer. Yet Python programs are full of lines like it, and they work. The trouble is not in the line. It is in reading = as "equals".

In Python, = does not state that two things are equal. It is an instruction: work out the value on the right, then store it under the name on the left. Programmers call this assignment. The Python tutorial puts it plainly: the equal sign is used to assign a value to a variable. A good habit is to read = aloud as "becomes" or "gets". score = score + 5 then reads "score becomes score plus 5", which makes perfect sense.

People have written about this exact confusion for a long time. In 1986 the researcher Benedict du Boulay described how beginners misunderstand assignment, partly by stretching a helpful picture of what a variable is too far. This lesson starts from that wrong reading and traces exactly where it fails.

What = really does

A variable in Python is a name attached to a value, just like a Scratch variable. Here is a program to trace before you run it.

score = 0
print(score)
score = 10
print(score)
score = score + 5
print(score)

Trace it line by line. For line 5, work out the right side first, using the value score has at that moment, and only then store the answer.

LineWhat Python doesscore afterwardsOutput
1store 0 under the name score0
2print the value of score00
3store 10, replacing the 010
4print the value1010
5work out 10 + 5, then store 1515
6print the value1515

The real output agrees.

0
10
15

score = 0 is Scratch's set score to 0, and score = score + 5 is change score by 5. A variable holds one value at a time. Line 3 did not add a second value next to the 0. It replaced it.

A copy, not a link

Predict this one carefully.

x = 5
y = x
x = 7
print(x, y)
7 5

Many beginners expect 7 7, reading y = x as a promise that y will always equal x. It is not a promise. Line 2 works out the value of x at that moment, which is 5, and stores 5 under y. Line 3 then changes x, and nothing tells y. The trace makes it plain.

Linexy
15not yet made
255
375

The swap that lost a value

Here is a real bug that the "equals" reading walks straight into. The program should swap two values, so that left ends up holding banana and right holds apple.

left = "apple"
right = "banana"
left = right
right = left
print(left, right)
banana banana

Apple has vanished. Trace it to find the exact moment.

Lineleftright
1applenot yet made
2applebanana
3bananabanana
4bananabanana

Line 3 replaced apple with banana, and apple was not stored anywhere else, so it was gone before line 4 ran. Line 4 then copied banana back onto itself. The fix is to keep a spare copy before overwriting anything.

left = "apple"
right = "banana"
spare = left
left = right
right = spare
print(left, right)
banana apple

The trace table found this bug in about a minute, without running anything. That is what traces are for.

Three kinds of value

Every value in Python has a type, which decides what you can do with it. The built-in function type tells you which one a value has.

name = "Pixel"
lives = 3
speed = 2.5
print(type(name))
print(type(lives))
print(type(speed))
<class 'str'>
<class 'int'>
<class 'float'>
TypeShort forWhat it holdsExamples
strstringText, in quote marks"Pixel", "3", ""
intintegerWhole numbers, with no decimal point3, -40, 0
floatfloating-point numberNumbers with a decimal point2.5, -0.1, 3.0

Look at "3" in the str row. With quote marks, 3 is text, the character you would type, not a number you can do sums with.

Doing arithmetic

print(7 + 2)
print(7 - 2)
print(7 * 2)
print(7 / 2)
print(7 // 2)
print(7 % 2)
print(7 ** 2)
print(6 / 2)
9
5
14
3.5
3
1
49
3.0
OperatorMeaning7 and 2 give
+ and -add, subtract9 and 5
*multiply: the star, not x14
/divide, always giving a float3.5
//divide and drop the remainder3
%the remainder after dividing1
**power: 7 to the power 249

The last line shows that / always gives a float, even when the answer is whole: 6 / 2 is 3.0, not 3. The pair // and % are more useful than they look. Together they split an amount into big units and what is left over.

seconds = 125
minutes = seconds // 60
left_over = seconds % 60
print(minutes, "minutes and", left_over, "seconds")
2 minutes and 5 seconds

When types clash

A game shows the player's lives. This looks reasonable, and it fails.

lives = 3
print("Lives: " + lives)
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print("Lives: " + lives)
          ~~~~~~~~~~^~~~~~~
TypeError: can only concatenate str (not "int") to str

A TypeError means an operation was used on the wrong kind of value. Between two strings, + joins them, which is called concatenating. Between two numbers, + adds them. Between a string and a number, Python will not guess which you meant. The ^ mark points at the + that caused the trouble. Two fixes work: turn the number into a string with str, or let print do the joining with a comma.

lives = 3
print("Lives: " + str(lives))
print("Lives:", lives)
Lives: 3
Lives: 3

The same rule explains a surprise that catches everyone once.

print(5 + 5)
print("5" + "5")
print("5" * 3)
10
55
555

Two strings joined make "55". A string times a number repeats it. Remember this, because in the next lesson every answer a user types arrives as a string.

Naming rules

A variable name may contain letters, digits and underscores, but it cannot start with a digit, and it cannot contain spaces or hyphens. It cannot be one of Python's own words, such as if, for or while. Capital letters count, so Score and score are different names. Python's style guide recommends lowercase words joined by underscores, such as high_score.

NameAllowed?Why
high_scoreyesletters and an underscore, and it says what it holds
player2yesa digit is fine after the first character
2nd_placenostarts with a digit
my scorenocontains a space
high-scorenothe hyphen is read as a minus sign
sallowed, but unhelpfulnobody can tell what it holds

Here is what Python says when the rules are broken. Each was a separate program.

  File "main.py", line 1
    2nd_place = "Ada"
    ^
SyntaxError: invalid decimal literal
  File "main.py", line 1
    my score = 10
       ^^^^^
SyntaxError: invalid syntax
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print(Score)
          ^^^^^
NameError: name 'Score' is not defined. Did you mean: 'score'?

The last one came from a program whose first line was score = 10. A NameError also appears if you use a variable before giving it a value. A program that starts with print(total) and only sets total = 0 on line 2 fails with NameError: name 'total' is not defined, because line 1 runs before line 2.

One shortcut is worth knowing. score += 1 means exactly the same as score = score + 1, and saves typing the name twice.

coins = 0
coins = coins + 1
coins += 1
coins += 10
print(coins)
12

Common misconceptions

  • "= means the two sides are equal." It means "work out the right side and store it under the name on the left". Read it as "becomes".
  • "y = x makes y follow x from now on." It copies the value x has at that moment. Changing x later does not change y.
  • "A variable can hold its old value and its new one." Assignment replaces the old value. If you will need it later, store it in another variable first, as in the swap.
  • "print('score') shows the score." With quote marks it is a string, so it prints the word score. print(score), with no quote marks, prints the value.

Recap

What matters here: In Python, = is an instruction, not a statement of fact. Python works out the right side using today's values, then stores the result under the name on the left.

You traced variables line by line and saw that assignment copies a value at one moment, which is why y kept 5 and why the swap lost apple until a spare variable saved it. You met the three basic types, str, int and float, and the arithmetic operators, including // and % for splitting seconds into minutes. You know why "Lives: " + 3 is a TypeError and two ways to fix it, and why "5" + "5" is "55". And you can name variables legally and clearly. Next, a program that asks the user a question, where every answer arrives as a string.

Sources

  1. Python Software Foundation. (2026). An informal introduction to Python. The Python Tutorial. (The equal sign assigns a value to a variable; / always returns a float; // and %; using an undefined variable gives an error.) docs.python.org
  2. Python Software Foundation. (2026). Lexical analysis: Identifiers and keywords. The Python Language Reference. (Which characters a name may use, digits not allowed first, and the reserved keywords.) docs.python.org
  3. van Rossum, G., Warsaw, B., and Coghlan, A. (2001, updated 2013). PEP 8: Style guide for Python code. (Variable names in lowercase with words separated by underscores.) peps.python.org
  4. du Boulay, B. (1986). Some difficulties of learning to program. Journal of Educational Computing Research, 2(1), 57-73. https://doi.org/10.2190/3LFX-9RRF-67T8-UVK9 (Abstract read: errors from misapplied analogies and from misunderstanding assignment.)
  5. Qian, Y., and Lehman, J. (2017). Students' misconceptions and other difficulties in introductory programming: A literature review. ACM Transactions on Computing Education, 18(1), article 1. https://doi.org/10.1145/3077618 (Abstract read: misconceptions in syntactic, conceptual and strategic knowledge, often linked to prior knowledge such as mathematics.)
Key terms
Variable
A name attached to a value. In Python it is created the first time you assign to it.
Assignment
Storing a value under a name with =. Python works out the right side first, then stores it.
Type
The kind of value something is, such as str, int or float, which decides what you can do with it.
int and float
Whole numbers such as 3 are ints; numbers with a decimal point such as 2.5 are floats.
Concatenate
To join strings end to end with +, so that 'ab' + 'cd' gives 'abcd'.
// and %
Floor division drops the remainder, and % gives the remainder: 125 // 60 is 2 and 125 % 60 is 5.
TypeError
An error meaning an operation was used on the wrong kind of value, such as adding a string to a number.

Input and Output: Programs That Ask

  • Use input to ask the user a question, and explain why the answer always arrives as a string.
  • Convert answers with int and float, and recognise the ValueError and TypeError that conversion mistakes cause.
  • Build clear output with f-strings, sep, end and round.

12 plus 9 is 129

Ask the first program in this lesson to add 12 and 9, and it answers 129. Here it is, with a sample run. The words after each question are what the user typed.

first = input("First number: ")
second = input("Second number: ")
print("Total:", first + second)
First number: 12
Second number: 9
Total: 129

Nothing crashed. No error message appeared. The program simply gave a wrong answer with total confidence. Before reading on, use the last lesson to work out why. There is a clue in "5" + "5".

input: the program asks, the user answers

input is Python's version of Scratch's ask and wait block. It shows a question, called a prompt, then waits for the user to type something and press Enter. Whatever they typed comes back as the value of input, so you usually store it in a variable straight away.

nickname = input("What should I call you? ")
print("Hi,", nickname)
What should I call you? Sky
Hi, Sky

In the Code Editor, a box appears in the Text output panel when input is waiting. Click in it, type, and press Enter. Leave a space at the end of the prompt string, before the closing quote mark, so the answer does not touch the question.

A small habit worth starting now: when a program asks for a name, give a nickname. It costs nothing here, and it is the same habit that keeps your real name out of projects you share and out of websites you do not know.

Every answer is a string

Here is the reason for 129.

answer = input("Type a number: ")
print(type(answer))
Type a number: 12
<class 'str'>

input always returns a string, even when the user types digits. The Python documentation says so directly: it reads a line from input, converts it to a string, and returns that. So in the first program, first held the text "12" and second held the text "9". With two strings, + joins them, and "12" joined to "9" is "129".

Linefirstsecondfirst + second
1, user types 12"12" (a str)not yet made
2, user types 9"12""9" (a str)
3"12""9""129", joined, not added

Forgetting this in the other direction gives an error instead of a wrong answer.

age = input("How old will your pet be this year? ")
print("Next year it will be", age + 1)
How old will your pet be this year? 3
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print("Next year it will be", age + 1)
                                  ~~~~^~~
TypeError: can only concatenate str (not "int") to str

Strangely, the error is the luckier outcome. An error message stops you and points at the line. The 129 program gave no warning at all.

Turning text into numbers

The built-in functions int and float turn a string of digits into a number you can calculate with. Wrap the input call inside int, and read the line from the inside out: input runs first and returns a string, then int converts that string into a whole number.

first = int(input("First number: "))
second = int(input("Second number: "))
print("Total:", first + second)
First number: 12
Second number: 9
Total: 21

Here is what each conversion does. The last three rows were checked with print.

ExpressionResultType of result
int("12")12int
float("12.5")12.5float
str(12)"12"str
int(" 12 ")12: spaces around the digits are ignoredint
int("-7")-7int
int(12.5)12: a float value loses its fractionint

Use int when the answer must be a whole number, such as a count of people. Use float when decimals make sense, such as a height or a price.

When the conversion fails

Users do not always type what you expect. Here is the same line given two different answers, in two separate runs.

slices = int(input("How many slices of pizza? "))
How many slices of pizza? twelve
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    slices = int(input("How many slices of pizza? "))
ValueError: invalid literal for int() with base 10: 'twelve'
How many slices of pizza? 12.5
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    slices = int(input("How many slices of pizza? "))
ValueError: invalid literal for int() with base 10: '12.5'

A ValueError means a function got the right type of value, a string, but one it cannot use. "Invalid literal for int()" is Python's way of saying "this text is not a whole number". The message quotes exactly what the user typed, which is the first clue to check. Notice that int("12.5") fails even though int(12.5) works: the string has a decimal point in it, and int will not guess what to do with it. If decimals are allowed, use float instead. In Lesson 13 you will write a loop that keeps asking until the answer is usable.

Putting values into sentences

Building sentences with commas and + gets clumsy fast. An f-string is a string with the letter f just before the opening quote mark. Inside it, anything in curly braces is worked out and its value is dropped into the text.

slices = int(input("How many slices of pizza? "))
people = int(input("How many people? "))
each = slices // people
spare = slices % people
print(f"Each person gets {each} slices, with {spare} left over.")
How many slices of pizza? 14
How many people? 4
Each person gets 3 slices, with 2 left over.

The braces can hold a calculation as well as a name. Here is a story generator that uses three answers.

animal = input("An animal: ")
colour = input("A colour: ")
number = int(input("A number: "))
print(f"One morning, {number} {colour} {animal}s walked into the school library.")
print(f"By lunchtime there were {number * 2} of them.")
An animal: penguin
A colour: purple
A number: 7
One morning, 7 purple penguins walked into the school library.
By lunchtime there were 14 of them.

Forget the f and the braces are printed as they are, with no error to warn you. Given each = 3, the line print("Each person gets {each} slices.") prints Each person gets {each} slices. Adding the f gives Each person gets 3 slices.

Shaping the output

print has two settings you can change. sep is what goes between the values, a space unless you say otherwise. end is what goes after the last value, a new line unless you say otherwise.

print("red", "green", "blue")
print("red", "green", "blue", sep=", ")
print("Loading", end="")
print("...", end="")
print(" done")
red green blue
red, green, blue
Loading... done

Long decimals look messy, so round them. round takes a number and how many digits to keep after the point.

height = float(input("Plant height in centimetres: "))
growth = height * 0.15
print("It might grow", round(growth, 1), "cm this week.")
Plant height in centimetres: 23.4
It might grow 3.5 cm this week.

Common misconceptions

  • "If the user types digits, input gives a number." input always gives a string. Convert it with int or float before doing arithmetic.
  • "The prompt is stored in the variable." The prompt is only shown. The variable gets what the user typed.
  • "int rounds 12.5 to a whole number." For the string "12.5", int gives a ValueError. For the float 12.5, it drops the fraction and gives 12, which is not rounding either.
  • "Braces in any string fill in values." Only in an f-string. Without the f, the braces and the name inside them are printed exactly as typed.

What to remember

Worth holding on to: Everything a user types reaches your program as a string. If you need a number, convert it, and expect that sometimes the conversion will fail.

You used input to ask questions and saw why 12 and 9 made 129: two strings joined, not two numbers added. You converted answers with int and float and read the ValueError that appears when the text is not a number. You built sentences with f-strings, including a calculation inside the braces, and you shaped output with sep, end and round. With input, your programs can now react to a person. Next, they learn to choose what to do with the answer.

Sources

  1. Python Software Foundation. (2026). Built-in functions: input, int, float, print and round. (input returns the line the user typed as a string; print's sep and end settings.) docs.python.org
  2. Python Software Foundation. (2026). Input and output: Formatted string literals. The Python Tutorial. (f-strings include the value of expressions written in braces.) docs.python.org
  3. Python Software Foundation. (2026). Built-in exceptions: ValueError and TypeError. (ValueError for an argument of the right type but an unsuitable value; TypeError for an operation on the wrong type.) docs.python.org
  4. Raspberry Pi Foundation. (2026). editor-ui end-to-end tests, spec-wc-pyodide.cy.js. GitHub. (The editor runs a program that asks for input and prints the answer.) GitHub
Key terms
input
A built-in function that shows a prompt, waits for the user to type, and returns what they typed as a string.
Prompt
The question or message input shows before the user types.
int() and float()
Functions that convert a value, such as the string '12', into a whole number or a decimal number.
ValueError
An error meaning a function got the right type but an unusable value, such as int('twelve').
f-string
A string with f before the opening quote, in which expressions inside braces are replaced by their values.
sep and end
Settings for print: sep goes between values (a space by default) and end goes after them (a new line by default).
round
A function that rounds a number to a chosen number of decimal places, such as round(3.51, 1) giving 3.5.

Module 4: Choosing a Path

Programs that decide: conditions that are True or False, if and else with the colon and indentation that Python requires, testing at the boundary, then chains of elif and the logical operators and, or and not, with the order of conditions as the classic bug.

If and Else: Programs That Choose

  • Write if and else statements with a condition, a colon and an indented block.
  • Use the comparison operators, tell = from ==, and predict whether a condition is True or False.
  • Test a decision just below, at and just above its boundary, and fix the bug that the boundary test exposes.

A sign at the gate

Suppose a ride's sign says that riders must be at least 120 cm tall. A person at the gate measures each rider and makes one decision: ride, or not. That decision is the whole program, and it takes six lines.

height = int(input("Your height in cm: "))
if height >= 120:
    print("You can ride.")
else:
    print("Sorry, not this time.")
print("Thanks for visiting!")

Here are two runs, one for each path.

Your height in cm: 135
You can ride.
Thanks for visiting!
Your height in cm: 110
Sorry, not this time.
Thanks for visiting!

Every run prints exactly one of the two middle messages, and every run prints the last line. This is Scratch's if then else block, written as text.

The parts of an if statement

PartIn the ride programWhat it does
ififStarts the decision
A conditionheight >= 120A question whose answer is True or False
A colon:Says that an indented block comes next
An indented blockprint("You can ride.")Runs only if the condition is True
else, with its own colonelse:Optional: the other path
Another indented blockprint("Sorry, not this time.")Runs only if the condition is False

In Scratch, the blocks inside the mouth of the if then else block belonged to it. In Python, the indentation, four spaces at the start of the line, does the same job. The lines pushed in under if belong to the if. The first line that is not pushed in, print("Thanks for visiting!"), is outside the decision and always runs. Python's style guide asks for four spaces per level. Keep them consistent: lines in the same block must line up exactly. A line pushed in by three spaces under one pushed in by four stops the program with IndentationError: unindent does not match any outer indentation level.

The else part is optional. Without it, the if either runs its block or skips it, and the program carries on.

The run that matters most

Two runs, two correct answers. Is the program right? The most important test is the one people forget: a rider who is exactly 120 cm tall.

Your height in cm: 120
You can ride.
Thanks for visiting!

Correct, because 120 is at least 120. Now make one small change, the kind that slips in when you type quickly: write > instead of >= on line 2. Run the same three tests again.

Height typedWith height >= 120With height > 120
135You can ride.You can ride.
110Sorry, not this time.Sorry, not this time.
120You can ride.Sorry, not this time.

The broken version passes the two easy tests. Only the value at the edge shows the difference, because 120 is not greater than 120. A value right at the limit of a decision is called a boundary value. Whenever a program compares, test just below the boundary, exactly on it, and just above it: here 119, 120 and 121. It takes three runs and catches one of the most common bugs there is.

Conditions are True or False

A condition is an expression that works out to one of two values: True or False, with capital letters. These are called Boolean values. You can print them to see a condition's answer.

print(7 > 3)
print(7 < 3)
print(7 == 7)
print(7 != 7)
print(3 >= 3)
print("cat" == "Cat")
True
False
True
False
True
False
OperatorMeansExample that is True
==is equal to7 == 7
!=is not equal to7 != 8
<is less than3 < 7
>is greater than7 > 3
<=is less than or equal to3 <= 3
>=is greater than or equal to120 >= 120

The comparison for "is equal to" is two equals signs. One equals sign is assignment, from Lesson 8, and Python refuses to let you use it in a condition.

height = 125
if height = 120:
    print("Exactly the limit.")
  File "main.py", line 2
    if height = 120:
       ^^^^^^^^^^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?

The message even suggests the fix. (The := it mentions is an advanced operator you will not need in this course.)

Indentation is part of the meaning

Indentation is not decoration. Moving one line in or out changes what the program does. Compare these two programs, both with temperature set to 18.

temperature = 18
if temperature > 25:
    print("It's hot.")
    print("Drink some water.")
print("Have a nice day.")
Have a nice day.
temperature = 18
if temperature > 25:
    print("It's hot.")
print("Drink some water.")
print("Have a nice day.")
Drink some water.
Have a nice day.

In the second program, "Drink some water." is no longer inside the if, so it prints even on a cool day. The code looks almost the same, and Python runs it without complaint, because both versions are legal. Only you know which one you meant.

Two mistakes do stop Python. A missing colon:

height = 125
if height >= 120
    print("You can ride.")
  File "main.py", line 2
    if height >= 120
                    ^
SyntaxError: expected ':'

And a block that is not indented at all:

height = 125
if height >= 120:
print("You can ride.")
  File "main.py", line 3
    print("You can ride.")
    ^^^^^
IndentationError: expected an indented block after 'if' statement on line 2

Look at how helpful that last message is: it names the if and gives its line number. In the Code Editor, programs that use turtle run on a different engine that words this more bluntly. A missing indent there showed up as SyntaxError: bad input on line 5 of main.py. The fix is the same: check the indentation around the line it names.

Even or odd

The remainder operator from Lesson 8 makes a neat condition. A number is even when dividing it by 2 leaves nothing over.

number = int(input("A whole number: "))
if number % 2 == 0:
    print(number, "is even")
else:
    print(number, "is odd")
A whole number: 14
14 is even
A whole number: 7
7 is odd

Testing the edges again: 0 gives "0 is even", which is correct, and -3 gives "-3 is odd", also correct. Edge cases are not only boundaries. They include zero, negative numbers and anything else you did not picture while writing the program.

Comparing text

answer = input("Do you like cats? (yes/no) ")
if answer == "yes":
    print("Me too!")
else:
    print("Fair enough.")
Do you like cats? (yes/no) Yes
Fair enough.

The user said yes, with a capital Y, and the program heard no. String comparison is exact: "Yes" and "yes" are different strings, just as "cat" == "Cat" printed False above. A stray space would break it too. The fix is to tidy the answer before comparing, using two string methods, which are functions that belong to a value and are written after it with a dot. strip removes spaces from both ends, and lower turns every capital into a small letter.

print("  Yes ".strip())
print("YeS".lower())
print("  YeS ".strip().lower())
Yes
yes
yes

Add one line after the input, answer = answer.strip().lower(), and the program now answers "Me too!" to yes, Yes, YES and " Yes". You will use exactly this line again in the quiz game project.

Common misconceptions

  • "= and == mean the same thing." = stores a value. == asks whether two values are equal. In a condition you need ==.
  • "Indentation is just to make code look neat." In Python it decides which lines belong to the if. Moving a line out of the block changes when it runs.
  • "If the program works for normal values, it works." The boundary is where > and >= differ. Test just below, on, and just above it.
  • "Python knows that Yes means yes." String comparison is exact, including capitals and spaces. Tidy the answer with strip and lower first.

What to carry forward

So what? A decision is only as good as its condition, and the condition is only tested properly at its boundary.

You wrote if and else with a condition, a colon and a four-space block, and you saw that the first unindented line is outside the decision. You met True and False and the six comparison operators, and you know why = cannot go in a condition. You saw a > instead of >= pass two tests and fail the third, which is why boundary values come first in testing. And you made a text comparison forgiving with strip and lower. One decision with two paths is often not enough. The next lesson deals with three, four or more.

Sources

  1. Python Software Foundation. (2026). More control flow tools: if statements. The Python Tutorial. (The if statement, with optional elif and else parts.) docs.python.org
  2. Python Software Foundation. (2026). Built-in types: comparisons, Boolean values, str.lower and str.strip. (The comparison operators; True and False; lower converts cased characters to lowercase; strip removes leading and trailing whitespace by default.) docs.python.org
  3. van Rossum, G., Warsaw, B., and Coghlan, A. (2001, updated 2013). PEP 8: Style guide for Python code. (Use 4 spaces per indentation level.) peps.python.org
  4. Raspberry Pi Foundation. (2026). editor-ui source code, PythonRunner.jsx. GitHub. (Programs that import turtle run on Skulpt rather than Pyodide, which is why some error messages are worded differently; the bad input message observed on 16 October 2026.) GitHub
Key terms
if statement
A statement that runs its indented block only when its condition is True.
else
The optional part of an if statement whose block runs when the condition is False.
Condition
An expression that works out to True or False, such as height >= 120.
Boolean
One of the two values True and False, written with capital letters in Python.
== and =
== asks whether two values are equal; = stores a value under a name.
Indentation
Spaces at the start of a line. In Python, four spaces show which lines belong to an if, a loop or a function.
Boundary value
A test value right at the limit of a decision, such as 120 for 'at least 120', where > and >= give different answers.
Method
A function that belongs to a value and is called with a dot, such as answer.lower().

Elif, And, Or, Not: More Than Two Paths

  • Write an if, elif and else chain and explain why only the first True condition's block runs.
  • Order the conditions in a chain correctly, and find the bug when they are in the wrong order.
  • Combine conditions with and, or and not, and avoid the 'or' mistake that is always True.

A 95 that came out as a C

A test score of 95 should get an A, and the program below gives it a C. It runs without any error message. Every line is legal Python. Read it, then look at the run.

score = int(input("Test score: "))
if score >= 50:
    print("Grade: C")
elif score >= 70:
    print("Grade: B")
elif score >= 85:
    print("Grade: A")
else:
    print("Not passed yet")
Test score: 95
Grade: C

The person who wrote this was thinking like a human marker: "95 is above 50, above 70 and above 85, so the highest band it reaches is A." That reasoning is sensible. It is just not what Python does. To see where it breaks, trace the program exactly.

How a chain is checked

elif is short for "else if". The Python tutorial describes it that way, and it is useful to avoid having to indent further and further. An if, elif and else chain works like this:

  1. Check the first condition. If it is True, run its block and skip everything else in the chain.
  2. If it is False, check the next elif, and so on down.
  3. If none is True, run the else block, if there is one.

Only one block in a chain ever runs: the block of the first condition that is True. Here is the trace for 95.

LineConditionValueWhat happens
2score >= 50TruePrint "Grade: C", then leave the chain
4score >= 70never checkedskipped
6score >= 85never checkedskipped
8elsenever reachedskipped

Python does not look for the best match. It takes the first True condition it meets. Because every score of 50 or more makes the first condition True, the B and A blocks can never run at all. A score of 75 gets a C, and so does 100.

The fix: the narrowest test first

Put the conditions in an order where the first True one is the right one: start with the band that is hardest to reach.

score = int(input("Test score: "))
if score >= 85:
    print("Grade: A")
elif score >= 70:
    print("Grade: B")
elif score >= 50:
    print("Grade: C")
else:
    print("Not passed yet")

Now test it the way the last lesson taught, at every boundary. These are real runs.

Score typedOutputWhy this score
95Grade: Athe score that exposed the bug
85Grade: Aon the A boundary
84Grade: Bjust below it
70Grade: Bon the B boundary
69Grade: Cjust below it
50Grade: Con the C boundary
49Not passed yetjust below it

Why does the elif score >= 70 line not need to say "and less than 85"? Because it is only reached if score >= 85 was False. The order of the chain does part of the work.

Why not three separate ifs?

Suppose you drop the elif and else and write three separate if statements, in the right order. Each if is a decision of its own, so each one is checked.

score = int(input("Test score: "))
if score >= 85:
    print("Grade: A")
if score >= 70:
    print("Grade: B")
if score >= 50:
    print("Grade: C")
Test score: 95
Grade: A
Grade: B
Grade: C

Three grades for one test. Use separate ifs when several things can all be true and should all happen. Use one chain when exactly one answer is wanted.

Joining conditions: and, or, not

Sometimes one comparison is not enough. Python has three words for combining conditions.

OperatorTrue whenExample
andboth sides are Truetemperature >= 20 and temperature <= 30
orat least one side is Trueage < 13 or age >= 65
notthe condition after it is Falsenot finished
print(True and False)
print(True or False)
print(not True)
False
True
False

Here is a ticket price list. Children under 5 go free. Children under 13 and people aged 65 or over pay 4. Everyone else pays 8.

age = int(input("Age: "))
if age < 5:
    price = 0
elif age < 13 or age >= 65:
    price = 4
else:
    price = 8
print("Ticket price:", price)

Testing every boundary gives these real results: age 3 pays 0, age 9 pays 4, age 13 pays 8, age 40 pays 8, age 64 pays 8, and age 65 pays 4. Notice the elif does not need "and age >= 5": anyone under 5 already left the chain at the first if.

And is the tool for a range. Water between 20 and 30 degrees, including both ends, is good for swimming.

temperature = float(input("Water temperature: "))
if temperature >= 20 and temperature <= 30:
    print("Good for swimming.")
else:
    print("Not today.")

Runs with 25 and 30 print "Good for swimming." Runs with 31 and 19.5 print "Not today." Python also lets you write the range the way maths does, 20 <= temperature <= 30, which means exactly the same thing.

The or that is always True

This one catches almost everyone. The program should say yes only when the answer is "yes" or "y". The answer here is "no".

answer = "no"
if answer == "yes" or "y":
    print("You said yes")
else:
    print("You said no")
You said yes

In English, "if the answer is yes or y" obviously compares the answer with both. Python does not read it that way. It sees two separate conditions joined by or: answer == "yes", which is False, and then just "y" on its own. A string with something in it counts as true in a condition, and only the empty string "" counts as false. So the right side is always true, and so is the whole or. Each side of an or must be a complete comparison.

answer = "no"
if answer == "yes" or answer == "y":
    print("You said yes")
else:
    print("You said no")
You said no

not, for catching what is not allowed

not flips a condition. It is handy for rejecting anything outside a list of allowed answers.

move = input("rock, paper or scissors? ").strip().lower()
if not (move == "rock" or move == "paper" or move == "scissors"):
    print("That is not a move.")
else:
    print("You chose", move)
rock, paper or scissors? lizard
That is not a move.

Typing " Paper" with a capital and a space prints "You chose paper", because strip and lower tidied it first. The brackets matter: they make Python work out the whole or first, then flip the answer with not.

Common misconceptions

  • "Python checks every elif and picks the best match." It runs the block of the first True condition and skips the rest. Order the conditions from the hardest to reach to the easiest.
  • "elif and a new if do the same thing." An elif is only checked if everything above it in the chain was False. A new if is always checked, so several can run.
  • "answer == 'yes' or 'y' compares answer with both." The right side is just the string 'y', which always counts as true. Write answer == 'yes' or answer == 'y'.
  • "and means either one is enough." That is or. With and, both sides must be True, which is why 'age < 13 and age >= 65' can never be True.

Putting it together

The core of it: In a chain, the first True condition wins, so the order of the conditions is part of the program's logic.

You traced a grading program that gave 95 a C and saw exactly why: the first condition was True for every passing score. You fixed it by testing the narrowest band first and checked it at every boundary. You saw that separate ifs can all run while a chain runs one block. And you combined conditions with and, or and not, including the always-True or that looks like English and is not Python. You can now write programs that choose between any number of paths. The next module makes them repeat.

Sources

  1. Python Software Foundation. (2026). More control flow tools: if statements. The Python Tutorial. (Zero or more elif parts and an optional else; elif is short for else if and avoids excessive indentation.) docs.python.org
  2. Python Software Foundation. (2026). Expressions: Boolean operations and comparisons. The Python Language Reference. (How and, or and not work; comparisons can be chained, so 20 <= t <= 30 means 20 <= t and t <= 30.) docs.python.org
  3. Python Software Foundation. (2026). Built-in types: Truth value testing. (Empty sequences such as '' are false; other strings count as true.) docs.python.org
  4. Qian, Y., and Lehman, J. (2017). Students' misconceptions and other difficulties in introductory programming: A literature review. ACM Transactions on Computing Education, 18(1), article 1. https://doi.org/10.1145/3077618 (Abstract read: difficulties often come from natural language and prior knowledge, as with reading 'or' as English.)
Key terms
elif
Short for else if: a further condition in a chain, checked only if every condition above it was False.
if, elif, else chain
A set of conditions checked in order, where only the block of the first True condition runs.
and
Joins two conditions; the result is True only when both are True.
or
Joins two conditions; the result is True when at least one is True. Each side must be a complete condition.
not
Flips a condition: not True is False, and not False is True.
Chained comparison
A range written as 20 <= t <= 30, meaning the same as 20 <= t and t <= 30.
Truthy string
Any string with at least one character counts as true in a condition; only the empty string '' counts as false.

Module 5: Going Round Again

Python's two loops: for with range, for counting passes that are known in advance, with the off-by-one slips that range invites and the running-total pattern; then while, for repeating until something changes, with infinite loops, the Stop button, input checking and a first guessing game.

For Loops and range: Counting Without Slipping

  • Write for loops over range and over the characters of a string, matching them to Scratch's repeat block.
  • Predict exactly which numbers each form of range gives, and avoid the off-by-one mistake.
  • Use a running total and a loop inside a loop, and count how many times a nested body runs.

Ten lines, or two

The 7 times table has ten lines, and typing them by hand takes ten print lines. Each one is almost the same as the last, which is exactly where Scratch reached for its repeat block. In Python, the same job takes two lines.

for number in range(1, 11):
    print(7, "x", number, "=", 7 * number)
7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70

Change the 7 to 9 and you have a different table. Change 11 to 13 and it goes up to 12. Nothing else needs touching.

How a for loop works

A for loop runs its indented block once for each value in a sequence, and on each pass it puts the current value into a variable, called the loop variable. In the times table, the loop variable is number, and the sequence comes from range(1, 11), which counts 1, 2, 3 and so on up to 10. The line ends with a colon and the body is indented, exactly as with if.

PassnumberWhat the body prints
117 x 1 = 7
227 x 2 = 14
.........
10107 x 10 = 70

When you only want something done a number of times, like Scratch's repeat 3, you can ignore the loop variable.

for count in range(3):
    print("Hip hip hooray!")
Hip hip hooray!
Hip hip hooray!
Hip hip hooray!

The forms of range, side by side

range can be given one, two or three numbers. Here each form is printed in a loop, with end=" " so the numbers sit on one line. These are real outputs.

for n in range(5):
    print(n, end=" ")
print()
for n in range(1, 6):
    print(n, end=" ")
print()
for n in range(0, 20, 5):
    print(n, end=" ")
print()
for n in range(10, 0, -2):
    print(n, end=" ")
print()
for n in range(5, 5):
    print(n, end=" ")
print("(nothing)")
0 1 2 3 4
1 2 3 4 5
0 5 10 15
10 8 6 4 2
(nothing)
FormGivesReads as
range(5)0 1 2 3 4five numbers, starting at 0
range(1, 6)1 2 3 4 5start at 1, stop before 6
range(0, 20, 5)0 5 10 15start at 0, step 5, stop before 20
range(10, 0, -2)10 8 6 4 2count down in 2s, stop before 0
range(5, 5)nothingstart and stop are the same, so the body runs zero times

One rule covers every row: range includes its start and stops before its stop. The Python tutorial says the end point is never part of the sequence. That is why range(5) gives five numbers, 0 to 4, and why 20 is missing from the third row even though the steps would land on it.

Off by one

The stop rule causes the most common loop bug of all. Someone wants the numbers 1 to 10 and writes this.

for n in range(1, 10):
    print(n, end=" ")
print()
1 2 3 4 5 6 7 8 9 

Nine numbers, not ten. A loop that runs one time too many or one time too few is said to have an off-by-one error. Two habits prevent it. First, the number of values in range(a, b) is b minus a, so range(1, 10) gives 9 values and range(1, 11) gives 10. Second, always check the last value, not just the first: write the final pass in your trace table before you run anything.

Counting down needs a negative step, and the same rule about the stop.

for n in range(10, 0, -1):
    print(n, end=" ")
print("Lift off!")
10 9 8 7 6 5 4 3 2 1 Lift off!

The countdown stops before 0, so 1 is the last number printed.

Running totals

One of the most useful patterns in programming is the running total: a variable that starts at 0 before the loop and grows on every pass. Here the body also prints, so you can watch it.

total = 0
for n in range(1, 5):
    total = total + n
    print("added", n, "so total is", total)
print("Final total:", total)
added 1 so total is 1
added 2 so total is 3
added 3 so total is 6
added 4 so total is 10
Final total: 10

Change range(1, 5) to range(1, 101) and remove the print inside the loop, and the program adds every whole number from 1 to 100 in a moment. It prints 5050.

Now the classic bug. Watch where total = 0 has moved.

total = 0
for n in range(1, 5):
    total = 0
    total = total + n
print("Final total:", total)
Final total: 4
Passntotal after the resettotal after adding
1101
2202
3303
4404

The reset is now inside the loop, so it wipes the total at the start of every pass, and only the last number survives. It is the same mistake as Lesson 6's if block outside the forever loop, the other way round: a line in the wrong place relative to the loop. Starting values go before the loop. Things that should happen every pass go inside it.

Looping over a string

A for loop can walk through anything that is a sequence, and a string is a sequence of characters. Here each pass gets the next letter of the word.

word = input("Type a word: ")
vowels = 0
for letter in word.lower():
    if letter in "aeiou":
        vowels = vowels + 1
print(f"{word} has {vowels} vowels.")
Type a word: Banana
Banana has 3 vowels.

The condition letter in "aeiou" is True when the letter appears anywhere in the string "aeiou". Typing rhythm gives "rhythm has 0 vowels.", which is a good edge case: the if never runs, and the count correctly stays at 0.

A loop inside a loop

Lesson 8 showed that a string times a number repeats it. A loop can use that to draw.

for row in range(1, 5):
    print("*" * row)
*
**
***
****

And a loop can sit inside another, like the Scratch flower of squares. The inner loop runs completely on every pass of the outer one.

for row in range(3):
    for column in range(4):
        print("#", end="")
    print()
####
####
####

The inner print runs 4 times per row, for 3 rows: 3 times 4 is 12 times in all. The plain print() is indented only once, so it belongs to the outer loop and ends each row. Indent it one level further and every # would land on its own line.

Common misconceptions

  • "range(10) counts from 1 to 10." It counts from 0 to 9. range always starts at 0 unless told otherwise, and always stops before the stop value.
  • "range(1, 10) gives ten numbers." It gives nine: 10 minus 1. For 1 to 10, write range(1, 11).
  • "It does not matter whether total = 0 goes inside or outside the loop." Inside, it resets the total on every pass and only the last value survives.
  • "The loop variable has to be called i." Any variable name works. A name that says what it holds, like number or letter, makes the loop easier to read.

The short version

Key idea: A for loop runs its body once for each value in a sequence, and range gives a sequence that includes its start and stops just before its stop.

You turned ten print lines into a two-line loop and traced the loop variable pass by pass. You compared five forms of range and learned the one rule behind them all, which is also the cause of the off-by-one error. You built a running total and saw what happens when its starting line drifts inside the loop. You looped over the letters of a word, and you drew with a loop inside a loop, multiplying the counts. A for loop knows how many passes it will make. The next lesson is about the loop that does not.

Sources

  1. Python Software Foundation. (2026). More control flow tools: for statements and the range() function. The Python Tutorial. (for iterates over the items of a sequence in order; the end point of range is never part of the sequence; range can start at another number and take a step, including a negative one.) docs.python.org
  2. Python Software Foundation. (2026). Built-in types: Ranges, and common sequence operations. (range objects; the in operator for strings.) docs.python.org
  3. Scratch Wiki contributors. (2026). Repeat () (block). Scratch Wiki. (The Scratch loop that a for loop over range replaces.) Scratch Wiki
  4. K-12 Computer Science Framework. (2016). Framework statements by concept: Algorithms and Programming. (Grades 6 to 8, control: programmers select and combine loops, event handlers and conditionals.) k12cs.org
Key terms
for loop
A loop that runs its indented block once for each value in a sequence, such as the numbers from range or the letters of a string.
Loop variable
The variable a for loop sets to the next value on each pass, such as number in 'for number in range(1, 11):'.
range
A built-in that produces a sequence of whole numbers; it includes its start and stops just before its stop.
Step
The third value given to range: how much to count by each time, which can be negative to count down.
Off-by-one error
A bug where a loop runs one time too many or one time too few, often from forgetting that range stops before its stop.
Running total
A variable set to 0 before a loop and added to on every pass.
Nested loop
A loop inside another loop; the inner body runs inner count times outer count times.

While Loops: Repeat Until Something Changes

  • Write a while loop that changes something on every pass so that its condition eventually becomes False.
  • Recognise and stop an infinite loop, and translate Scratch's repeat until into a while loop.
  • Use while loops to check input and to run a guessing game with random numbers and break.

Four lines that never stop

The program below has four lines, and it never stops. It is meant to count five laps of a race and then say Finished.

count = 1
while count <= 5:
    print("Lap", count)
print("Finished")
Lap 1
Lap 1
Lap 1
Lap 1
Lap 1
Lap 1

That is only the start of the output. It goes on printing Lap 1, over and over, and never reaches Finished. In the Code Editor, the Run button turns into a Stop button while a program runs. Click it and the editor halts the program and reports that execution was interrupted. If you leave it, the editor stops printing when the output reaches its limit. Either way, nothing is damaged. Before reading on, find the problem by tracing the first three passes.

How a while loop works

A while loop checks a condition, and if it is True, runs its indented block, then goes back and checks the condition again. It keeps doing this for as long as the condition is True. The Python reference describes it as repeated execution "as long as an expression is true".

Check numbercountcount <= 5?Output
11TrueLap 1
21TrueLap 1
31TrueLap 1

The trace shows it at once: count is 1 every time, because nothing in the loop changes it. The condition can never become False. A loop like that is called an infinite loop. A for loop moves on to the next value by itself. A while loop does not, so the job of changing something is yours.

count = 1
while count <= 5:
    print("Lap", count)
    count = count + 1
print("Finished")
Lap 1
Lap 2
Lap 3
Lap 4
Lap 5
Finished
Check numbercountcount <= 5?Output, then count becomes
11TrueLap 1, then 2
22TrueLap 2, then 3
33TrueLap 3, then 4
44TrueLap 4, then 5
55TrueLap 5, then 6
66Falseleave the loop, print Finished

Notice the sixth check. The loop does not stop at the moment count becomes 6, halfway through a pass. It finishes the pass, goes back to the top, checks, and only then leaves. Every while loop needs three things: a starting value before the loop, a condition, and a change inside the loop that moves towards making the condition False.

If the condition is False the very first time, the body never runs at all. A program that sets count = 10 and then loops while count < 5 skips straight past the loop.

Repeat until, turned inside out

Scratch's repeat until and Python's while are mirror images. Repeat until keeps going while its condition is False and stops when it turns True. While keeps going while its condition is True and stops when it turns False. To turn one into the other, put not in front of the condition.

ScratchPython
repeat until touching edge?while not touching_edge:
repeat until score > 9while score <= 9:
foreverwhile True:

The second row shows that not is often avoidable: "not more than 9" is the same as "9 or less".

When to use which loop

forwhile
Use it whenYou know the number of passes, or you are walking through a sequenceYou repeat until something changes, and cannot know how many passes in advance
Moves on by itself?Yes, to the next valueNo: your code must change something
RiskOff by oneNever stopping
ExampleThe 7 times tableAsking again until the answer is usable

Asking until the answer is usable

Lesson 9 promised a loop that keeps asking until the user gives a usable answer. Here it is. The loop body runs only while the number is outside the range.

number = int(input("Pick a number from 1 to 10: "))
while number < 1 or number > 10:
    print("That is not from 1 to 10.")
    number = int(input("Pick a number from 1 to 10: "))
print("You picked", number)
Pick a number from 1 to 10: 15
That is not from 1 to 10.
Pick a number from 1 to 10: 0
That is not from 1 to 10.
Pick a number from 1 to 10: 7
You picked 7

The change inside this loop is the new input. Without that last indented line, a bad answer would trap the user forever, printing the same complaint.

That program still crashes with a ValueError if someone types a word. To catch that, check the text before converting it. The string method isdigit is True only if every character is a digit and there is at least one.

answer = input("How many slices? ")
while not answer.isdigit():
    print("Please type a whole number, like 8.")
    answer = input("How many slices? ")
slices = int(answer)
print("Slices:", slices)
How many slices? eight
Please type a whole number, like 8.
How many slices? -3
Please type a whole number, like 8.
How many slices? 8
Slices: 8

Notice -3 was refused too, because the minus sign is not a digit. For counting slices that is exactly right.

A guessing game

Module 1 traced guess my number by hand. Now the computer can think of the number. Python's random module has a function, random.randint(1, 20), that returns a random whole number from 1 to 20, including both ends.

import random

secret = random.randint(1, 20)
guesses = 0
guess = 0
while guess != secret:
    guess = int(input("Guess my number (1 to 20): "))
    guesses = guesses + 1
    if guess < secret:
        print("Too low")
    elif guess > secret:
        print("Too high")
print(f"Got it in {guesses} guesses!")

Here is one real run, using the halving strategy from Lesson 2. Your secret number will almost certainly be different, because it is random.

Guess my number (1 to 20): 10
Too low
Guess my number (1 to 20): 15
Too high
Guess my number (1 to 20): 12
Too high
Guess my number (1 to 20): 11
Got it in 4 guesses!

Why does guess start at 0? The while line checks guess != secret before the first guess is typed, so guess must already exist, and it must hold a value that cannot be the secret. Zero is outside 1 to 20, so the loop always runs at least once. This is a while loop because nobody knows in advance how many guesses a player will need.

Leaving early with break

Sometimes the clearest way to write a loop is "keep going forever, and leave when this happens". Scratch would use forever with a stop block. Python uses while True with break, which leaves the loop at once and carries on with the first line after it.

while True:
    command = input("Command (type quit to stop): ").strip().lower()
    if command == "quit":
        break
    print("You typed", command)
print("Bye!")
Command (type quit to stop): hello
You typed hello
Command (type quit to stop): jump
You typed jump
Command (type quit to stop): quit
Bye!

while True is an infinite loop on purpose. The break is its only way out, so check that the break can really be reached. If a typo made the if compare with "quitt", the program would never end.

Common misconceptions

  • "A while loop moves on by itself, like a for loop." It does not. If nothing inside the loop changes the condition, it runs forever.
  • "The loop stops the instant the condition becomes False." The condition is only checked at the top. The rest of the pass still runs first.
  • "while works like Scratch's repeat until." It is the opposite: while repeats as long as its condition is True. Put not in front, or flip the comparison, to convert.
  • "break stops the whole program." It leaves the innermost loop only. The lines after the loop still run, which is how Bye! was printed.

Where this leaves us

The point: A while loop repeats for as long as its condition is True, so something inside it must change, or it never ends.

You traced an infinite loop to the missing line that should have changed count, and stopped one with the editor's Stop button. You saw the three things every while loop needs, why repeat until becomes while not, and when to choose while over for. You used while to keep asking until an answer was usable, with isdigit to catch words before int could crash. And you built a guessing game with random.randint, and used break to leave a while True loop on a command. With variables, choices and both kinds of loop, you have every structure from Module 1. Module 6 is about organising them: lists to hold many values, and functions to name and reuse code.

Sources

  1. Python Software Foundation. (2026). Compound statements: The while statement. The Python Language Reference. (Repeated execution as long as an expression is true; the expression may be false the first time it is tested; break terminates the loop.) docs.python.org
  2. Python Software Foundation. (2026). random: Generate pseudo-random numbers. (randint returns a random integer N with a <= N <= b.) docs.python.org
  3. Python Software Foundation. (2026). Built-in types: str.isdigit. (True if all characters are digits and there is at least one character.) docs.python.org
  4. Raspberry Pi Foundation. (2026). editor-ui end-to-end tests, spec-wc-pyodide.cy.js. GitHub. (The Stop button interrupts a running program and the editor reports that execution was interrupted; a loop that prints forever reaches an output limit.) GitHub
  5. Scratch Foundation. (2026). scratch-vm source code: scratch3_control.js. GitHub. (repeat until runs its inner blocks while its condition is false.) GitHub
Key terms
while loop
A loop that checks its condition at the top and runs its block again and again for as long as the condition is True.
Infinite loop
A loop whose condition never becomes False, so it never ends, usually because nothing inside it changes.
while True
A loop meant to run until a break inside it is reached, like Scratch's forever with a stop block.
break
A statement that leaves the innermost loop at once; the program carries on after the loop.
random.randint
A function that returns a random whole number between two values, including both ends.
isdigit
A string method that is True only if the string is not empty and every character is a digit.
Input validation
Checking a user's answer and asking again until it is usable, instead of letting the program crash.

Module 6: Lists and Functions

Organising programs: lists that hold many values under one name, read by position from 0 and walked with loops; then functions you define with def, give parameters to, and have return answers, so a big problem becomes several small tested pieces.

Lists: One Name, Many Values

  • Create a list, read items by position starting from 0, and explain the IndexError that one step too far causes.
  • Walk through a list with a for loop to total, average, count and find the largest item.
  • Change a list with item assignment, append and remove, and build a new list from user input.

Seven numbers, one name

Here are seven numbers: the steps a walker's phone counted on each day of one week, Monday first. 6200, 8150, 4300, 10020, 7600, 12340 and 5100. Suppose you want a program to find the total, the average and the best day.

With what you know so far, you would need seven variables: monday = 6200, tuesday = 8150, and so on. Adding them means typing all seven names. A month would need thirty. And a program cannot make up new variable names as it runs, so it could never handle "however many days there are". What you need is one name that holds the whole week.

That is a list: a sequence of values, in order, stored under one name. You write it in square brackets with commas between the items. This lesson follows this one list of steps through a program, and the ideas arrive as the week needs them.

steps = [6200, 8150, 4300, 10020, 7600, 12340, 5100]
print(steps)
print(len(steps))
print(steps[0])
print(steps[3])
print(steps[-1])
[6200, 8150, 4300, 10020, 7600, 12340, 5100]
7
6200
10020
5100

len gives the number of items. The square brackets after the name pick out one item by its position, called its index.

Positions start at 0

The first item is steps[0], not steps[1]. Python counts positions from 0, as the tutorial says of strings: the first character has index 0. Lists work the same way. Negative indexes count backwards from the end, so steps[-1] is always the last item, however long the list is.

DayMonTueWedThuFriSatSun
Value620081504300100207600123405100
Index0123456
Negative index-7-6-5-4-3-2-1

If you made lists in Scratch, with the Make a List button in the Variables category, watch out: Scratch numbers list items from 1, so item 1 is the first one. Python starts from 0. The same list has its first item at 1 in Scratch and at 0 in Python.

One step too far

Seven items, so surely the last one is steps[7]?

steps = [6200, 8150, 4300, 10020, 7600, 12340, 5100]
print(steps[7])
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print(steps[7])
          ~~~~~^^^
IndexError: list index out of range

An IndexError means you asked for a position that does not exist. With 7 items, the positions are 0 to 6. The last index is always one less than the length, which is the same off-by-one idea as range stopping before its stop. In fact range(len(steps)) gives exactly the valid indexes, 0 to 6. Trying to store into a position that does not exist fails in the same way: on a two-item list, steps[2] = 9050 gives IndexError: list assignment index out of range.

Walking through the week

A for loop can walk through a list just as it walked through a string. Each pass gets the next item. Here is the running total from Lesson 12, applied to the week.

steps = [6200, 8150, 4300, 10020, 7600, 12340, 5100]
total = 0
for day_steps in steps:
    total = total + day_steps
print("Total:", total)
print("Average:", round(total / len(steps)))
print(sum(steps), max(steps), min(steps))
Total: 53710
Average: 7673
53710 12340 4300

The average is the total divided by the number of days, 53710 / 7, which is about 7672.86 and rounds to 7673. The last line shows three built-in functions that save writing the loop at all: sum adds up a list of numbers, max finds the largest and min the smallest. Writing the loop yourself first is still worth it, because the loop is what you adapt when the question gets more specific.

Two lists, side by side

Numbers alone do not say which day is which. A second list can hold the names, in the same order. To walk through both together, loop over the indexes and use the same index in each list.

days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
steps = [6200, 8150, 4300, 10020, 7600, 12340, 5100]
for i in range(len(days)):
    print(days[i], steps[i])
Mon 6200
Tue 8150
Wed 4300
Thu 10020
Fri 7600
Sat 12340
Sun 5100

A list can hold strings as easily as numbers. The two lists stay matched only as long as their orders match, so if you change one, change the other in the same place.

Questions about the week

How many days reached a goal of 8000 steps? That is a loop with an if inside: a count that only goes up when the condition is True.

steps = [6200, 8150, 4300, 10020, 7600, 12340, 5100]
goal_days = 0
for day_steps in steps:
    if day_steps >= 8000:
        goal_days = goal_days + 1
print(f"Reached 8000 steps on {goal_days} of {len(steps)} days.")
Reached 8000 steps on 3 of 7 days.

Which day was best? max gives the largest number, but not where it was. For the position, use the largest-card algorithm from Lesson 2, keeping the index of the best day so far instead of the number.

steps = [6200, 8150, 4300, 10020, 7600, 12340, 5100]
best_day = 0
for i in range(len(steps)):
    if steps[i] > steps[best_day]:
        best_day = i
print("Best day is item", best_day, "with", steps[best_day], "steps")
Best day is item 5 with 12340 steps

Item 5 is Saturday, because Monday is item 0. Notice that best_day starts at 0, the first real item, not at some made-up number. That is the fix from Lesson 2 that saved the winter temperatures.

Changing a list

Unlike a string, a list can be changed after it is made. Suppose Wednesday's count was wrong and should be 4500, and an eighth day of 9050 steps needs adding.

steps = [6200, 8150, 4300, 10020, 7600, 12340, 5100]
steps[2] = 4500
steps.append(9050)
print(steps)
print(len(steps))
print(4500 in steps)
print(4300 in steps)
[6200, 8150, 4500, 10020, 7600, 12340, 5100, 9050]
8
True
False

Assigning to steps[2] replaces the item at index 2. The method append adds an item to the end, and the list grows to 8. The in operator asks whether a value is anywhere in the list: 4500 is now there, and 4300 is gone. The method remove takes a value out of a list, not a position: with names = ["Ada", "Grace", "Alan"], names.remove("Ada") leaves ['Grace', 'Alan'].

Building a list as you go

Often you do not know the values in advance. Start with an empty list, [], and append to it inside a loop.

scores = []
for round_number in range(1, 4):
    score = int(input(f"Score for round {round_number}: "))
    scores.append(score)
print(scores)
print("Best:", max(scores))
Score for round 1: 40
Score for round 2: 75
Score for round 3: 62
[40, 75, 62]
Best: 75

Strings share a lot with lists. "Python"[0] is "P", "Python"[-1] is "n", and len("Python") is 6. The big difference is that you cannot change a single character of a string in place, while you can change an item of a list.

Common misconceptions

  • "The first item is at index 1." In Python it is at index 0. Scratch lists start at 1, which makes this an easy slip when you switch.
  • "A list of 7 items has a last index of 7." The last index is 6, one less than the length. steps[-1] always gets the last item safely.
  • "append can add an item anywhere, or steps[7] = x adds an eighth item." append always adds to the end. Assigning to an index that does not exist is an IndexError.
  • "max(steps) tells you which day was best." It gives the largest value, not its position. To know the day, keep track of the index as you loop.

What you now know

Remember: A list keeps many values in order under one name, and its positions start at 0, so the last index is always the length minus 1.

You stored a week of steps in one list, read items by positive and negative index, and met the IndexError that one step too far causes. You walked the list with a loop to total and average it, used sum, max and min, and paired two lists by index. You counted the days that met a goal and found the best day with Lesson 2's algorithm. And you changed a list with item assignment, append and remove, and built one from user input. Lists let a program handle any amount of data. The next lesson lets you name a piece of code, so you can use it as often as you like.

Sources

  1. Python Software Foundation. (2026). An informal introduction to Python: Text and lists. The Python Tutorial. (The first character has index 0; negative indices count from the right, starting at -1; lists are mutable.) docs.python.org
  2. Python Software Foundation. (2026). Data structures: More on lists. The Python Tutorial. (append adds an item to the end; remove takes out the first item equal to a value.) docs.python.org
  3. Python Software Foundation. (2026). Built-in functions: len, sum, max and min. docs.python.org
  4. Scratch Wiki contributors. (2026). List. Scratch Wiki. (List blocks are in the Variables palette; each item has an index starting from 1.) Scratch Wiki
Key terms
List
A sequence of values in order, stored under one name and written in square brackets, such as [6200, 8150, 4300].
Index
An item's position in a list. Python counts from 0, so the first item is steps[0].
Negative index
A position counted from the end: steps[-1] is the last item and steps[-2] the one before it.
len
A built-in function that gives the number of items in a list, or characters in a string.
IndexError
An error meaning a program asked for a position that does not exist, such as steps[7] in a list of 7 items.
append
A list method that adds one item to the end of the list.
in
An operator that is True if a value appears anywhere in a list or string.

Functions: Making Your Own Commands

  • Define a function with def and call it, and explain why defining a function does not run it.
  • Give a function parameters and call it with arguments, including keyword arguments.
  • Match Scratch's My Blocks to Python functions, and fix the errors that come from calling a function wrongly.

An empty category

Scratch's pink-red My Blocks category starts empty. Every other category is full of blocks the Scratch team made. This one holds only a button, Make a Block, and whatever you build with it. Clicking it opens a dialog where you name a new block and add inputs to it. A define hat block then appears in the code area, and the blocks you stack under it run whenever your new block is used.

Python has the same idea, and you have been using its ready-made version since Lesson 7. print, input, len and range are all functions: named pieces of code you run by writing the name and brackets. This lesson is about making your own.

The scoreboard, written twice

Here is a scoreboard for two rounds of a game, written with no functions.

print("=" * 18)
print("Robin: 12 points")
print("Sky: 9 points")
print("=" * 18)
print("Robin: 15 points")
print("Sky: 14 points")
print("=" * 18)

It works. But the divider line is typed three times, and every score line has the same shape. If you decide the divider should be 24 characters wide, you must find and fix three lines and hope you miss none. Here is the same program built from two functions.

def divider():
    print("=" * 18)


def show_score(player, points):
    print(f"{player}: {points} points")


divider()
show_score("Robin", 12)
show_score("Sky", 9)
divider()
show_score("Robin", 15)
show_score("Sky", 14)
divider()

Both programs print exactly the same thing.

==================
Robin: 12 points
Sky: 9 points
==================
Robin: 15 points
Sky: 14 points
==================

Now the width of the divider lives in one place. Change 18 to 24 in the function, and all three dividers change. The bottom half of the program also reads almost like a description of what it does.

My Blocks beside def

ScratchPython
Click Make a Block and type a nameWrite def, the name, brackets and a colon
The define hat blockThe def line itself
Blocks stacked under defineThe indented lines under def, called the body
Add an input to the blockA name inside the brackets of the def line, called a parameter
Dragging your block into a scriptWriting the name with brackets, such as divider(), called a call
Typing a value into the block's inputPutting a value in the call's brackets, called an argument
The block belongs to one spriteThe function can be used anywhere after its def line has run

The Python tutorial gives the rules: the keyword def introduces a function definition, followed by the function name and the list of parameters in brackets, and the body starts on the next line and must be indented. Python's style guide also asks for two blank lines around each function definition at the top level of a file, which is why the examples have them. They make each function easy to spot.

Defining is not running

Predict the output of this program before reading on.

def cheer():
    print("Hooray!")


print("Program finished")
Program finished

Hooray! never appears. A def statement teaches Python a new name and stores the body for later. It does not run the body. The body runs only when the function is called, by writing cheer(). A Scratch define stack is the same: it sits in the code area doing nothing until your custom block is used in a running script.

When a call happens, Python jumps into the function, runs its body from the top, then comes back to the line after the call. Tracing the scoreboard program's first four calls looks like this.

StepLine runningOutput
1the def lines: two functions stored, nothing printed
2divider() jumps into divider, which prints, then returns==================
3show_score("Robin", 12) jumps in with player = "Robin" and points = 12Robin: 12 points
4show_score("Sky", 9) jumps in with player = "Sky" and points = 9Sky: 9 points
5divider() again==================

Parameters: the inputs of your block

A parameter is a name in the def line's brackets. It works like a variable inside the function, and each call fills it with a value, called an argument. The Python glossary puts it this way: an argument is a value passed to a function when calling it, and a parameter is a named entity in the definition that says what argument the function can accept.

def greet(name):
    print(f"Hello, {name}!")
    print("Welcome to the game.")


greet("Sky")
greet("Robin")
Hello, Sky!
Welcome to the game.
Hello, Robin!
Welcome to the game.

A function can take several parameters. The arguments are matched to them in order: the first argument to the first parameter, and so on.

def draw_box(width, height):
    for row in range(height):
        print("#" * width)


draw_box(5, 2)
print()
draw_box(3, 3)
#####
#####

###
###
###

Order matters, and it is easy to get wrong. draw_box(2, 4) draws four rows of two, not two rows of four. You can make a call clearer by naming the parameters, which are then called keyword arguments. With names, the order no longer matters.

def draw_box(width, height):
    for row in range(height):
        print("#" * width)


draw_box(2, 4)
print()
draw_box(height=2, width=4)
##
##
##
##

####
####

Three ways a call can go wrong

Calling before defining. Python runs a file from the top, so the def line must run before the first call.

greet("Sky")


def greet(name):
    print("Hello,", name)
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    greet("Sky")
    ^^^^^
NameError: name 'greet' is not defined

Too few arguments. With the same greet function defined first, calling greet() gives:

Traceback (most recent call last):
  File "main.py", line 5, in <module>
    greet()
    ~~~~~^^
TypeError: greet() missing 1 required positional argument: 'name'

Too many arguments. Calling greet("Sky", "Robin") gives:

Traceback (most recent call last):
  File "main.py", line 5, in <module>
    greet("Sky", "Robin")
    ~~~~~^^^^^^^^^^^^^^^^
TypeError: greet() takes 1 positional argument but 2 were given

Both TypeErrors tell you exactly how many arguments the function wanted. Compare the call with the def line, count, and the fix is usually obvious.

Why programmers bother

The K-12 Computer Science Framework sums up what functions are for at this level: programs use procedures to organise code, hide the details of how something is done, and make code easier to reuse. In practice that means four things.

  • One change, one place. The divider's width is set once.
  • Names that explain. show_score("Sky", 9) says what it does. The print line inside it says how.
  • Test once, trust everywhere. Check draw_box carefully with a few sizes, and every later call can rely on it.
  • Big problems in small pieces. A game becomes a set of functions, each small enough to get right. The next lesson builds a program this way.

Common misconceptions

  • "def runs the code inside it." def only stores the function. Nothing in the body runs until the function is called.
  • "You can call a function anywhere in the file." Only after its def line has run. A call on line 1 to a function defined on line 4 is a NameError.
  • "The argument has to have the same name as the parameter." Any value or variable can be passed. greet(friend) works, and inside the function its value is called name.
  • "Arguments are matched to parameters by meaning." They are matched by position, unless you name them. draw_box(2, 4) is 2 wide and 4 high, whatever you intended.

Recap

In short: def stores a named piece of code with parameters; a call runs it with arguments, and the program returns to the line after the call.

You rebuilt a scoreboard from two functions and saw the output stay exactly the same while the code got shorter and easier to change. You matched every part of a Scratch custom block to its Python form. You saw that defining is not running, traced a call into the function and back, and passed arguments by position and by name. And you read the NameError and two TypeErrors that come from calling a function wrongly. So far your functions only do things, such as printing. The next lesson's functions give answers back.

Sources

  1. Python Software Foundation. (2026). More control flow tools: Defining functions, and keyword arguments. The Python Tutorial. (def followed by the name and the parameter list in brackets; the body starts on the next line and must be indented.) docs.python.org
  2. Python Software Foundation. (2026). Glossary: argument, parameter. (An argument is a value passed when calling a function; a parameter is a named entity in the definition that specifies an argument it can accept.) docs.python.org
  3. Scratch Wiki contributors. (2026). My Blocks. Scratch Wiki. (Make a Block opens a dialog for making a procedure; the code under the define hat runs whenever the block is run; inputs can be added; the setting to run without screen refresh.) Scratch Wiki
  4. K-12 Computer Science Framework. (2016). Framework statements by concept: Algorithms and Programming, modularity. (Grades 6 to 8: programs use procedures to organise code, hide implementation details and make code easier to reuse.) k12cs.org
  5. van Rossum, G., Warsaw, B., and Coghlan, A. (2001, updated 2013). PEP 8: Style guide for Python code. (Two blank lines around top-level function definitions.) peps.python.org
Key terms
Function
A named piece of code that runs when it is called, such as print or one you define with def.
def
The keyword that defines a function: def, the name, brackets with any parameters, a colon, and an indented body.
Call
Running a function by writing its name followed by brackets, such as divider().
Parameter
A name in a function's def line that receives a value each time the function is called.
Argument
The value given to a function in a call, such as 'Sky' in greet('Sky').
Keyword argument
An argument given with its parameter's name, such as height=2, so that order does not matter.
Body
The indented lines under a def line, which run each time the function is called.

Return Values: Functions That Answer

  • Use return to send a value back from a function, and explain why a function with no return gives None.
  • Explain that variables made inside a function are local and disappear when it finishes.
  • Break a small game into functions and test each one with known inputs before combining them.

A total that came out as None

This function is meant to add up a shopping bill, and the last line printed None. Read the program and the real output, and try to explain both lines of output before reading on.

def bill_total(prices):
    total = 0
    for price in prices:
        total = total + price
    print(total)


shopping = [2.5, 4.0, 1.25]
result = bill_total(shopping)
print("Total to pay:", result)
7.75
Total to pay: None

The function clearly worked out the right answer, because 7.75 appeared. So why did result end up holding None? The plausible wrong belief is that printing a value inside a function hands it back to whoever called the function. The output proves that it does not.

print shows; return hands back

print puts a value on the screen for a person to read. It does not give the value to the rest of the program. To send a value back to the line that called the function, a function needs return. When Python reaches a return statement, it leaves the function immediately, and the call is replaced by the value after return.

A function that never reaches a return still gives something back. The Python tutorial explains that even functions without a return statement return a value, a rather boring one called None, and that falling off the end of a function also returns None. None is Python's way of saying "no value". That is what landed in result.

print(total)return total
Who gets the valueThe person looking at the screenThe line that called the function
Can the program use it later?NoYes: store it, add to it, compare it
Does the function carry on?Yes, to the next lineNo, it ends at once
What the call gives backNone, if there is no returnThe value after return

The fix

def bill_total(prices):
    total = 0
    for price in prices:
        total = total + price
    return total


shopping = [2.5, 4.0, 1.25]
result = bill_total(shopping)
print("Total to pay:", result)
print("Each of 2 friends pays:", bill_total(shopping) / 2)
Total to pay: 7.75
Each of 2 friends pays: 3.875

Now the call bill_total(shopping) is worth 7.75, and you can use it anywhere a number could go: stored in a variable, or divided by 2 inside another expression. That is the real power of return. A function that returns a value can be built into bigger calculations, the way len and max are.

The same slip has a smaller form. This function is meant to double a number, but the line in its body works out the answer and then throws it away.

def double(number):
    number * 2


print(double(21))
None

Nothing is wrong with number * 2. The answer just never leaves the function. Writing return number * 2 makes print(double(21)) show 42.

return ends the function

Because return leaves at once, a function can use it to stop as soon as it knows the answer.

def grade(score):
    if score >= 85:
        return "A"
    if score >= 70:
        return "B"
    if score >= 50:
        return "C"
    return "Not passed yet"


print(grade(95), grade(72), grade(50), grade(10))
A B C Not passed yet

In Lesson 11, three separate ifs printed three grades for one score. Here they do not, because the first return that runs ends the function. For 95, return "A" runs and the other ifs are never reached. This is a common, readable way to write a choice inside a function.

A function can also return True or False, which makes its call usable as a condition.

def is_even(number):
    return number % 2 == 0


print(is_even(4), is_even(7), is_even(0))
True False True

Now if is_even(count): reads almost like English.

What happens inside stays inside

Variables created inside a function are local: they exist only while the function runs, and the rest of the program cannot see them. The Python reference puts it formally: if a name is bound in a block, such as a function body, it is a local variable of that block.

def bill_total(prices):
    total = 0
    for price in prices:
        total = total + price
    return total


bill_total([1, 2])
print(total)
Traceback (most recent call last):
  File "main.py", line 9, in <module>
    print(total)
          ^^^^^
NameError: name 'total' is not defined

total was created inside bill_total and vanished when the function finished. The parameter prices is local too. This is a feature, not a nuisance. It means a function cannot accidentally change a variable somewhere else in a long program, and two functions can both use a variable called total without getting in each other's way. The one way a value leaves a function is through return.

Building a game from small tested pieces

Breaking a big problem into small pieces that can be built and checked one at a time is called decomposition. Here is a tiny dice game, decomposed into two functions: one rolls a die, and one decides who won.

import random


def roll_die():
    return random.randint(1, 6)


def winner(first, second):
    if first > second:
        return "Player 1"
    elif second > first:
        return "Player 2"
    return "nobody, it is a draw"

Before using winner in a game full of random rolls, test it on its own with inputs whose answers you already know. Random numbers make a bad test, because you cannot repeat them on purpose. Known inputs do not have that problem.

print(winner(5, 2))
print(winner(1, 6))
print(winner(3, 3))
Player 1
Player 2
nobody, it is a draw
TestExpectedGot
winner(5, 2)Player 1Player 1
winner(1, 6)Player 2Player 2
winner(3, 3), the edge casea drawnobody, it is a draw

All three pass, including the draw, which is the case most likely to be forgotten. Now the game itself is short, because the thinking is inside the functions.

for round_number in range(1, 4):
    roll_1 = roll_die()
    roll_2 = roll_die()
    print(f"Round {round_number}: {roll_1} against {roll_2}, winner: {winner(roll_1, roll_2)}")

Here is one real run of the whole program, with the functions above it. Your rolls will be different.

Round 1: 3 against 2, winner: Player 1
Round 2: 4 against 6, winner: Player 2
Round 3: 1 against 1, winner: nobody, it is a draw

If this game ever printed a wrong winner, you would already know winner was not to blame: it passed its tests. You would look at the loop instead. Tested pieces shrink the places a bug can hide.

Common misconceptions

  • "Printing inside a function gives the value back." print only shows it. Without return, the call gives back None.
  • "A function keeps running after return." return ends the function immediately. Lines after it in the same block do not run.
  • "Variables made in a function can be used afterwards." They are local and vanish when the function ends. Return the value and store it if you need it.
  • "Random programs can only be tested by running them lots of times." Split out the parts that do not depend on chance, like winner, and test those with known inputs.

Looking back

Why this matters: return turns a function from something that does a job into something that answers a question, and answers can be combined into bigger programs.

You traced a bill that printed 7.75 and returned None, and fixed it with one word. You know that a function with no return gives back None, that return ends the function at once, and that a function can return True or False to be used as a condition. You saw that variables inside a function are local and disappear afterwards. And you built a dice game by decomposition, testing winner with known inputs before trusting it with random rolls. That habit, testing in small pieces, is the bridge to the next module, which treats debugging as a method.

Sources

  1. Python Software Foundation. (2026). More control flow tools: Defining functions. The Python Tutorial. (return returns a value from a function; functions without a return statement return None; falling off the end of a function also returns None; assignments in a function store values in the local symbol table.) docs.python.org
  2. Python Software Foundation. (2026). Execution model: Naming and binding. The Python Language Reference. (A name bound in a block is a local variable of that block.) docs.python.org
  3. Python Software Foundation. (2026). random: Generate pseudo-random numbers. (randint for the dice rolls.) docs.python.org
  4. K-12 Computer Science Framework. (2016). Framework statements by concept: Algorithms and Programming, modularity and program development. (Grades 6 to 8: procedures organise code and make it easier to reuse.) k12cs.org
Key terms
return
A statement that ends a function and sends a value back to the line that called it.
Return value
The value a call is replaced by, such as 7.75 for bill_total(shopping).
None
Python's value meaning 'no value'. A function that ends without return gives back None.
Local variable
A variable created inside a function. It exists only while the function runs and cannot be seen outside it.
Decomposition
Breaking a big problem into small pieces, such as functions, that can be built and tested one at a time.
Known-input test
Calling a function with inputs whose correct answer you already know, and comparing what it returns.

Module 7: Debugging as a Method

Finding mistakes on purpose rather than by luck: reading a traceback from the bottom up, knowing the common error types and what usually causes each, and a four-step method of reading, locating, forming a hypothesis and testing one change; then tracking down bugs that give wrong answers with no error message at all.

Reading an Error Message

  • Tell a syntax error, which stops a program before it starts, from a runtime error, which stops it partway.
  • Read a traceback from the bottom up, including one that passes through a function call.
  • Name the common error types, say what usually causes each, and fix errors one at a time with a four-step method.

A moth in Relay 70

On 9 September 1947, operators testing the Mark II Aiken Relay Calculator at Harvard University found a moth trapped between the contacts of Relay 70, Panel F. They taped the moth into the machine's logbook, next to the entry "First actual case of bug being found." In 1988 that logbook, with the moth still taped beside the entry, was kept in a computer museum at the Naval Surface Warfare Center in Dahlgren, Virginia.

Read the note again: first actual case. The joke only works because engineers already called faults "bugs". The operators had simply found one that was really an insect. Today almost every bug is a mistake in a program, and finding and fixing them is called debugging. Your best tool for it is something the Mark II operators did not have: a message from Python saying where it got stuck and why.

Research on programming error messages agrees that beginners find them hard, and a 2008 review of debugging research described debugging as both difficult for new programmers to learn and challenging to teach. This lesson takes the messages apart so they stop being hard.

Two kinds of error

Compare these two broken programs and their real output.

print("Starting")
lives = 3
print("Lives: " + lives)
print("Done")
Starting
Traceback (most recent call last):
  File "main.py", line 3, in <module>
    print("Lives: " + lives)
          ~~~~~~~~~~^~~~~~~
TypeError: can only concatenate str (not "int") to str
print("Starting")
lives = 3
print("Lives: " + lives
print("Done")
  File "main.py", line 3
    print("Lives: " + lives
         ^
SyntaxError: '(' was never closed

The first program printed Starting, then stopped at line 3. That is a runtime error, also called an exception: the program started, ran lines 1 and 2, and hit a problem on line 3. Line 4 never ran.

The second program printed nothing, not even Starting, although line 1 is fine. That is a syntax error. Before running anything, Python reads the whole file to understand it, and a line it cannot understand stops the program before it begins. The Python tutorial makes the same split: syntax errors, and exceptions that happen while a correct-looking program runs.

Anatomy of a traceback

A runtime error prints a traceback, a report of where the program was when it stopped. Here is one that passes through a function.

def average(numbers):
    return sum(numbers) / len(numbers)


print(average([4, 8]))
print(average([]))
6.0
Traceback (most recent call last):
  File "main.py", line 6, in <module>
    print(average([]))
          ~~~~~~~^^^^
  File "main.py", line 2, in average
    return sum(numbers) / len(numbers)
           ~~~~~~~~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zero
PartWhat it tells you
Traceback (most recent call last):A runtime error follows. The most recent step is at the bottom.
File "main.py", line 6, in <module>The main program was running line 6, which called a function.
File "main.py", line 2, in averageInside the function average, line 2 was running when it stopped.
The code line with ~ and ^ under itThe exact part of the line Python was working on.
ZeroDivisionError: division by zeroThe error type, then a description. Read this first.

Read a traceback from the bottom up. The last line says what went wrong: dividing by zero. The line above it says where: line 2, inside average. The lines above that say how the program got there: line 6 called average with an empty list, so len(numbers) was 0. The first call, with [4, 8], worked and printed 6.0. The bug is not in the arithmetic. It is that nobody decided what the average of an empty list should be.

The errors you will meet most

Every message below is real output from a one- or two-line program saved as main.py.

Error typeUsually meansReal messageFirst thing to check
SyntaxErrorPython cannot read the line'(' was never closed, or expected ':'Brackets, quote marks and colons in pairs and in place
IndentationErrorSpaces at the start of a line are wrongunexpected indent, or expected an indented block after 'if' statement on line 2Four spaces under if, for, while and def, and none elsewhere
NameErrorA name Python does not knowname 'score' is not defined. Did you mean: 'scroe'?Spelling, capitals, and whether the name was set before this line
TypeErrorThe wrong type of value for an operationcan only concatenate str (not "int") to strMixing strings and numbers; the number of arguments in a call
ValueErrorThe right type but an unusable valueinvalid literal for int() with base 10: 'twelve'What the user actually typed
IndexErrorA list position that does not existlist index out of rangeIndexes start at 0 and end at the length minus 1
ZeroDivisionErrorDividing by zerodivision by zeroWhere the zero came from, such as an empty list
AttributeErrorA method that does not exist for that value'str' object has no attribute 'uppercase'The method's name: here it is upper

Learning the eight names is worth it. Once you know that a TypeError almost always means "two kinds of value were mixed", half the search is already done.

Where Python noticed, and where you went wrong

The line in the message is where Python got stuck. It is not always the line with the mistake.

scroe = 10
bonus = 5
print(score + bonus)
Traceback (most recent call last):
  File "main.py", line 3, in <module>
    print(score + bonus)
          ^^^^^
NameError: name 'score' is not defined. Did you mean: 'scroe'?

Line 3 is spelled correctly. The typo is on line 1, which created a variable called scroe. Python only noticed when line 3 asked for score. The suggestion at the end, "Did you mean: 'scroe'?", is the clue that points back to line 1. So go to the line the message names, then look upwards at the lines that set up the values it uses.

In the Code Editor

The Code Editor shows the same information squeezed into one line, with the line number and file at the end, such as NameError: name 'pirnt' is not defined. Did you mean: 'print'? on line 2 of main.py. Below it, a plain-English hint explains the error type. The editor runs Python 3.12 rather than the 3.14 used for the outputs here, and turtle programs run on a different engine, so the wording can differ a little. The error type and the line number are what you work from, and they agree.

A four-step method

  1. Read the last line: the error type and the description.
  2. Locate the line it names, then look at the lines just above it.
  3. Hypothesise: say in one sentence what you think is wrong and why, such as "I think price is misspelled, because the message suggests a similar name."
  4. Test one change: make the single change your hypothesis suggests, run again, and see whether this error has gone.

Here is the method used on one program with three mistakes. It should print the average of three prices.

prices = [3, 5, 2]
total = 0
for price in prices
    total = total + prise
print("Average: " + total / len(prices))
  File "main.py", line 3
    for price in prices
                       ^
SyntaxError: expected ':'

Read: a SyntaxError, expected ':'. Locate: line 3, the for line. Hypothesis: the for line is missing its colon. Test: add the colon and run.

Traceback (most recent call last):
  File "main.py", line 4, in <module>
    total = total + prise
                    ^^^^^
NameError: name 'prise' is not defined. Did you mean: 'price'?

A different error is progress: the colon fix worked, and Python got further. Read: NameError. Locate: line 4. Hypothesis: prise is a misspelling of the loop variable price. Test: correct it and run.

Traceback (most recent call last):
  File "main.py", line 5, in <module>
    print("Average: " + total / len(prices))
          ~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~
TypeError: can only concatenate str (not "float") to str

Read: TypeError, a string and a float joined with +. Locate: line 5. Hypothesis: total / len(prices) is a number, so + cannot join it to a string. Test: let print join them with a comma, and round the answer.

prices = [3, 5, 2]
total = 0
for price in prices:
    total = total + price
print("Average:", round(total / len(prices), 2))
Average: 3.33

Three runs, three errors, each fixed by one change. Notice the order the errors arrived in. The syntax error came first, because Python reads the whole file before running it. The runtime errors then came in the order the lines run. And because each run tested exactly one change, you always knew which change fixed what.

Common misconceptions

  • "The line in the error is always the line with the mistake." It is where Python got stuck. A typo on line 1 can surface on line 3.
  • "Read a traceback from the top." The most useful part, the error type and description, is the last line.
  • "A new error means I made things worse." Often it means the last fix worked and Python got further. Check whether the line number has moved on.
  • "Change everything that looks wrong, then run once." If several changes go in together, you cannot tell which one mattered, or whether one of them added a new bug.

Summing up

The upshot: An error message is evidence. Read its last line, go to the line it names, say what you think is wrong, and test that idea with one change.

You met the first recorded "actual" bug, a moth in 1947, and saw that the word was already in use. You can tell a syntax error, which stops everything before the first line runs, from a runtime error, which stops partway. You can read a traceback from the bottom up, including one that passes through a function, and you know eight common error types and what to check first for each. You learned that Python names the line where it noticed, not always the line where you slipped. And you fixed three errors in three runs with the four-step method. The next lesson faces harder bugs: the ones that produce no message at all.

Sources

  1. Naval History and Heritage Command. (n.d.). NH 96566-KN: The first "computer bug". (The moth found at Relay 70, Panel F, of the Mark II Aiken Relay Calculator at Harvard on 9 September 1947, and the log entry "First actual case of bug being found".) history.navy.mil
  2. Python Software Foundation. (2026). Errors and exceptions. The Python Tutorial. (Syntax errors compared with exceptions raised while a program runs, and how tracebacks report them.) docs.python.org
  3. Python Software Foundation. (2026). Built-in exceptions. (TypeError, ValueError, IndexError, NameError, ZeroDivisionError, AttributeError and the rest.) docs.python.org
  4. Becker, B. A., Denny, P., Pettit, R., Bouchard, D., Bouvier, D. J., Harrington, B., Kamil, A., Karkare, A., McDonald, C., Osera, P.-M., Pearce, J. L., and Prather, J. (2019). Compiler error messages considered unhelpful: The landscape of text-based programming error message research. In Proceedings of the Working Group Reports on Innovation and Technology in Computer Science Education (pp. 177-210). ACM. https://doi.org/10.1145/3344429.3372508 (Abstract read: error messages present substantial difficulty, particularly for novices.)
  5. McCauley, R., Fitzgerald, S., Lewandowski, G., Murphy, L., Simon, B., Thomas, L., and Zander, C. (2008). Debugging: A review of the literature from an educational perspective. Computer Science Education, 18(2), 67-92. https://doi.org/10.1080/08993400802114581 (Abstract read: debugging is difficult for novices to learn and challenging to teach.)
Key terms
Debugging
Finding and fixing the mistakes, or bugs, in a program.
Syntax error
A mistake that stops Python understanding the file, so no line runs at all.
Runtime error
An error, also called an exception, that stops a program partway through; earlier lines have already run.
Traceback
Python's report of where a program was when a runtime error stopped it, read from the bottom up.
Error type
The name at the start of the last line, such as TypeError or IndexError, which says what kind of problem it was.
Hypothesis
A one-sentence guess about the cause of a bug, which you then test with a single change.
ZeroDivisionError
The error raised when a program divides by zero, often because a list was empty.

Bugs With No Error Message

  • Recognise a logic bug: a program that runs without complaint but gives a wrong answer.
  • Find a logic bug by working out the right answer first, then printing values at each step.
  • Design tests that include normal values, boundaries and edge cases, and rerun them after every fix.

An average bigger than every score

The program below says the average of 90, 80 and 70 is 193.33.

first = 90
second = 80
third = 70
average = first + second + third / 3
print("Average:", round(average, 2))
Average: 193.33

There is no traceback. No line is named. Python ran every line and printed a number with complete confidence. Yet one glance says it is wrong: an average can never be bigger than the largest value, and 193.33 is more than twice 90.

This is a logic bug: the program is valid Python, so Python has nothing to complain about, but it does not do what its writer meant. These are the hardest bugs to find, because the error message, your best clue from the last lesson, does not exist. This lesson replaces it with a method.

Why Python stayed quiet

Python did exactly what the line says. Like maths, Python does division before addition, so first + second + third / 3 means 90 + 80 + (70 / 3), which is 170 + 23.33. The writer meant "add them all up, then divide". Python cannot read intentions, only code. The Python tutorial points out that brackets can be used for grouping, and that is the fix: (first + second + third) / 3.

But knowing the fix is the end of the story. The useful part is how you would find it in a program you did not just read about.

Step 1: know the right answer first

You cannot spot a wrong answer unless you know the right one. Before running a program, work out what it should print for one simple input, by hand or with a calculator. For 90, 80 and 70: the total is 240, and 240 divided by 3 is 80. That number, the expected output, is what turns "193.33" from a number into evidence.

A quick sense check helps too. Averages lie between the smallest and the largest value. Counts are whole numbers and never negative. A total of prices should be more than any single price. A result that breaks a rule like that is a bug, even before you know what the exact answer should be.

Then reproduce the bug: find an input that makes it happen every time. A bug you can make happen on purpose can be studied. One that only shows up sometimes has to be pinned down first.

Step 2: look inside with print

A program hides its working. Add print lines that show the values in the middle of the calculation, and the hiding stops. These are called debugging prints.

first = 90
second = 80
third = 70
print("third / 3 is", third / 3)
print("first + second is", first + second)
average = first + second + third / 3
print("Average:", round(average, 2))
third / 3 is 23.333333333333332
first + second is 170
Average: 193.33

Now the story is visible: 170 plus 23.33 makes 193.33. Only one number, 70, was ever divided.

Steps 3 and 4: one hypothesis, one change

Hypothesis: "The division is applied to third alone, because Python divides before it adds." Change one thing: add the brackets. Run it again.

first = 90
second = 80
third = 70
average = (first + second + third) / 3
print("Average:", round(average, 2))
Average: 80.0

80.0 matches the expected 80. Delete the debugging prints so the output is clean again. This is the same four-step method as for error messages. The only difference is where the clue comes from: from Python's message before, and from your own expected answer now.

A count that is one short

Here is a second logic bug. The pass mark is 50, so a score of exactly 50 passes. Five students scored 55, 72, 49, 50 and 91. By hand, four passed: everyone except 49.

scores = [55, 72, 49, 50, 91]
passed = 0
for score in scores:
    if score > 50:
        passed = passed + 1
print(passed, "students passed")
3 students passed

Expected 4, got 3. Which student went missing? Print inside the loop and watch the count change.

scores = [55, 72, 49, 50, 91]
passed = 0
for score in scores:
    if score > 50:
        passed = passed + 1
    print("checked", score, "- passed so far:", passed)
print(passed, "students passed")
checked 55 - passed so far: 1
checked 72 - passed so far: 2
checked 49 - passed so far: 2
checked 50 - passed so far: 2
checked 91 - passed so far: 3
3 students passed

The count did not move at 50. Hypothesis: "> 50 leaves out exactly 50, the boundary." One change: > becomes >=. The fixed program prints "4 students passed". This is the boundary bug from Lesson 10, found here without being told where to look. The debugging print pointed straight at it.

Choosing tests that catch bugs

Both bugs in this lesson would have been caught earlier by better tests. A good set of tests mixes three kinds of input.

Kind of testWhat it isFor the passing count
NormalOrdinary, everyday inputscores well above and below 50
BoundaryExactly on, just below and just above a limit49, 50 and 51
Edge caseInput at the extremes of what is possiblean empty list; every score the same; one student

Writing the expected answer next to each test makes checking quick. Here is an average function with its tests in the same program, each printed beside the answer it should give.

def average(numbers):
    if len(numbers) == 0:
        return 0
    return sum(numbers) / len(numbers)


print(average([90, 80, 70]), "expected 80.0")
print(average([5]), "expected 5.0")
print(average([-4, 4]), "expected 0.0")
print(average([]), "expected 0, our choice for an empty list")
80.0 expected 80.0
5.0 expected 5.0
0.0 expected 0.0
0 expected 0, our choice for an empty list

The empty list is the interesting one. Without the if, it would crash with a ZeroDivisionError, as in the last lesson. With it, the function returns 0. Is 0 the right average for no numbers at all? That is a decision, not a fact, and the test makes you take it on purpose.

After any fix, run all the tests again, not just the one that failed. A change that fixes one case can break another, and the only way to know is to check.

When you are stuck

  • Shrink the input. Find the smallest input that still shows the bug. Three numbers are easier to trace than three hundred.
  • Trace it on paper. A trace table of every variable, pass by pass, shows the first moment a value goes wrong.
  • Explain it aloud. Say what each line does, one line at a time, to a friend or even to an object on your desk. Programmers call this rubber duck debugging. Saying "and then this divides the total..." is often the moment you hear that it does not.
  • Keep a bug diary. For each bug, note the symptom, the expected result, your hypothesis, the change and what happened. Patterns appear quickly: most people make the same few mistakes again and again.
SymptomExpectedHypothesisChangeResult
Average 193.3380Only third is dividedAdded bracketsFixed: 80.0
3 passed4> leaves out 50Changed > to >=Fixed: 4

Common misconceptions

  • "No error message means the program is right." It only means the program is valid Python. Whether it is right depends on what it was meant to do, which only you know.
  • "Python works out sums from left to right." It follows the same order as maths: powers, then multiplication and division, then addition and subtraction. Brackets come first of all.
  • "If the program works for one input, it works." One test checks one case. Boundaries and edge cases are where logic bugs live.
  • "Debugging prints should stay in once the bug is fixed." Take them out, or the program's real output gets buried in working notes.

The takeaway

Bottom line: When there is no error message, your expected answer is the clue. Work it out first, look inside with print, then change one thing and rerun every test.

You found two logic bugs that Python could never report: an average that divided only one number, and a count that left out the boundary score. You worked out the right answer before trusting the program, used debugging prints to see the values in the middle, and fixed each bug with one change. You know the three kinds of test, normal, boundary and edge, and that every test reruns after every fix. And when stuck, you can shrink the input, trace on paper, explain the code aloud, and keep a bug diary. With this, the course turns to building things: four projects, each built and tested in stages.

Sources

  1. Python Software Foundation. (2026). An informal introduction to Python: Numbers. The Python Tutorial. (The arithmetic operators, with brackets used for grouping.) docs.python.org
  2. Python Software Foundation. (2026). Expressions: Operator precedence. The Python Language Reference. (The precedence table, in which / binds more tightly than +.) docs.python.org
  3. McCauley, R., Fitzgerald, S., Lewandowski, G., Murphy, L., Simon, B., Thomas, L., and Zander, C. (2008). Debugging: A review of the literature from an educational perspective. Computer Science Education, 18(2), 67-92. https://doi.org/10.1080/08993400802114581 (Abstract read: what causes bugs, what kinds occur, the debugging process, and how to teach it.)
  4. K-12 Computer Science Framework. (2016). Framework statements by concept: Algorithms and Programming, program development. (Grades 6 to 8: designing solutions for others and testing whether their criteria and constraints were met.) k12cs.org
Key terms
Logic bug
A mistake that makes a valid program give the wrong result, with no error message.
Expected output
The result a program should give for a test input, worked out before the program is run.
Reproduce
To find an input that makes a bug happen every time, so it can be studied.
Debugging print
A temporary print line that shows values partway through a program, removed once the bug is fixed.
Test case
One input together with its expected output, chosen to check one thing about a program.
Edge case
An input at the extremes of what is possible, such as an empty list or a single item.
Operator precedence
The order Python applies operators in: division before addition, unless brackets say otherwise.

Module 8: Projects: Drawing and Playing

Three projects built in stages, each stage run and tested before the next: a turtle drawing of a night scene made from shape functions, a quiz game that forgives capital letters and spaces and keeps score, and a text adventure with rooms, an inventory and a game loop.

Project: A Turtle Drawing, Step by Step

  • Steer a turtle with forward, left, penup, goto and setheading, and predict where each line will go.
  • Write shape functions with parameters and use them, with fill colours and dots, to build a scene.
  • Build a drawing in stages, running and checking each stage before adding the next.

A turtle from 1967

Python's turtle module is an implementation of the drawing tools introduced in Logo, a language developed in 1967 by Wally Feurzeig, Seymour Papert and Cynthia Solomon. The idea is simple enough for a young child and deep enough for geometry: a turtle carries a pen across the floor, and you steer it with commands like "forward 100" and "left 90". You have already met the idea twice, as Scratch's Pen extension and in the grid walk of Lesson 1.

A new turtle starts at (0, 0), the middle of the drawing area, facing right, which is east. x grows to the right and y grows upwards, like a maths graph. In the Code Editor, any program that imports turtle shows its drawing in a Visual output panel beside the text output. The editor runs turtle programs on a separate engine, Skulpt, so this project sticks to commands that both it and ordinary Python support. Its drawing area is 400 by 400, so every shape here stays between -200 and 200.

Because this page cannot show you the picture, each stage below is followed by a description of exactly what it draws. Every program was run, and the positions given are the ones the turtle really reached.

The plan: a night scene in five stages

StageWhat it addsIdea it uses
1A squarefor loops and the 360 rule
2One function for any regular shapefunctions with parameters
3A house of three filled shapesfunctions calling functions
4A dark sky, stars and a moonrandom numbers in a loop
5Ground and a row of treesrange with a step

Run the program after every stage. If a shape lands in the wrong place, you will know it came from the stage you just added.

Stage 1: a square

import turtle

t = turtle.Turtle()
for side in range(4):
    t.forward(100)
    t.left(90)

turtle.done()

What it draws: a black square outline, 100 by 100, sitting above and to the right of the centre. The turtle walks right to (100, 0), turns left to face up, walks to (100, 100), turns to face left, walks to (0, 100), turns to face down, and walks home to (0, 0), facing right again.

Turning left means turning anticlockwise, so the square goes up from the starting line. Scratch's square in Lesson 4 turned right and went down. turtle.done() belongs at the end of every turtle program. The documentation says it must be the last statement; in ordinary Python it keeps the drawing window open until you close it.

Stage 2: one function for every shape

The 360 rule from Lesson 4 works here too: a regular shape with n sides turns 360 / n at each corner. Put that in a function and it draws any of them. A second function, move_to, lifts the pen, jumps to a point, and puts the pen down, so moving between shapes leaves no line.

import turtle


def polygon(t, sides, length):
    for side in range(sides):
        t.forward(length)
        t.left(360 / sides)


def move_to(t, x, y):
    t.penup()
    t.goto(x, y)
    t.pendown()


pen = turtle.Turtle()
pen.speed(0)
move_to(pen, -180, 0)
polygon(pen, 3, 80)
move_to(pen, -40, 0)
polygon(pen, 5, 50)
move_to(pen, 100, 0)
polygon(pen, 8, 35)

turtle.done()

What it draws: three outlines in a row, each sitting on the horizontal line through the centre. On the left, a triangle with sides of 80, from x -180 to -100, with its top point at (-140, 69.3). In the middle, a pentagon with sides of 50, its top corner at (-15, 76.9). On the right, an octagon with sides of 35, from x 75.3 to 159.7 and 84.5 tall. speed(0) means draw as fast as possible, so all three appear almost at once.

Notice that polygon takes the turtle t as a parameter. That lets the same function steer any turtle you pass to it.

Stage 3: a house from three shapes

A filled shape needs two more commands. color sets the colour, and the shape drawn between begin_fill() and end_fill() is filled in when end_fill runs. A third function puts these together, and a house function builds a house out of it.

import turtle


def move_to(t, x, y):
    t.penup()
    t.goto(x, y)
    t.pendown()


def polygon(t, sides, length):
    for side in range(sides):
        t.forward(length)
        t.left(360 / sides)


def filled_polygon(t, sides, length, colour):
    t.color(colour)
    t.begin_fill()
    polygon(t, sides, length)
    t.end_fill()


def house(t, x, y):
    move_to(t, x, y)
    t.setheading(0)
    filled_polygon(t, 4, 100, "saddlebrown")
    move_to(t, x, y + 100)
    filled_polygon(t, 3, 100, "firebrick")
    move_to(t, x + 40, y)
    t.color("gold")
    t.begin_fill()
    for side in range(2):
        t.forward(20)
        t.left(90)
        t.forward(40)
        t.left(90)
    t.end_fill()


pen = turtle.Turtle()
pen.speed(0)
house(pen, -50, -100)

turtle.done()

What it draws: a brown square for the walls, from (-50, -100) to (50, 0). On top of it, a dark red triangle for the roof, the same width as the walls, with its peak at (0, 86.6). In the middle of the bottom edge, a gold rectangle 20 wide and 40 tall: a lit doorway from (-10, -100) to (10, -60).

house takes x and y as parameters, so a second call such as house(pen, 60, -100) would build another house next door. setheading(0) points the turtle right before each house is drawn. Without it, a house drawn after some other shape would start at whatever angle that shape left behind, and come out tilted. It is initialising again, from Lesson 3: each function sets up its own starting state.

Stages 4 and 5: sky, ground and trees

The final program adds three things. screen.bgcolor colours the whole background. dot(size, colour) draws a filled circle where the turtle stands, which makes stars and a moon easy. And random positions spread the stars across the sky.

import random
import turtle


def move_to(t, x, y):
    t.penup()
    t.goto(x, y)
    t.pendown()


def polygon(t, sides, length):
    for side in range(sides):
        t.forward(length)
        t.left(360 / sides)


def filled_polygon(t, sides, length, colour):
    t.color(colour)
    t.begin_fill()
    polygon(t, sides, length)
    t.end_fill()


def house(t, x, y):
    move_to(t, x, y)
    t.setheading(0)
    filled_polygon(t, 4, 100, "saddlebrown")
    move_to(t, x, y + 100)
    filled_polygon(t, 3, 100, "firebrick")
    move_to(t, x + 40, y)
    t.color("gold")
    t.begin_fill()
    for side in range(2):
        t.forward(20)
        t.left(90)
        t.forward(40)
        t.left(90)
    t.end_fill()


def tree(t, x, y):
    move_to(t, x, y)
    t.setheading(0)
    filled_polygon(t, 4, 10, "sienna")
    move_to(t, x - 15, y + 10)
    filled_polygon(t, 3, 40, "darkgreen")


def ground(t):
    move_to(t, -200, -100)
    t.setheading(0)
    t.color("darkolivegreen")
    t.begin_fill()
    for side in range(2):
        t.forward(400)
        t.right(90)
        t.forward(100)
        t.right(90)
    t.end_fill()


screen = turtle.Screen()
screen.bgcolor("midnightblue")
pen = turtle.Turtle()
pen.speed(0)
pen.hideturtle()

for star in range(40):
    move_to(pen, random.randint(-190, 190), random.randint(0, 190))
    pen.dot(4, "white")

move_to(pen, 120, 120)
pen.dot(60, "lightyellow")

ground(pen)
house(pen, -50, -100)
for x in range(-180, -70, 40):
    tree(pen, x, -100)
for x in range(80, 190, 40):
    tree(pen, x, -100)

turtle.done()

What it draws: a dark blue night sky filling the whole area. Forty small white stars are scattered across the top half, at different places every time you run it, because their positions are random. A pale yellow moon, 60 across, hangs at (120, 120), upper right. Across the bottom, an olive-green strip of ground runs the full width, from y -100 down to -200. The house from Stage 3 stands on it in the middle. Three trees stand to its left, at x -180, -140 and -100, and three to its right, at 80, 120 and 160: each a small brown trunk with a dark green triangle above it, the triangles of each group touching to make a little wood. No arrowhead shows, because hideturtle hid it.

Read the two tree loops carefully. range(-180, -70, 40) gives -180, -140 and -100, then stops before -70. That is the stop rule from Lesson 12, doing real work: it puts three trees in exactly the gap between the left edge and the house.

Checking a drawing before you see it

A drawing can be traced like any other program. Before running a stage, write down the corners you expect. For the house walls starting at (-50, -100) with sides of 100: (-50, -100), (50, -100), (50, 0), (-50, 0). If the picture disagrees, the trace tells you which step to look at. You can also ask the turtle where it is: print(pen.xcor(), pen.ycor()) shows its position in the text output.

The most common turtle bugs, and what they look like:

SymptomLikely cause
Thin lines join shapes that should be separateThe pen was not lifted: use move_to, with penup and pendown
An outline appears but no colourend_fill() is missing, or begin_fill() came after the shape
A shape comes out tiltedThe heading was not reset with setheading(0)
A triangle does not closeThe turn is the inside angle, 60, instead of 120
Part of the scene is missingIt was drawn outside the area: check the coordinates

Common misconceptions

  • "goto always draws a line." It draws only while the pen is down. Between penup and pendown it simply moves.
  • "color() fills in the last shape drawn." Filling happens at end_fill(), for the shape drawn since begin_fill(). Setting a colour afterwards changes only what comes next.
  • "left and right turn the drawing area." They turn the turtle. The axes never move: y is always up and x is always right.
  • "A random program draws the same picture every time." The stars move on every run. Everything not using random stays exactly where it was.

Pulling it together

What matters here: A scene is a set of small shape functions, each given a position and starting from a known heading, called in the right order.

You steered a turtle with forward, left, penup, goto and setheading, and drew any regular shape with one function using the 360 rule. You built filled shapes with begin_fill and end_fill, combined them into house and tree functions with x and y parameters, and added a sky with bgcolor, random stars and a moon with dot. You used range with a step to place the trees, and you have a table of turtle bugs and their usual causes. Most of all, you built it in five stages and checked each one. The next project uses the same habit to build a game.

Sources

  1. Python Software Foundation. (2026). turtle: Turtle graphics, Python 3.12 documentation. (An implementation of the drawing tools introduced in Logo, developed by Wally Feurzeig, Seymour Papert and Cynthia Solomon in 1967; the start at (0, 0) facing east; color, begin_fill, end_fill, dot, setheading, bgcolor, speed, hideturtle; done() as the last statement.) docs.python.org
  2. Raspberry Pi Foundation. (2026). editor-ui source code, PythonRunner.jsx. GitHub. (Programs that import turtle run on Skulpt and draw in the Visual output panel; observed 16 October 2026 with a 400 by 400 drawing area.) GitHub
  3. Python Software Foundation. (2026). random: Generate pseudo-random numbers. (randint for the star positions.) docs.python.org
  4. Papert, S. (1980). Mindstorms: Children, computers, and powerful ideas. Basic Books. (The case for learning geometry by steering a turtle.)
Key terms
turtle
A Python module that draws by moving a pen you steer with commands such as forward and left.
Heading
The direction the turtle faces, in degrees: 0 is right, 90 is up. setheading(0) points it right.
penup and pendown
Lift the pen so the turtle moves without drawing, and put it down to draw again.
begin_fill and end_fill
Bracket a shape so it is filled with the fill colour when end_fill() runs.
dot
Draws a filled circle of a given size and colour where the turtle stands.
Visual output
The Code Editor panel where turtle drawings appear, next to the text output.
turtle.done()
The last line of a turtle program. In ordinary Python it keeps the drawing window open.

Project: A Quiz Game

  • Build a quiz game in versions, from one question to a shuffled, scored quiz with a final message.
  • Make answer checking forgiving with strip, lower and a list of accepted answers.
  • Keep related data together in a list of lists, and test the game at its score boundaries.

Paris, with a space

A player types Paris, with a space after it, and the quiz says Wrong. Here is the first version of the quiz, and two real runs.

answer = input("What is the capital of France? ")
if answer == "Paris":
    print("Correct!")
else:
    print("Wrong! The answer was Paris.")
What is the capital of France? Paris
Wrong! The answer was Paris.
What is the capital of France? paris
Wrong! The answer was Paris.

The player knew the answer both times. The program was the one that got it wrong. You cannot see the space at the end of "Paris " on the screen, so how do you prove it is there? The built-in function repr shows a value the way it would be written in code, quote marks and all.

answer = input("What is the capital of France? ")
print(repr(answer))
What is the capital of France? Paris
'Paris '

The closing quote mark is one space away from the s. That space is part of the string, and "Paris " is not equal to "Paris". The fix is the tidy-up line from Lesson 10: strip the spaces, make it lower case, and compare with a lower-case answer.

answer = input("What is the capital of France? ")
if answer.strip().lower() == "paris":
    print("Correct!")
else:
    print("Wrong! The answer was Paris.")
What is the capital of France?  PARIS
Correct!

A quiz that marks right answers wrong is worse than no quiz. This project builds one that is fair to the player, in five versions, testing each before moving on.

The plan

VersionNew featureIdea it uses
1One question, forgiving of spaces and capitalsif, strip, lower
2Several questions and a scorelists, a for loop, a running total
3More than one right answerthe in operator on a list
4Each question kept with its answersa list of lists
5Shuffled order and a final verdictfunctions, return, elif, random.shuffle

The questions in this quiz are about Python itself, so you can check every answer by running a line of code.

Version 2: several questions and a score

Two lists, kept in the same order, hold the questions and their answers. One loop asks each question by index.

questions = ["What does len('robot') give?", "What is 17 % 5?", "Which keyword starts a function definition?"]
answers = ["5", "2", "def"]
score = 0
for i in range(len(questions)):
    reply = input(questions[i] + " ").strip().lower()
    if reply == answers[i]:
        print("Correct!")
        score = score + 1
    else:
        print("Not quite. The answer was", answers[i])
print(f"You scored {score} out of {len(questions)}.")
What does len('robot') give? 5
Correct!
What is 17 % 5? 3
Not quite. The answer was 2
Which keyword starts a function definition?  Def
Correct!
You scored 2 out of 3.

" Def" with a space and a capital was accepted, because strip and lower run before the comparison. The score is a running total, starting at 0 before the loop.

This version has a weakness. Someone who types five instead of 5 knows the answer, but is marked wrong.

Version 3: more than one right answer

Instead of one answer per question, store a list of accepted answers and ask whether the reply is in that list.

accepted = ["5", "five"]
print("five" in accepted)
print("Five".lower() in accepted)
print("5.0" in accepted)
True
True
False

5.0 is not accepted. Should it be? That is a decision for you, the quiz writer. A test that shows it is not accepted makes sure you decide on purpose rather than by accident.

Version 4: keep each question with its answers

Two separate lists only work while they stay in step. Delete a question from one and forget the other, and every later question is marked against the wrong answer. A safer shape is one list in which each item is itself a small list: the question first, then its accepted answers.

quiz = [
    ["What does len('robot') give?", ["5", "five"]],
    ["What is 17 % 5?", ["2", "two"]],
]
print(quiz[1])
print(quiz[1][0])
print(quiz[1][1])
print(quiz[1][1][0])
['What is 17 % 5?', ['2', 'two']]
What is 17 % 5?
['2', 'two']
2

Read the indexes from left to right. quiz[1] is the second item, a small list. quiz[1][0] is the first thing in that small list, the question. quiz[1][1] is the list of answers, and quiz[1][1][0] is the first accepted answer. A list inside a list is called a nested list.

Version 5: functions, shuffling and a verdict

The final version splits the work into two functions. ask asks one question and returns True or False. verdict turns the score into a message. random.shuffle mixes up the order of the quiz list, so the game feels different each time. Because each question travels with its own answers, shuffling can never separate them.

import random


def ask(question, accepted):
    reply = input(question + " ").strip().lower()
    if reply in accepted:
        print("Correct!")
        return True
    print("Not quite. The answer was", accepted[0])
    return False


def verdict(score, total):
    percent = round(100 * score / total)
    if percent == 100:
        message = "Perfect!"
    elif percent >= 60:
        message = "Well done."
    else:
        message = "Keep practising."
    return f"You scored {score} out of {total} ({percent}%). {message}"


quiz = [
    ["What does len('robot') give?", ["5", "five"]],
    ["What is 17 % 5?", ["2", "two"]],
    ["Which keyword starts a function definition?", ["def"]],
    ["What type does input() always give back?", ["str", "string"]],
    ["What is the index of the first item in a list?", ["0", "zero"]],
]
random.shuffle(quiz)
score = 0
for item in quiz:
    if ask(item[0], item[1]):
        score = score + 1
print(verdict(score, len(quiz)))

Here is one real game. Your questions will come in a different order.

What does len('robot') give? Five
Correct!
Which keyword starts a function definition? def
Correct!
What type does input() always give back? String
Correct!
What is the index of the first item in a list? 1
Not quite. The answer was 0
What is 17 % 5? 2
Correct!
You scored 4 out of 5 (80%). Well done.

Look at how short the main loop has become. if ask(item[0], item[1]): reads as "if the player gets this question right". The details of asking and checking live inside ask, where they were tested once. The player in this run made the classic Lesson 14 slip, answering 1 for the first index. The quiz caught it and showed the right answer, which is what a quiz is for.

Notice that ask has two returns. If the reply is accepted, return True ends the function at once, and the "Not quite" line never runs. That is the early return from Lesson 16.

Testing the quiz

A quiz is tested like any program, with expected results written first. The verdict has two boundaries, 100% and 60%, so they get tests of their own.

TestExpectedWhy this test
Answer " DEF" to the def questionCorrect!spaces and capitals
Answer five to the len questionCorrect!a second accepted answer
Press Enter without typing anythingNot quitean empty reply
All 5 right100%, Perfect!the top boundary
Exactly 3 right60%, Well done.the 60% boundary
Exactly 2 right40%, Keep practising.just below the boundary

There is one edge case the program does not handle: an empty quiz list would make total 0, and verdict would divide by zero. Since you write the quiz list yourself, you can decide that it will never be empty. That is a fair decision, as long as it is a decision and not an accident.

Fair and kind quizzes

Quizzes get shared, so two practical notes. Write questions about things, not people: a quiz about a classmate can turn into teasing very quickly, even if nobody meant it to. And show the right answer kindly when the player misses, as this one does. "Not quite. The answer was 0" teaches something. "WRONG!!" just stings.

Common misconceptions

  • "If two strings look the same on screen, they are equal." Spaces and capital letters are part of a string. repr shows what is really there.
  • "Two lists in the same order are as good as one list of pairs." They work until one list is changed or shuffled without the other. A list of lists keeps each question with its answers.
  • "random.shuffle gives you a new, shuffled list." It shuffles the list you give it, in place. The original order is gone.
  • "quiz[1][1][0] is too many brackets to be real." Each pair of brackets picks one item, reading from left to right. Three pairs means three steps inwards.

What to carry forward

Worth holding on to: A program that judges a person's input should be generous about form, strict about meaning, and tested at exactly the points where its judgement changes.

You found an invisible space with repr and made answer checking forgiving with strip, lower and a list of accepted answers. You moved from two parallel lists to a list of lists, so questions and answers can never drift apart, even when shuffled. You split the game into an ask function that returns True or False and a verdict function with an elif chain, and you tested the score boundaries at 100% and 60%. The next project keeps a player inside a loop for as long as they like: a text adventure.

Sources

  1. Python Software Foundation. (2026). Built-in types: str.strip and str.lower; membership tests with in. docs.python.org
  2. Python Software Foundation. (2026). Built-in functions: repr and len. (repr returns a string containing a printable representation of an object.) docs.python.org
  3. Python Software Foundation. (2026). random: Generate pseudo-random numbers. (shuffle shuffles a sequence in place.) docs.python.org
  4. Python Software Foundation. (2026). An informal introduction to Python: Lists. The Python Tutorial. (Lists can be nested, creating lists that contain other lists.) docs.python.org
Key terms
repr
A built-in function that shows a value as it would be written in code, revealing hidden spaces inside quote marks.
Accepted answers
A list of every reply that counts as correct, checked with the in operator.
Nested list
A list whose items are themselves lists, such as a question stored with its list of answers.
random.shuffle
A function that puts the items of a list into a random order, changing that list in place.
Parallel lists
Two lists kept in the same order so that matching items share an index. They break if one changes without the other.
Early return
Leaving a function with return as soon as the answer is known, so the lines after it do not run.

Project: A Text Adventure

  • Plan a text adventure as a map of rooms, commands and items before writing code.
  • Build a game loop that tracks the current room in a variable and an inventory in a list.
  • Write one function per room, and test every command in every room, including unexpected ones.

A cave in Kentucky, on a 1970s computer

In the 1975-76 school year, a programmer named Will Crowther wrote a game about exploring a cave. Crowther worked at the company Bolt, Beranek and Newman, where he had helped create the ARPANET, the network that came before the internet. He had also helped map a real cave system in Kentucky, and his game followed it. There were no pictures. The game described where you were in words, and you typed short commands back. Its instructions told the player to give commands of one or two words.

A year later another programmer, Don Woods, expanded the game. By May 1977 he had added many new places, and features such as scorekeeping and an inventory of the things a player carries. The game, known as Adventure, was the first of a whole family of text-based games, sometimes called interactive fiction.

In this project you build a small game in the same tradition: a cave with three places, a lamp, a key, a locked door and some treasure. It needs almost every idea from the course, which is why it is a good test of them.

What a text adventure needs

Part of the gamePython ideaFrom lesson
Where the player is nowA variable, room, holding a room name8
Reading commandsinput, tidied with strip and lower9 and 10
Each room's rulesif, elif and else10 and 11
Keep playing until the endA while loop, the game loop13
What the player carriesA list, inventory, with append and in14
One room's code in one placeA function per room that returns the next room15 and 16

The map, before any code

Plan on paper first. A table of rooms is the design. Writing code before you have one is like building a house without a drawing.

RoomWhat the player seesCommandsNeeds
entranceThe mouth of a cave; a brass lamp on a rocktake, north, quitnothing
tunnelA narrow tunnel; a small key in the dusttake, east, south, quitthe lamp, or it is too dark
doorA heavy wooden door to the northnorth, west, quitthe key, to open the door
treasureGold: the game is wonnonereached through the door

Stage 1: two rooms and a loop

Start with the smallest thing that is a game: two rooms, and a loop that keeps going until the player quits. The variable room remembers where the player is.

room = "entrance"
while room != "quit":
    if room == "entrance":
        print("You are at the mouth of a cave. A passage leads north.")
        command = input("> ").strip().lower()
        if command == "north":
            room = "tunnel"
        elif command == "quit":
            room = "quit"
    elif room == "tunnel":
        print("You are in a dark tunnel. The entrance is south.")
        command = input("> ").strip().lower()
        if command == "south":
            room = "entrance"
        elif command == "quit":
            room = "quit"
print("Goodbye.")
You are at the mouth of a cave. A passage leads north.
> north
You are in a dark tunnel. The entrance is south.
> jump
You are in a dark tunnel. The entrance is south.
> south
You are at the mouth of a cave. A passage leads north.
> quit
Goodbye.

Moving between rooms is just changing one variable. Each pass of the while loop looks at room and runs that room's part. The loop is the game loop: describe, ask, update, repeat.

Testing already found a weakness. When the player typed jump, nothing told them it did not work. The room description simply appeared again. That is the first thing the final version fixes, with a reply for anything it does not understand.

Stage 2: one function per room

With more rooms, one long loop gets hard to read. So each room becomes a function. It describes the room, reads one command, and returns the name of the room the player should be in next. The game loop then only has to call the right function and store what it returns.

Stage 3 is built into this version too: an inventory list. The tunnel is too dark without the lamp, and the door will not open without the key. Here is the whole game.

def entrance(inventory):
    print("You are at the mouth of a cave. A passage leads north.")
    if "lamp" not in inventory:
        print("A brass lamp lies on a rock.")
    command = input("> ").strip().lower()
    if command == "take" and "lamp" not in inventory:
        inventory.append("lamp")
        print("You pick up the lamp.")
    elif command == "north":
        return "tunnel"
    elif command == "quit":
        return "quit"
    else:
        print("You can't do that here.")
    return "entrance"


def tunnel(inventory):
    if "lamp" not in inventory:
        print("It is pitch dark. You stumble back to the entrance.")
        return "entrance"
    print("Your lamp lights a narrow tunnel. Paths lead east and south.")
    if "key" not in inventory:
        print("A small key glints in the dust.")
    command = input("> ").strip().lower()
    if command == "take" and "key" not in inventory:
        inventory.append("key")
        print("You pick up the key.")
    elif command == "east":
        return "door"
    elif command == "south":
        return "entrance"
    elif command == "quit":
        return "quit"
    else:
        print("You can't do that here.")
    return "tunnel"


def door(inventory):
    print("A heavy wooden door blocks the way north. The tunnel lies west.")
    command = input("> ").strip().lower()
    if command == "north":
        if "key" in inventory:
            print("The key turns. Beyond the door, gold glitters in the lamplight!")
            return "treasure"
        print("The door is locked.")
    elif command == "west":
        return "tunnel"
    elif command == "quit":
        return "quit"
    else:
        print("You can't do that here.")
    return "door"


inventory = []
room = "entrance"
while room != "treasure" and room != "quit":
    if room == "entrance":
        room = entrance(inventory)
    elif room == "tunnel":
        room = tunnel(inventory)
    elif room == "door":
        room = door(inventory)

if room == "treasure":
    print("You found the treasure. You win!")
else:
    print("You leave the cave. Come back soon.")
print("You were carrying:", inventory)

Each room function ends with return and its own name, such as return "entrance". That line is reached when the player did something that keeps them in the room, such as taking the lamp or typing nonsense. Commands that move the player return early with a different room.

Stage 3: the inventory, shared

Lesson 16 said that variables made inside a function vanish when it ends. So how does inventory.append("lamp") inside entrance still count after entrance returns? The list was not made inside the function. It was made once, before the loop, and passed in. The parameter is a new name, but it refers to the same list. The Python tutorial notes that if a list is passed to a function, the caller sees any changes the function makes to it, such as items inserted into the list. A tiny test shows it.

def pick_up(items, thing):
    items.append(thing)


bag = []
pick_up(bag, "lamp")
pick_up(bag, "key")
print(bag)
['lamp', 'key']

Playing it

Here is a real game that wins. The player tries the tunnel too early, then does things in the right order.

You are at the mouth of a cave. A passage leads north.
A brass lamp lies on a rock.
> north
It is pitch dark. You stumble back to the entrance.
You are at the mouth of a cave. A passage leads north.
A brass lamp lies on a rock.
> take
You pick up the lamp.
You are at the mouth of a cave. A passage leads north.
> north
Your lamp lights a narrow tunnel. Paths lead east and south.
A small key glints in the dust.
> take
You pick up the key.
Your lamp lights a narrow tunnel. Paths lead east and south.
> east
A heavy wooden door blocks the way north. The tunnel lies west.
> north
The key turns. Beyond the door, gold glitters in the lamplight!
You found the treasure. You win!
You were carrying: ['lamp', 'key']

And here is the start of a second real game, with a player who types oddly: dance at the entrance gets "You can't do that here.", while " Take" with a space and a capital, and then " NORTH", work fine. At the door without the key, north gives "The door is locked." Typing quit back at the entrance ends with "You leave the cave. Come back soon." and "You were carrying: ['lamp']".

Testing every path

Players will try things you never thought of, so test every command in every room, including the silly ones. A table keeps you honest.

RoomSituationCommandExpected
entranceno lampnorthtunnel says pitch dark, back to entrance
entranceno lamptakelamp added; it is no longer described
entrancehas lamptakeYou can't do that here.
tunnelhas lampeastdoor
doorno keynorthThe door is locked.
doorhas keynorthtreasure: You win!
any roomanydanceYou can't do that here.
any roomanyquitYou leave the cave.

Look at the third row. Typing take when you already hold the lamp gives "You can't do that here." because the condition 'command == "take" and "lamp" not in inventory' is False, so the else runs. Is that the reply you want? Perhaps "You already have it." would be kinder. Testing found a decision, not a crash, and you get to make it.

Common misconceptions

  • "Each room needs its own while loop." One game loop is enough. The room variable decides which room's code runs on each pass.
  • "A function cannot change a list that is passed to it." It can. The parameter refers to the same list, so append inside the function changes the list outside it.
  • "Unknown commands can just be ignored." Silence leaves the player stuck and confused. Answer every command, even if only with "You can't do that here."
  • "A text adventure needs a complicated program." It needs a clear map. The code is a loop, some functions and a list, all of which you already know.

What to remember

So what? A game is a loop around a state: a variable for where the player is, a list for what they carry, and rules that change both in response to commands.

You met Crowther's Adventure, built from a real cave map in the 1970s, and planned your own game as a table of rooms, commands and items. You built a game loop that tracks the room in a variable, then gave each room its own function returning the next room. You added an inventory list and saw why a function can change a list passed to it. And you tested every command in every room, which turned up a reply worth improving. Next comes a different kind of program: one that does not wait for a player at all, but runs an experiment.

Sources

  1. Jerz, D. G. (2007). Somewhere nearby is Colossal Cave: Examining Will Crowther's original "Adventure" in code and in Kentucky. Digital Humanities Quarterly, 1(2). (Written in the 1975-76 academic year; Crowther at BBN and the ARPANET; based on a real Kentucky cave he had helped map; commands of 1 or 2 words; Woods's expansion by May 1977 with scorekeeping and an inventory.) Digital Humanities Quarterly
  2. Python Software Foundation. (2026). More control flow tools: Defining functions. The Python Tutorial. (When a list is passed to a function, the caller sees changes the function makes to it.) docs.python.org
  3. Python Software Foundation. (2026). Data structures: More on lists. The Python Tutorial. (append, and testing membership with in.) docs.python.org
Key terms
Text adventure
A game that describes places in words and responds to commands the player types.
Game loop
The loop at the heart of a game that describes, reads a command, updates the state and repeats.
State
Everything a program remembers about the situation, such as the current room and the inventory.
Inventory
A list of the items the player is carrying, added to with append and checked with in.
Room function
A function that handles one room and returns the name of the room the player should be in next.
Interactive fiction
Text-based games that emphasise exploring, puzzles and story, starting with Adventure in the 1970s.

Module 9: Projects: Simulations, and One of Your Own

Programs that run experiments instead of waiting for a player: coin and dice simulations and what they can and cannot tell you about real coins, a small ecosystem of grass, rabbits and foxes, and finally a program of your own, planned, tested and shared safely.

Project: Coin Flips and Dice, Simulated

  • Simulate coin flips and dice rolls with the random module, and use random.seed to repeat a run exactly.
  • Explain why percentages settle down as the number of trials grows, and how far a fair coin can wander by chance.
  • Say what a simulation can show about chance, and why it cannot tell you what real coins do.

350,757 flips

In 2025, a team of 50 researchers led by František Bartoš published the results of 350,757 coin flips in the Journal of the American Statistical Association. They recorded which side was up before each flip and which side landed up. Two numbers came out of it, and they seem to disagree.

Heads came up 50.0% of the time. As a contest between heads and tails, the coins were fair. But coins landed on the same side they started on 50.8% of the time, a little more often than the other side. The researchers also found that this bias was stronger for some people than for others. In 2007, the mathematicians Persi Diaconis, Susan Holmes and Richard Montgomery had predicted something like it. Using the physics of a spinning coin and high-speed photography, they estimated that a naturally flipped coin comes up the way it started about 51% of the time.

So is a coin flip 50-50? One side says yes: the heads count was as even as anyone could ask. The other says no: if you know how the coin started, you can beat 50% by a little. Both rest on the same 350,757 flips, because they answer different questions. One asks whether heads beats tails. The other asks whether the starting side wins. In this project you build simulations of coins and dice, and use them to see what a simulation can add to an argument like this, and what it never can.

A coin in one line

random.random() returns a random decimal from 0.0 up to, but not including, 1.0. Half of that range lies below 0.5, so random.random() < 0.5 is True about half the time. Call True heads.

import random

for flip in range(10):
    if random.random() < 0.5:
        print("heads", end=" ")
    else:
        print("tails", end=" ")
print()

Two real runs of the same program:

tails tails heads tails heads tails tails tails heads tails
heads tails heads heads tails tails heads tails tails tails 

Computers cannot really flip coins. The random module makes pseudo-random numbers: a long sequence worked out by a formula, which looks random but is completely fixed once you know where it started. That starting point is called the seed. Normally Python picks a different seed every run, which is why the two runs differ.

More flips, steadier percentages

This program counts heads for bigger and bigger experiments. random.seed(1) fixes the starting point, so the program gives exactly the same results every time it runs. The Python documentation guarantees that random() repeats its sequence when given the same seed, which is what makes an experiment like this repeatable.

import random

random.seed(1)


def count_heads(flips):
    heads = 0
    for flip in range(flips):
        if random.random() < 0.5:
            heads = heads + 1
    return heads


for flips in [10, 100, 1000, 10000, 100000]:
    heads = count_heads(flips)
    percent = 100 * heads / flips
    print(f"{flips} flips: {heads} heads, {percent:.1f}%")
10 flips: 6 heads, 60.0%
100 flips: 46 heads, 46.0%
1000 flips: 480 heads, 48.0%
10000 flips: 5037 heads, 50.4%
100000 flips: 50031 heads, 50.0%

The :.1f after percent in the f-string means "show one digit after the decimal point". Read the percentages down the column. Ten flips gave 60%, a long way from 50. As the experiments grow, the percentage settles closer and closer to 50%. This is the pattern statisticians call the law of large numbers: the more trials you run, the closer the share of an outcome tends to get to its true chance.

Look at the counts as well, though. At 100,000 flips, heads beat tails by 62, not by 0. The percentage gets close to 50, but the gap in the count does not have to shrink.

How far can a fair coin wander?

If you flipped a real coin 100 times and got 46 heads, would you suspect it? Run 20 experiments of 100 flips with a perfectly fair simulated coin and see what normal looks like.

import random

random.seed(2)

results = []
for experiment in range(20):
    heads = 0
    for flip in range(100):
        if random.random() < 0.5:
            heads = heads + 1
    results.append(heads)

print(results)
print("lowest:", min(results), "highest:", max(results))
[41, 57, 53, 43, 57, 49, 53, 49, 43, 51, 50, 50, 50, 40, 51, 50, 49, 52, 57, 42]
lowest: 40 highest: 57

A coin that is fair by construction gave anything from 40 to 57 heads in 100 flips. So 46 heads is nothing unusual. This is something a simulation is genuinely good at: showing how much chance alone can move a result.

What a simulation can and cannot tell you

Now model the same-side question. Here are two simulated coins: one where landing on the starting side has a chance of exactly 0.5, and one where it is 0.508, the study's figure. Each is flipped 350,757 times, the size of the real study.

import random

random.seed(3)


def same_side_percent(chance, flips):
    same = 0
    for flip in range(flips):
        if random.random() < chance:
            same = same + 1
    return 100 * same / flips


print(f"Model with 0.5:   {same_side_percent(0.5, 350757):.2f}% same side")
print(f"Model with 0.508: {same_side_percent(0.508, 350757):.2f}% same side")
Model with 0.5:   49.97% same side
Model with 0.508: 50.74% same side

Each model gave back almost exactly the chance it was built with. That is the limit of every simulation: it can only show the consequences of the rules you put in. Typing 0.508 does not prove that real coins are biased, any more than typing 0.5 proves they are fair. Only real flips can say what real coins do. That is why the study needed people flipping actual coins.

But a simulation can answer a sharper question: if coins were perfectly fair, how far from 50% could 350,757 flips land just by chance? Run the fair model ten times.

import random

random.seed(5)

for run in range(10):
    same = 0
    for flip in range(350757):
        if random.random() < 0.5:
            same = same + 1
    print(f"Run {run + 1}: {100 * same / 350757:.2f}%")
Run 1: 50.13%
Run 2: 49.97%
Run 3: 50.04%
Run 4: 49.94%
Run 5: 50.12%
Run 6: 50.06%
Run 7: 50.14%
Run 8: 49.92%
Run 9: 50.05%
Run 10: 49.93%

With this many flips, a fair coin stays within about 0.15 of 50%. The study's 50.8% is far outside that range. So "it was just luck" is a poor explanation for the real result. That is the core idea behind the statistics in the study, done roughly here by simulation. The simulation did not tell you what real coins do. It told you what chance alone would do, and the real data did not look like that.

Two dice, and why 7 wins

Roll two dice and add them. Totals run from 2 to 12, but they are not equally likely. Count the ways: 36 pairs of faces are possible, and more of them add up to 7 than to anything else.

Total23456789101112
Ways out of 3612345654321
Expected in 3600 rolls100200300400500600500400300200100

A 7 can be 1 and 6, 2 and 5, 3 and 4, 4 and 3, 5 and 2, or 6 and 1. A 2 can only be 1 and 1. Here a simulation checks the counting, and draws a bar chart in text, one # for every 10 rolls.

import random

random.seed(4)

counts = [0] * 13
for roll in range(3600):
    total = random.randint(1, 6) + random.randint(1, 6)
    counts[total] = counts[total] + 1

for total in range(2, 13):
    bar = "#" * (counts[total] // 10)
    print(f"{total:2} {counts[total]:4} {bar}")
 2  108 ##########
 3  176 #################
 4  285 ############################
 5  410 #########################################
 6  500 ##################################################
 7  603 ############################################################
 8  524 ####################################################
 9  400 ########################################
10  307 ##############################
11  172 #################
12  115 ###########

Two tricks make this short. [0] * 13 makes a list of 13 zeros, one tally for each index from 0 to 12, so counts[7] can hold the number of 7s directly; the first two tallies are simply never used. And {total:2} and {counts[total]:4} in the f-string pad each number to a fixed width, so the bars line up. The chart has the shape the counting predicted, a peak at 7, and each count lands near its expected value without matching it exactly. That is chance again, wandering a little around the truth.

One honest note about seeds. The documentation guarantees that random() repeats with the same seed across Python versions, but makes no such promise for every other function. If your dice program uses a different version of Python, its counts may differ from these even with seed 4. The pattern will not.

Common misconceptions

  • "After a run of heads, tails is due." Each flip ignores the ones before it. A simulation of a million flips found 124,792 moments where three heads had just come in a row, and the next flip was heads 50.0% of the time.
  • "With enough flips, heads and tails come out exactly equal." The percentage gets close to 50, but the counts usually differ: 50,031 heads in 100,000 flips.
  • "A simulation proves what real coins do." It shows what your model does. Real coins need real flips. A simulation can show how far chance alone could move a result.
  • "A seed makes the numbers less random." The numbers are pseudo-random either way. A seed only fixes the starting point, so the experiment can be repeated and checked.

Putting it together

The core of it: A simulation shows the consequences of the rules you give it. That makes it excellent for asking what chance alone would do, and useless for discovering what the real world does.

You simulated coins with random.random() and saw percentages settle towards 50% as the flips grew, while the raw counts still differed. You used random.seed to make experiments repeatable. You found that a fair coin can give anywhere from 40 to 57 heads in 100 flips. You saw that a model returns the chance you put into it, and that running a fair model at the study's size shows 50.8% is far beyond what chance alone produces. And you checked the 36 ways two dice can fall with a tally list and a text bar chart. The next project simulates something that changes over time: a field of rabbits.

Sources

  1. Bartoš, F., Sarafoglou, A., Godmann, H. R., Sahrani, A., and 46 others. (2025). Fair coins tend to land on the same side they started: Evidence from 350,757 flips. Journal of the American Statistical Association, 120(552), 2118-2127. https://doi.org/10.1080/01621459.2025.2516210 (Same-side probability 0.508; heads probability 0.500; variation between people.) arXiv preprint and abstract
  2. Diaconis, P., Holmes, S., and Montgomery, R. (2007). Dynamical bias in the coin toss. SIAM Review, 49(2), 211-235. https://doi.org/10.1137/S0036144504446436 (Abstract read: vigorously flipped coins tend to come up the way they started; measurements from high-speed photography; about .51 for natural flips.)
  3. Python Software Foundation. (2026). random: Generate pseudo-random numbers. (random() returns a float from 0.0 up to but not including 1.0; reusing a seed reproduces the sequence; random() is guaranteed to repeat its sequence for the same seed across versions.) docs.python.org
  4. Python Software Foundation. (2026). Input and output: Formatted string literals and the format specification. The Python Tutorial. (Format specifiers after a colon inside f-string braces, such as :.1f and widths.) docs.python.org
Key terms
Simulation
A program that imitates a real process, such as flipping a coin, by following rules you choose.
Pseudo-random
Numbers made by a formula that look random but repeat exactly if the formula starts from the same point.
Seed
The starting point of a pseudo-random sequence. random.seed(1) makes a program give the same numbers every run.
random.random()
Returns a random decimal from 0.0 up to, but not including, 1.0; random.random() < 0.5 is True about half the time.
Law of large numbers
The pattern that, over more and more trials, the share of an outcome tends to get closer to its true chance.
Tally list
A list of counters, such as [0] * 13, where each index counts one possible result.
Model
The set of rules a simulation follows. Its results show the consequences of those rules, not facts about the world.

Project: A Small Ecosystem

  • Turn a set of rules in words into a year-by-year simulation, and spot when a model is missing a rule.
  • Model a population limit and a predator, store each year in lists, and read the cycle in a text chart.
  • Change one number at a time to experiment, and explain what a model's results can and cannot claim.

132,387 rabbits

Start with 40 rabbits, let every pair have one baby a year, and after twenty years a simulation says there are 132,387 of them. Here is the whole simulation. Each year, the number of rabbits grows by half, because rabbits // 2 is the number of pairs and each pair adds one baby.

rabbits = 40
for year in range(1, 21):
    rabbits = rabbits + rabbits // 2
    if year % 5 == 0:
        print(f"Year {year}: {rabbits} rabbits")
Year 5: 303 rabbits
Year 10: 2296 rabbits
Year 15: 17434 rabbits
Year 20: 132387 rabbits

The line if year % 5 == 0 prints only every fifth year, using the remainder trick from Lesson 10. The arithmetic is right. The answer is absurd. No field holds 132,387 rabbits, and given another twenty years this model would need millions. Before reading on, ask yourself what is wrong. It is not a bug in the code.

A model with a missing rule

A simulation like this is a model: a set of rules that stands in for something real. This model has one rule, "rabbits breed", and nothing that ever pushes the other way. Growth like this, where the population is multiplied by the same amount every year, is called exponential growth. The OpenStax biology textbook puts it bluntly: exponential growth is possible only when infinite natural resources are available, which is not the case in the real world.

So the fix is not in the Python. It is in the rules. Real rabbits run out of food, get eaten, and get sick. This project adds two of those, one rule at a time, running the model after each.

Rule 2: a field that can feed 1000

Suppose the field has grass for at most 1000 rabbits, and any extra starve. One new line says so: min picks the smaller of two numbers, so rabbits can never go above 1000.

rabbits = 40
for year in range(1, 13):
    rabbits = rabbits + rabbits // 2
    rabbits = min(rabbits, 1000)
    print(f"Year {year}: {rabbits} rabbits")
Year 1: 60 rabbits
Year 2: 90 rabbits
Year 3: 135 rabbits
Year 4: 202 rabbits
Year 5: 303 rabbits
Year 6: 454 rabbits
Year 7: 681 rabbits
Year 8: 1000 rabbits
Year 9: 1000 rabbits
Year 10: 1000 rabbits
Year 11: 1000 rabbits
Year 12: 1000 rabbits

Now the population grows fast, then hits the ceiling in year 8 and stays there. Biologists call the largest population an environment can keep supporting its carrying capacity. In this model, it is the 1000 in the min line.

The full set of rules

Foxes come next. Here is the whole model in words and in Python, one year at a time. Writing the rules in a table first, as you did with the cave map, makes it far easier to check the code against what you meant.

Step each yearIn wordsIn Python
1Every pair of rabbits has one babyrabbits = rabbits + rabbits // 2
2The field feeds at most 1000 rabbitsrabbits = min(rabbits, 1000)
3Foxes catch rabbits; more of either means more meetingseaten = min(rabbits, foxes * rabbits // 200)
4The caught rabbits are gonerabbits = rabbits - eaten
5One fox cub for every 10 rabbits eaten, and one fox in five diesfoxes = foxes + eaten // 10 - foxes // 5

Step 3 is the clever one. Foxes catch more when there are more rabbits to find and more foxes looking, so the catch depends on both numbers multiplied together. The min makes sure foxes can never eat more rabbits than exist.

Forty years with foxes

The model goes in a function that returns two lists, the rabbit and fox numbers for each year, so the whole history can be printed and studied afterwards. Each line of the output shows the year, the rabbits, the foxes, and a bar with one # for every 50 rabbits.

def simulate(years, rabbits, foxes):
    rabbit_history = []
    fox_history = []
    for year in range(years):
        rabbits = rabbits + rabbits // 2
        rabbits = min(rabbits, 1000)
        eaten = min(rabbits, foxes * rabbits // 200)
        rabbits = rabbits - eaten
        foxes = foxes + eaten // 10 - foxes // 5
        rabbit_history.append(rabbits)
        fox_history.append(foxes)
    return rabbit_history, fox_history


rabbit_history, fox_history = simulate(40, 40, 5)
for year in range(40):
    bar = "#" * (rabbit_history[year] // 50)
    print(f"{year + 1:2} {rabbit_history[year]:4} {fox_history[year]:4}  {bar}")
 1   59    4  #
 2   87    4  #
 3  128    4  ##
 4  189    4  ###
 5  278    4  #####
 6  409    4  ########
 7  601    5  ############
 8  879    6  #################
 9  970    8  ###################
10  960   11  ###################
11  945   14  ##################
12  930   19  ##################
13  905   25  ##################
14  875   32  #################
15  840   42  ################
16  790   55  ###############
17  725   71  ##############
18  645   92  ############
19  523  118  ##########
20  322  141  ######
21  143  147  ##
22   57  133  #
23   29  112
24   19   92
25   16   75
26   15   60
27   16   48
28   19   39
29   23   32
30   29   26
31   38   21
32   52   17  #
33   72   14  #
34  101   12  ##
35  142   10  ##
36  203    9  ####
37  291    9  #####
38  417    9  ########
39  597   10  ###########
40  851   12  #################

A function can return two values at once, separated by a comma, and the line that calls it can catch both in two names. That is what rabbit_history, fox_history = simulate(40, 40, 5) does.

Reading the forty years

Read the chart from the top. For nine years the rabbits boom while the few foxes barely matter, and the field fills to 970. Plenty of rabbits means plenty of meetings, so the foxes start to grow: 11, 19, 32, 55. As they grow, they eat more, and from year 10 the rabbits slowly decline. The foxes peak at 147 in year 21. By then the rabbits are collapsing, and they hit bottom at 15 in year 26. Now the foxes are starving: with so few rabbits, one in five dies each year and hardly any cubs are born. They fall to 9. With the foxes gone, the rabbits recover, reaching 851 by year 40, and in the last few years the foxes begin to rise again. The cycle is about to repeat.

Notice the order. The fox peak came after the rabbits had already started falling. Predators follow their prey. Real animals do something similar. The OpenStax biology textbook describes nearly 200 years of trapping records from North American forests in which snowshoe hares and the lynx that hunt them rise and fall in a cycle of about 10 years, with the lynx numbers lagging one to two years behind the hares. A 1942 study by Charles Elton and Mary Nicholson was titled simply "The ten-year cycle in numbers of the lynx in Canada". In the 1920s, Alfred Lotka and Vito Volterra each published equations for predators and prey that produce cycles like this. Your five rules are a small cousin of theirs.

Experiments: change one number

A model earns its keep when you experiment with it. Change one thing, keep everything else the same, and compare. Here the field size becomes a parameter, and a report function sums up each run in one line.

def simulate(years, rabbits, foxes, field):
    rabbit_history = []
    fox_history = []
    for year in range(years):
        rabbits = rabbits + rabbits // 2
        rabbits = min(rabbits, field)
        eaten = min(rabbits, foxes * rabbits // 200)
        rabbits = rabbits - eaten
        foxes = foxes + eaten // 10 - foxes // 5
        rabbit_history.append(rabbits)
        fox_history.append(foxes)
    return rabbit_history, fox_history


def report(foxes, field):
    rabbit_history, fox_history = simulate(40, 40, foxes, field)
    print(f"foxes={foxes:2} field={field:4}: most foxes {max(fox_history):3}, "
          f"fewest rabbits after year 10: {min(rabbit_history[10:]):3}, "
          f"rabbits in year 40: {rabbit_history[-1]}")


report(0, 1000)
report(5, 1000)
report(5, 500)
report(5, 2000)
foxes= 0 field=1000: most foxes   0, fewest rabbits after year 10: 1000, rabbits in year 40: 1000
foxes= 5 field=1000: most foxes 147, fewest rabbits after year 10:  15, rabbits in year 40: 851
foxes= 5 field= 500: most foxes   5, fewest rabbits after year 10: 488, rabbits in year 40: 488
foxes= 5 field=2000: most foxes 264, fewest rabbits after year 10:   0, rabbits in year 40: 0

rabbit_history[10:] means "the list from index 10 to the end", so the fewest-rabbits figure ignores the first ten years. Two of these results are worth stopping on.

With a field of 500, nothing cycles at all. The rabbits settle at 488 and the foxes at exactly 5. Each year the foxes eat 12 rabbits, so one cub is born, and one fox in five, which is one fox, dies. Everything balances.

With a field of 2000, twice the food, the result is worse, not better. The rabbits boom higher, the foxes boom to 264, and in year 16 the foxes eat every last rabbit. Giving the prey more food destroyed the ecosystem. That sounds wrong, but ecologists know the pattern. In 1971 Michael Rosenzweig studied six models of predators and prey and found that in each one, increasing the food supply tended to destroy the steady state. He called it the paradox of enrichment. Your 40-line program found the same surprise.

What this model leaves out

Every model simplifies. This one has no seasons, no young or old animals, no illness, no luck, no hiding places, and no other food for the foxes. It counts in whole animals, so rounding with // matters when numbers are small: a year with 1 rabbit gives 1 // 2, which is 0 babies. The results are what these five rules imply, not a forecast for any real field. The lesson is the same as with the coins. A simulation shows the consequences of its assumptions, and it is most useful for asking "what if?" and getting surprised.

Common misconceptions

  • "More food for the rabbits always means more rabbits." In this model, a field of 2000 fed a fox boom that wiped the rabbits out. Rosenzweig found the same pattern in six models.
  • "A model with correct code gives correct answers." The first model's code was perfect. Its rules were missing something, so its answer was nonsense.
  • "Predators and prey peak at the same time." The foxes peaked after the rabbits had started to fall, as real lynx lag behind hares.
  • "A simulation tells you what will happen." It tells you what its rules imply. Change a rule and the future changes.

The short version

Key idea: A simulation is only as sensible as its rules. Write the rules out first, add them one at a time, and experiment by changing one number at a time.

You saw a one-rule model grow to 132,387 rabbits with flawless code, and fixed it with a carrying capacity, one min line. You added foxes with a rule that depends on both populations, stored forty years in two lists, and read a boom, a crash and a recovery in a text chart, with the foxes lagging behind. You compared field sizes and found a steady balance at 500 and a collapse at 2000, a real ecological surprise. And you can say what the model leaves out. One project is left: a program of your own, from idea to sharing.

Sources

  1. Clark, M. A., Douglas, M., and Choi, J. (2018). Biology 2e, section 45.3: Environmental limits to population growth. OpenStax. (Exponential growth is possible only with infinite resources; carrying capacity as the maximum population an environment can support.) OpenStax
  2. Clark, M. A., Douglas, M., and Choi, J. (2018). Biology 2e, section 45.6: Community ecology. OpenStax. (Snowshoe hare and lynx trapping data spanning nearly 200 years; a cycle of about 10 years with the predator lagging 1 to 2 years behind.) OpenStax
  3. Elton, C., and Nicholson, M. (1942). The ten-year cycle in numbers of the lynx in Canada. Journal of Animal Ecology, 11(2), 215. https://doi.org/10.2307/1358
  4. Rosenzweig, M. L. (1971). Paradox of enrichment: Destabilization of exploitation ecosystems in ecological time. Science, 171(3969), 385-387. https://doi.org/10.1126/science.171.3969.385 (Abstract read: in six models, increasing the supply of nutrients or energy tends to destroy the steady state.)
  5. Wikipedia. (2026). Lotka-Volterra equations. (Background: the predator and prey equations of Lotka, 1925, and Volterra, 1926.) Wikipedia
Key terms
Model
A set of rules that stands in for something real, such as rabbits breeding and foxes hunting.
Exponential growth
Growth by the same multiple each step, such as half as many again every year, which soon becomes enormous.
Carrying capacity
The largest population an environment can keep supporting; in this model, the 1000 in min(rabbits, 1000).
Predator and prey
An animal that hunts, such as a fox, and the animal it hunts, such as a rabbit.
History list
A list that records a value for every step of a simulation, so the whole run can be studied afterwards.
Paradox of enrichment
The finding, in predator and prey models, that giving the prey more food can destroy the balance instead of helping.

Project: Plan, Build, Test and Share Your Own

  • Turn an idea into a short plan: what the program must do, what it can leave out, and how it splits into functions.
  • Build the smallest working version first, then add one tested feature at a time, keeping a working copy.
  • Test with a written table and a real user, then share safely, with credit for anything you reused.

An idea on one index card

The plan for this project fit on one index card: ask 5 times-table questions from the 2 to 12 tables, check each answer, say the score, and list the facts to practise. This last lesson follows that one small program from the card to a version worth sharing. The point is not the times tables. It is the route, which works for any program you will ever write, including your own project at the end of the lesson.

From idea to plan

The K-12 Computer Science Framework describes program development at this level as designing solutions for other people by defining a problem's criteria and constraints, and testing whether they were met. In plainer words: decide what the program must do before writing it, then check that it does. A plan table is enough.

Must doNice to haveNot now
Ask a question from the 2 to 12 tablesList the facts the player missedTiming each answer
Check the answer and say whether it is rightChoose how many questionsSaving scores between runs
Ask 5 questions and give a scorePick which tables to practisePictures or sound

The "Not now" column matters as much as the others. Every feature you add is more to build and more to test. A small program that works beats a big one that does not.

Next, split the work into pieces, the decomposition from Lesson 16. Three functions cover the must-do column: one that asks a single question and checks it, one that runs the whole session, and, as you are about to discover, one that makes sure the answer is a number.

The smallest version that works

Start with one question, no loop, no score. If this does not work, nothing built on it will.

import random

a = random.randint(2, 12)
b = random.randint(2, 12)
answer = int(input(f"What is {a} x {b}? "))
if answer == a * b:
    print("Correct!")
else:
    print(f"Not quite: {a} x {b} = {a * b}")
What is 9 x 10? 90
Correct!

random.randint(2, 12) includes both 2 and 12, as the documentation says, so every table from 2 to 12 can come up. Now the first test that is not a polite one: answer with a word.

What is 9 x 10? seven
Traceback (most recent call last):
  File "main.py", line 5, in <module>
    answer = int(input(f"What is {a} x {b}? "))
ValueError: invalid literal for int() with base 10: 'seven'

The ValueError from Lesson 9. Real users type words, so the plan gains a new must-have: handle answers that are not numbers. Finding that on the first day, with nine lines of code, is cheap. Finding it after building everything else would mean untangling it from all the rest.

Before changing anything, click Download in the Code Editor to keep a copy of this working version. If the next change goes badly, you can always get back to something that worked.

Growing it, one tested step at a time

Each step below was added and tested on its own: the number check, then the loop and score, then the list of missed facts. Here is the finished program.

# Times-table trainer
# Asks multiplication questions from the 2 to 12 times tables,
# keeps score, and lists the facts to practise.
import random


def ask_number(prompt):
    reply = input(prompt).strip()
    while not reply.isdigit():
        print("Please type a whole number, like 42.")
        reply = input(prompt).strip()
    return int(reply)


def one_question(missed):
    a = random.randint(2, 12)
    b = random.randint(2, 12)
    answer = ask_number(f"What is {a} x {b}? ")
    if answer == a * b:
        print("Correct!")
        return True
    print(f"Not quite: {a} x {b} = {a * b}")
    missed.append(f"{a} x {b} = {a * b}")
    return False


def main(rounds):
    missed = []
    score = 0
    for question in range(rounds):
        if one_question(missed):
            score = score + 1
    print(f"You got {score} out of {rounds}.")
    if len(missed) > 0:
        print("Practise these:")
        for fact in missed:
            print("  " + fact)


main(5)

Every piece of it is something you have done before. ask_number is the input-checking while loop from Lesson 13. one_question returns True or False like the quiz game's ask, and appends to a list it was given, like the adventure's inventory. main keeps a running score and prints the missed list with a for loop. The comment at the top says what the program is for, which is the first thing anyone opening the file will want to know. To change the number of questions, change one number: main(10).

Here is one real session. Your questions will differ.

What is 5 x 7? 35
Correct!
What is 8 x 4? thirty-two
Please type a whole number, like 42.
What is 8 x 4? 32
Correct!
What is 5 x 2? 10
Correct!
What is 3 x 4? 16
Not quite: 3 x 4 = 12
What is 5 x 10?  50
Correct!
You got 4 out of 5.
Practise these:
  3 x 4 = 12

A test table, written before testing

Write the tests and their expected results before you run them, so you are checking against a decision and not against whatever the program happens to do.

TestExpectedResult
The right answerCorrect!passed: 35 for 5 x 7
A wrong answerNot quite, with the right fact, added to the practise listpassed: 16 for 3 x 4
A wordAsk again, without counting a questionpassed: thirty-two
A space before the numberAcceptedpassed: " 50"
Pressing Enter with nothing typedAsk againpassed: "".isdigit() is False
A negative number such as -12Ask againpassed: the minus sign is not a digit, and no product here is negative
All 5 right5 out of 5, and no practise listpassed: "You got 5 out of 5." and nothing after it

The last row depends on the condition len(missed) > 0. Without it, a perfect game would still print "Practise these:" followed by nothing, which looks broken even though nothing crashed.

Feedback from a real person

Your test table only contains the mistakes you thought of. Other people make different ones. Ask someone to use the program while you watch without helping, and write down every moment they hesitate, frown or ask a question. Each one is a test you had not written. Then decide what to change, one change at a time, retesting after each, exactly as in Module 7.

The PRIMM approach from Lesson 2 ends with "Make": after predicting, running, investigating and modifying other people's programs, you make your own. This lesson is that last step.

Sharing safely, with credit

  • Keep it to people you know. The Code Editor's Download button saves your program as a file you can send to a friend or a teacher, with a grown-up's OK. You do not need an account for that.
  • Accounts are a grown-up decision. A Raspberry Pi Foundation account for anyone under 13 needs a parent or guardian to set it up, and Scratch's terms ask a grown-up to agree for anyone under 18.
  • Nothing personal in the code. Read your program and its comments as a stranger would. Your full name, school, address or photos do not belong in it.
  • Credit what you reused. If your program borrows a structure, a function or an idea, say so in a comment at the top, such as "# Quiz structure based on the quiz game in CS 060". The Scratch Community Guidelines ask for credit for everyone whose work you used, and the same courtesy belongs in Python.

Your own project, and what comes next

Now plan one of your own. Pick something small enough to finish in a few sessions. Some starting points: a dice game for two players, a story generator that asks for words, a unit converter, a pet that gets hungry and needs feeding, a turtle drawing of a place you like, or a quiz on a subject you know well. Make the plan table first, then the smallest version, then one tested feature at a time.

After this course, the next step on this site is High School Programming: Python from the First Line, which picks up where these projects stop, with longer programs, files and bigger projects. The Raspberry Pi Foundation's free Python path on its projects site offers guided projects that run in the same Code Editor you have been using. Most of all, keep building. Every program you finish teaches you something the next one needs.

Common misconceptions

  • "Real programmers start by writing all the code." A plan and a small working version come first. Big programs grow from small tested ones.
  • "Testing is something you do once, at the end." Testing happens after every step, so a new problem is always near the last change.
  • "If a tester gets confused, they are using it wrong." Their confusion is information about your program. Fix the program, or the instructions, not the person.
  • "Code from a course or a website is free to reuse without saying so." Say where it came from in a comment. Credit costs one line.

What to carry forward

The point: Plan small, build the smallest working version, add one tested feature at a time, and share with care and with credit.

You followed one program from an index card to a shareable trainer. You wrote a plan table with a "Not now" column, built a nine-line first version, and found a real problem with one unfriendly test. You grew the program in tested steps, keeping a working copy, and wrote the tests before running them. You know why watching a real user matters, how to share safely, and how to credit what you reuse. And you know what to build next. That is the whole course in one lesson: exact instructions, tested by hand and by machine, built a step at a time.

Sources

  1. K-12 Computer Science Framework. (2016). Framework statements by concept: Algorithms and Programming, program development. (Grades 6 to 8: people design meaningful solutions for others by defining a problem's criteria and constraints and testing whether they were met.) k12cs.org
  2. Raspberry Pi Foundation. (2026). Python path. Raspberry Pi Projects. (Free guided Python projects for the Code Editor.) projects.raspberrypi.org
  3. Code Club, Raspberry Pi Foundation. (2026). Accounts for creators. (Under-13s need a parent or guardian to set up an account; the Code Editor works without one.) codeclub.org
  4. Scratch Foundation. (2026). Scratch community guidelines. (Credit everyone whose work you used; keep personal information private.) Scratch
  5. Python Software Foundation. (2026). random: Generate pseudo-random numbers. (randint includes both end points.) docs.python.org
  6. Sentance, S., Waite, J., and Kallia, M. (2019). Teaching computer programming with PRIMM: A sociocultural perspective. Computer Science Education, 29(2-3), 136-176. https://doi.org/10.1080/08993408.2019.1608781 (Predict, Run, Investigate, Modify, Make.)
Key terms
Plan table
A short list of what a program must do, what would be nice, and what is left out for now.
Smallest working version
The simplest program that does the core job, built and tested before any other feature is added.
Working copy
A saved version that runs correctly, kept with Download before making the next change.
User testing
Watching a real person use your program without helping, and noting every point where they get stuck.
Credit
A note, such as a comment at the top of the code, saying whose work or idea you reused.
Program development
Planning, building, testing and improving a program for the people who will use it.

Open the interactive version with quizzes and progress →