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.
| Check | The question to ask | A step that fails it |
|---|---|---|
| Exact | Does every step have only one meaning? | "Add a little water." |
| Doable | Can the follower really do this step? | "Pick the funniest joke." A machine cannot judge funny. |
| In order | Will the steps work in this order? | "Butter the toast" coming before "Toast the bread." |
| Finishes | Is 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.
| Vague | Exact | What 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.
- Stand on S, facing the top of the page.
- Move forward 3 squares.
- Turn right 90 degrees.
- Move forward 2 squares.
- Turn right 90 degrees.
- 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 step | Across | Up | Facing |
|---|---|---|---|
| 1 | 0 | 0 | up |
| 2 | 0 | 3 | up |
| 3 | 0 | 3 | right |
| 4 | 2 | 3 | right |
| 5 | 2 | 3 | down |
| 6 | 2 | 0 | down |
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.
| Module | What you do | Where |
|---|---|---|
| 1 | Write and trace algorithms | Paper |
| 2 | Sequences, loops, events, variables and decisions with blocks | Scratch |
| 3 to 6 | The same ideas as typed code, then lists and functions | Python |
| 7 | Debugging as a method | Python |
| 8 and 9 | Projects: a drawing, a quiz game, a text adventure, two simulations and one of your own | Python |
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
- 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
- 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
- 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
- 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
- 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.
- Turn over the first card. Remember its number as BIGGEST SO FAR.
- 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.
- 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.
| Card | Number on it | Greater than BIGGEST SO FAR? | BIGGEST SO FAR afterwards |
|---|---|---|---|
| 1 | 14 | first card, so no question | 14 |
| 2 | 3 | no | 14 |
| 3 | 27 | yes | 27 |
| 4 | 9 | no | 27 |
| 5 | 27 | no: 27 is equal, not greater | 27 |
| 6 | 5 | no | 27 |
| 7 | 18 | no | 27 |
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.
| Structure | What it means | Pseudocode word | In Scratch | In Python |
|---|---|---|---|---|
| Sequence | Steps run one after another, in order | none: just the next line | Blocks stacked under each other | Lines written one below another |
| Selection | A step runs only if a condition is true | IF, ELSE | The light orange 'if then' and 'if then else' blocks | if, elif and else |
| Repetition | Steps run again and again | REPEAT | The light orange 'repeat', 'forever' and 'repeat until' blocks | for 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.
| Card | Number on it | Greater than BIGGEST SO FAR? | BIGGEST SO FAR afterwards |
|---|---|---|---|
| start | none | none | 0 |
| 1 | -4 | no | 0 |
| 2 | -9 | no | 0 |
| 3 | -2 | no | 0 |
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.
- Set LOW to 1 and HIGH to 100.
- 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 number | LOW | HIGH | Guess | Answer | Change |
|---|---|---|---|---|---|
| 1 | 1 | 100 | 50 | too high | HIGH becomes 49 |
| 2 | 1 | 49 | 25 | too low | LOW becomes 26 |
| 3 | 26 | 49 | 37 | correct | stop |
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
- 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.)
- Wikipedia. (2026). Structured program theorem. (Background on the Böhm and Jacopini result stated as sequence, selection and iteration.) Wikipedia
- 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.)
- Computer Science Education Research Group, University of Canterbury. (n.d.). Searching algorithms. CS Unplugged. (Sequential and binary searching taught without a computer.) CS Unplugged
- 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.
| Category | Colour | What its blocks do | One example |
|---|---|---|---|
| Motion | blue | Move, turn and place sprites | move 10 steps |
| Looks | purple | Speech bubbles, costumes, size | say Hello! for 2 seconds |
| Sound | pink | Play sounds | start sound Meow |
| Events | yellow | Start scripts when something happens | when green flag clicked |
| Control | light orange | Waiting, repeating and choosing | wait 1 seconds |
| Sensing | light blue | Notice touches, keys and the mouse | key space pressed? |
| Operators | green | Arithmetic, comparisons and random numbers | pick random 1 to 10 |
| Variables | dark orange | Store and change values | change my variable by 1 |
| My Blocks | pink-red | Blocks you design yourself | Make 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.
- The yellow when green flag clicked block, from Events. It has a rounded top, like a hat, because nothing can go above it.
- The blue go to x: 0 y: 0 block, from Motion. Set both numbers to 0.
- The purple say Hello! for 2 seconds block, from Looks.
- 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.
- when green flag clicked
- go to x: -200 y: 0
- move 50 steps
- move 50 steps
- move 50 steps
- 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.
| Block | Cat's x afterwards | Time since the click |
|---|---|---|
| go to x: -200 y: 0 | -200 | 0 seconds |
| move 50 steps | -150 | 0 seconds |
| wait 0.2 seconds | -150 | 0.2 seconds |
| move 50 steps | -100 | 0.2 seconds |
| wait 0.2 seconds | -100 | 0.4 seconds |
| move 50 steps | -50 | 0.4 seconds |
| wait 0.2 seconds | -50 | 0.6 seconds |
| move 50 steps | 0 | 0.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 finished | Starts it and moves on |
|---|---|
| say Hello! for 2 seconds: the next block runs after 2 seconds | say 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 ends | start 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 ends | go 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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.
- yellow: when green flag clicked
- Pen: erase all
- blue: go to x: 0 y: 0
- blue: point in direction 90
- Pen: pen down
- 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
- 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.
| Pass | x | y | Direction after the turn |
|---|---|---|---|
| start | 0 | 0 | 90 (right) |
| 1 | 100 | 0 | 180 (down) |
| 2 | 100 | -100 | -90 (left) |
| 3 | 0 | -100 | 0 (up) |
| 4 | 0 | 0 | 90 (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
| Block | How many passes | When it stops | Good for |
|---|---|---|---|
| repeat 10 | The number you type in it, known before it starts | After that many passes | Shapes, counting, an animation of a set length |
| forever | No limit | Never by itself: only the red stop sign or a stop block ends it | Games and anything that keeps checking or moving |
| repeat until, with a condition | Unknown in advance | When its condition becomes true, checked before each pass | Waiting 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.
| Shape | Sides: the repeat number | Turn at each corner | Total turning |
|---|---|---|---|
| Triangle | 3 | 120 | 360 |
| Square | 4 | 90 | 360 |
| Pentagon | 5 | 72 | 360 |
| Hexagon | 6 | 60 | 360 |
| Octagon | 8 | 45 | 360 |
| Almost a circle | 36 | 10 | 360 |
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:
- when green flag clicked, erase all, go to x: 0 y: 0, pen down
- 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.
| Block | Runs per outer pass | Outer passes | Total runs |
|---|---|---|---|
| move 60 steps | 4 | 12 | 48 |
| turn 90 degrees | 4 | 12 | 48 |
| turn 30 degrees | 1 | 12 | 12 |
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
- 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
- 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
- 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
- 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
- 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 block | Its script starts when | Typical use |
|---|---|---|
| when green flag clicked | The green flag above the stage is clicked | Setting things up at the start |
| when space key pressed | The chosen key is pressed: space, an arrow, a letter, a digit, or any key | Jumping, firing, pausing |
| when this sprite clicked | Someone clicks on this sprite | Buttons, things to catch or pop |
| when backdrop switches to | The stage changes to the chosen backdrop | Starting a new level or scene |
| when loudness > 10 | The sound level or the timer goes over a number | Clap-controlled projects, time limits |
| when I receive message1 | Any sprite or the stage broadcasts that message | One 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.
| Sprite | Waits for | Then does |
|---|---|---|
| Cat | green flag | goes to the start, gives instructions |
| Balloon | green flag | shows itself, then wanders forever |
| Cat | space key | jumps |
| Balloon | a click on itself | pops, hides, broadcasts popped |
| Cat | the message popped | says 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 pressed | forever, if key right arrow pressed? | |
|---|---|---|
| Who starts the action | The key press event | The loop, by checking |
| Holding the key | One move, a pause, then repeats | Smooth movement on every pass |
| Best for | One-off actions: jump, pause, fire | Continuous 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
- 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
- Scratch Wiki contributors. (2026). Broadcast () and wait (block). Scratch Wiki. (Waits until all scripts activated by the broadcast have ended.) Scratch Wiki
- Scratch Foundation. (2026). scratch-gui sprite library, sprites.json. GitHub. (The Balloon1 sprite and its Pop sound.) GitHub
- 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
- 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.
- when green flag clicked
- set score to 0
- go to x: (pick random -220 to 220) y: 170
- 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 happens | Block that runs | score afterwards |
|---|---|---|
| Green flag clicked | set score to 0 | 0 |
| First apple lands in the bowl | change score by 1 | 1 |
| Second apple lands in the bowl | change score by 1 | 2 |
| Third apple falls past the bowl | neither score block | 2 |
| Fourth apple lands in the bowl | change score by 1 | 3 |
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 is | How often it asks | Score when the apple lands |
|---|---|---|
| Inside the forever loop | On every pass, about 30 times a second | Goes up |
| Above the loop | Once, when the flag is clicked | Stays 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
- 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
- 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
- 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
- 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
- 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.
- Go to editor.raspberrypi.org. The page says "Start coding, no setup required!"
- Under "What do you want to code with?", choose Python.
- 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 block | Python | Lesson |
|---|---|---|
| when green flag clicked | Clicking Run: the program starts at line 1 | this one |
| say Hello! | print("Hello!") | this one |
| blocks stacked in order | lines written one below another | this one |
| wait 1 seconds | time.sleep(1), after import time | this one |
| set score to 0 | score = 0 | 8 |
| change score by 1 | score = score + 1 | 8 |
| ask and wait, then answer | input() | 9 |
| if then, if then else | if, else | 10 and 11 |
| repeat 10 | for count in range(10): | 12 |
| forever, repeat until | while | 13 |
| Make a Block | def | 15 |
| the red stop sign | the Stop button that replaces Run while a program runs | 13 |
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
- 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
- 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
- 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
- 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
- Trinket. (2026). Trinket has shut down. (Shut down on 31 August 2026 after being used by millions of learners and educators.) trinket.io
- 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
- 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
- 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.
| Line | What Python does | score afterwards | Output |
|---|---|---|---|
| 1 | store 0 under the name score | 0 | |
| 2 | print the value of score | 0 | 0 |
| 3 | store 10, replacing the 0 | 10 | |
| 4 | print the value | 10 | 10 |
| 5 | work out 10 + 5, then store 15 | 15 | |
| 6 | print the value | 15 | 15 |
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.
| Line | x | y |
|---|---|---|
| 1 | 5 | not yet made |
| 2 | 5 | 5 |
| 3 | 7 | 5 |
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.
| Line | left | right |
|---|---|---|
| 1 | apple | not yet made |
| 2 | apple | banana |
| 3 | banana | banana |
| 4 | banana | banana |
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'>
| Type | Short for | What it holds | Examples |
|---|---|---|---|
| str | string | Text, in quote marks | "Pixel", "3", "" |
| int | integer | Whole numbers, with no decimal point | 3, -40, 0 |
| float | floating-point number | Numbers with a decimal point | 2.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
| Operator | Meaning | 7 and 2 give |
|---|---|---|
| + and - | add, subtract | 9 and 5 |
| * | multiply: the star, not x | 14 |
| / | divide, always giving a float | 3.5 |
| // | divide and drop the remainder | 3 |
| % | the remainder after dividing | 1 |
| ** | power: 7 to the power 2 | 49 |
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.
| Name | Allowed? | Why |
|---|---|---|
| high_score | yes | letters and an underscore, and it says what it holds |
| player2 | yes | a digit is fine after the first character |
| 2nd_place | no | starts with a digit |
| my score | no | contains a space |
| high-score | no | the hyphen is read as a minus sign |
| s | allowed, but unhelpful | nobody 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
- 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
- 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
- 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
- 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.)
- 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".
| Line | first | second | first + 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.
| Expression | Result | Type of result |
|---|---|---|
| int("12") | 12 | int |
| float("12.5") | 12.5 | float |
| str(12) | "12" | str |
| int(" 12 ") | 12: spaces around the digits are ignored | int |
| int("-7") | -7 | int |
| int(12.5) | 12: a float value loses its fraction | int |
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
- 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
- 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
- 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
- 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
| Part | In the ride program | What it does |
|---|---|---|
| if | if | Starts the decision |
| A condition | height >= 120 | A question whose answer is True or False |
| A colon | : | Says that an indented block comes next |
| An indented block | print("You can ride.") | Runs only if the condition is True |
| else, with its own colon | else: | Optional: the other path |
| Another indented block | print("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 typed | With height >= 120 | With height > 120 |
|---|---|---|
| 135 | You can ride. | You can ride. |
| 110 | Sorry, not this time. | Sorry, not this time. |
| 120 | You 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
| Operator | Means | Example that is True |
|---|---|---|
| == | is equal to | 7 == 7 |
| != | is not equal to | 7 != 8 |
| < | is less than | 3 < 7 |
| > | is greater than | 7 > 3 |
| <= | is less than or equal to | 3 <= 3 |
| >= | is greater than or equal to | 120 >= 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
- 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
- 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
- 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
- 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:
- Check the first condition. If it is True, run its block and skip everything else in the chain.
- If it is False, check the next elif, and so on down.
- 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.
| Line | Condition | Value | What happens |
|---|---|---|---|
| 2 | score >= 50 | True | Print "Grade: C", then leave the chain |
| 4 | score >= 70 | never checked | skipped |
| 6 | score >= 85 | never checked | skipped |
| 8 | else | never reached | skipped |
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 typed | Output | Why this score |
|---|---|---|
| 95 | Grade: A | the score that exposed the bug |
| 85 | Grade: A | on the A boundary |
| 84 | Grade: B | just below it |
| 70 | Grade: B | on the B boundary |
| 69 | Grade: C | just below it |
| 50 | Grade: C | on the C boundary |
| 49 | Not passed yet | just 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.
| Operator | True when | Example |
|---|---|---|
| and | both sides are True | temperature >= 20 and temperature <= 30 |
| or | at least one side is True | age < 13 or age >= 65 |
| not | the condition after it is False | not 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
- 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
- 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
- Python Software Foundation. (2026). Built-in types: Truth value testing. (Empty sequences such as '' are false; other strings count as true.) docs.python.org
- 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.
| Pass | number | What the body prints |
|---|---|---|
| 1 | 1 | 7 x 1 = 7 |
| 2 | 2 | 7 x 2 = 14 |
| ... | ... | ... |
| 10 | 10 | 7 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)
| Form | Gives | Reads as |
|---|---|---|
| range(5) | 0 1 2 3 4 | five numbers, starting at 0 |
| range(1, 6) | 1 2 3 4 5 | start at 1, stop before 6 |
| range(0, 20, 5) | 0 5 10 15 | start at 0, step 5, stop before 20 |
| range(10, 0, -2) | 10 8 6 4 2 | count down in 2s, stop before 0 |
| range(5, 5) | nothing | start 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
| Pass | n | total after the reset | total after adding |
|---|---|---|---|
| 1 | 1 | 0 | 1 |
| 2 | 2 | 0 | 2 |
| 3 | 3 | 0 | 3 |
| 4 | 4 | 0 | 4 |
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
- 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
- Python Software Foundation. (2026). Built-in types: Ranges, and common sequence operations. (range objects; the in operator for strings.) docs.python.org
- Scratch Wiki contributors. (2026). Repeat () (block). Scratch Wiki. (The Scratch loop that a for loop over range replaces.) Scratch Wiki
- 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 number | count | count <= 5? | Output |
|---|---|---|---|
| 1 | 1 | True | Lap 1 |
| 2 | 1 | True | Lap 1 |
| 3 | 1 | True | Lap 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 number | count | count <= 5? | Output, then count becomes |
|---|---|---|---|
| 1 | 1 | True | Lap 1, then 2 |
| 2 | 2 | True | Lap 2, then 3 |
| 3 | 3 | True | Lap 3, then 4 |
| 4 | 4 | True | Lap 4, then 5 |
| 5 | 5 | True | Lap 5, then 6 |
| 6 | 6 | False | leave 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.
| Scratch | Python |
|---|---|
| repeat until touching edge? | while not touching_edge: |
| repeat until score > 9 | while score <= 9: |
| forever | while 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
| for | while | |
|---|---|---|
| Use it when | You know the number of passes, or you are walking through a sequence | You repeat until something changes, and cannot know how many passes in advance |
| Moves on by itself? | Yes, to the next value | No: your code must change something |
| Risk | Off by one | Never stopping |
| Example | The 7 times table | Asking 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
- 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
- Python Software Foundation. (2026). random: Generate pseudo-random numbers. (randint returns a random integer N with a <= N <= b.) docs.python.org
- 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
- 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
- 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.
| Day | Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|---|
| Value | 6200 | 8150 | 4300 | 10020 | 7600 | 12340 | 5100 |
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| 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
- 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
- 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
- Python Software Foundation. (2026). Built-in functions: len, sum, max and min. docs.python.org
- 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
| Scratch | Python |
|---|---|
| Click Make a Block and type a name | Write def, the name, brackets and a colon |
| The define hat block | The def line itself |
| Blocks stacked under define | The indented lines under def, called the body |
| Add an input to the block | A name inside the brackets of the def line, called a parameter |
| Dragging your block into a script | Writing the name with brackets, such as divider(), called a call |
| Typing a value into the block's input | Putting a value in the call's brackets, called an argument |
| The block belongs to one sprite | The 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.
| Step | Line running | Output |
|---|---|---|
| 1 | the def lines: two functions stored, nothing printed | |
| 2 | divider() jumps into divider, which prints, then returns | ================== |
| 3 | show_score("Robin", 12) jumps in with player = "Robin" and points = 12 | Robin: 12 points |
| 4 | show_score("Sky", 9) jumps in with player = "Sky" and points = 9 | Sky: 9 points |
| 5 | divider() 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
- 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
- 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
- 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
- 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
- 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 value | The person looking at the screen | The line that called the function |
| Can the program use it later? | No | Yes: store it, add to it, compare it |
| Does the function carry on? | Yes, to the next line | No, it ends at once |
| What the call gives back | None, if there is no return | The 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
| Test | Expected | Got |
|---|---|---|
| winner(5, 2) | Player 1 | Player 1 |
| winner(1, 6) | Player 2 | Player 2 |
| winner(3, 3), the edge case | a draw | nobody, 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
- 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
- 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
- Python Software Foundation. (2026). random: Generate pseudo-random numbers. (randint for the dice rolls.) docs.python.org
- 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
| Part | What 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 average | Inside the function average, line 2 was running when it stopped. |
| The code line with ~ and ^ under it | The exact part of the line Python was working on. |
| ZeroDivisionError: division by zero | The 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 type | Usually means | Real message | First thing to check |
|---|---|---|---|
| SyntaxError | Python cannot read the line | '(' was never closed, or expected ':' | Brackets, quote marks and colons in pairs and in place |
| IndentationError | Spaces at the start of a line are wrong | unexpected indent, or expected an indented block after 'if' statement on line 2 | Four spaces under if, for, while and def, and none elsewhere |
| NameError | A name Python does not know | name 'score' is not defined. Did you mean: 'scroe'? | Spelling, capitals, and whether the name was set before this line |
| TypeError | The wrong type of value for an operation | can only concatenate str (not "int") to str | Mixing strings and numbers; the number of arguments in a call |
| ValueError | The right type but an unusable value | invalid literal for int() with base 10: 'twelve' | What the user actually typed |
| IndexError | A list position that does not exist | list index out of range | Indexes start at 0 and end at the length minus 1 |
| ZeroDivisionError | Dividing by zero | division by zero | Where the zero came from, such as an empty list |
| AttributeError | A 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
- Read the last line: the error type and the description.
- Locate the line it names, then look at the lines just above it.
- 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."
- 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
- 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
- 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
- Python Software Foundation. (2026). Built-in exceptions. (TypeError, ValueError, IndexError, NameError, ZeroDivisionError, AttributeError and the rest.) docs.python.org
- 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.)
- 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 test | What it is | For the passing count |
|---|---|---|
| Normal | Ordinary, everyday input | scores well above and below 50 |
| Boundary | Exactly on, just below and just above a limit | 49, 50 and 51 |
| Edge case | Input at the extremes of what is possible | an 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.
| Symptom | Expected | Hypothesis | Change | Result |
|---|---|---|---|---|
| Average 193.33 | 80 | Only third is divided | Added brackets | Fixed: 80.0 |
| 3 passed | 4 | > leaves out 50 | Changed > 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
- Python Software Foundation. (2026). An informal introduction to Python: Numbers. The Python Tutorial. (The arithmetic operators, with brackets used for grouping.) docs.python.org
- Python Software Foundation. (2026). Expressions: Operator precedence. The Python Language Reference. (The precedence table, in which / binds more tightly than +.) docs.python.org
- 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.)
- 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
| Stage | What it adds | Idea it uses |
|---|---|---|
| 1 | A square | for loops and the 360 rule |
| 2 | One function for any regular shape | functions with parameters |
| 3 | A house of three filled shapes | functions calling functions |
| 4 | A dark sky, stars and a moon | random numbers in a loop |
| 5 | Ground and a row of trees | range 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:
| Symptom | Likely cause |
|---|---|
| Thin lines join shapes that should be separate | The pen was not lifted: use move_to, with penup and pendown |
| An outline appears but no colour | end_fill() is missing, or begin_fill() came after the shape |
| A shape comes out tilted | The heading was not reset with setheading(0) |
| A triangle does not close | The turn is the inside angle, 60, instead of 120 |
| Part of the scene is missing | It 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
- 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
- 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
- Python Software Foundation. (2026). random: Generate pseudo-random numbers. (randint for the star positions.) docs.python.org
- 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
| Version | New feature | Idea it uses |
|---|---|---|
| 1 | One question, forgiving of spaces and capitals | if, strip, lower |
| 2 | Several questions and a score | lists, a for loop, a running total |
| 3 | More than one right answer | the in operator on a list |
| 4 | Each question kept with its answers | a list of lists |
| 5 | Shuffled order and a final verdict | functions, 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.
| Test | Expected | Why this test |
|---|---|---|
| Answer " DEF" to the def question | Correct! | spaces and capitals |
| Answer five to the len question | Correct! | a second accepted answer |
| Press Enter without typing anything | Not quite | an empty reply |
| All 5 right | 100%, Perfect! | the top boundary |
| Exactly 3 right | 60%, Well done. | the 60% boundary |
| Exactly 2 right | 40%, 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
- Python Software Foundation. (2026). Built-in types: str.strip and str.lower; membership tests with in. docs.python.org
- Python Software Foundation. (2026). Built-in functions: repr and len. (repr returns a string containing a printable representation of an object.) docs.python.org
- Python Software Foundation. (2026). random: Generate pseudo-random numbers. (shuffle shuffles a sequence in place.) docs.python.org
- 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 game | Python idea | From lesson |
|---|---|---|
| Where the player is now | A variable, room, holding a room name | 8 |
| Reading commands | input, tidied with strip and lower | 9 and 10 |
| Each room's rules | if, elif and else | 10 and 11 |
| Keep playing until the end | A while loop, the game loop | 13 |
| What the player carries | A list, inventory, with append and in | 14 |
| One room's code in one place | A function per room that returns the next room | 15 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.
| Room | What the player sees | Commands | Needs |
|---|---|---|---|
| entrance | The mouth of a cave; a brass lamp on a rock | take, north, quit | nothing |
| tunnel | A narrow tunnel; a small key in the dust | take, east, south, quit | the lamp, or it is too dark |
| door | A heavy wooden door to the north | north, west, quit | the key, to open the door |
| treasure | Gold: the game is won | none | reached 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.
| Room | Situation | Command | Expected |
|---|---|---|---|
| entrance | no lamp | north | tunnel says pitch dark, back to entrance |
| entrance | no lamp | take | lamp added; it is no longer described |
| entrance | has lamp | take | You can't do that here. |
| tunnel | has lamp | east | door |
| door | no key | north | The door is locked. |
| door | has key | north | treasure: You win! |
| any room | any | dance | You can't do that here. |
| any room | any | quit | You 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
- 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
- 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
- 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.
| Total | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Ways out of 36 | 1 | 2 | 3 | 4 | 5 | 6 | 5 | 4 | 3 | 2 | 1 |
| Expected in 3600 rolls | 100 | 200 | 300 | 400 | 500 | 600 | 500 | 400 | 300 | 200 | 100 |
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
- 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
- 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.)
- 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
- 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 year | In words | In Python |
|---|---|---|
| 1 | Every pair of rabbits has one baby | rabbits = rabbits + rabbits // 2 |
| 2 | The field feeds at most 1000 rabbits | rabbits = min(rabbits, 1000) |
| 3 | Foxes catch rabbits; more of either means more meetings | eaten = min(rabbits, foxes * rabbits // 200) |
| 4 | The caught rabbits are gone | rabbits = rabbits - eaten |
| 5 | One fox cub for every 10 rabbits eaten, and one fox in five dies | foxes = 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
- 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
- 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
- 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
- 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.)
- 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.